1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137import type { SQSEvent, SQSRecord } from "aws-lambda";
import middy from "@middy/core";
import {
BatchProcessor,
EventType,
processPartialResponse,
} from "@aws-lambda-powertools/batch";
import type { PartialItemFailureResponse } from "@aws-lambda-powertools/batch/types";
import { logMetrics } from "@aws-lambda-powertools/metrics/middleware";
import { MetricUnit } from "@aws-lambda-powertools/metrics";
import { logger, metrics } from "./telemetry.ts";
import scan from "./scan.ts";
import convertToEqualifyV2 from "../../../shared/convertors/AxeToEqualify2.ts"
const processor = new BatchProcessor(EventType.SQS);
const RESULTS_ENDPOINT_PROD = "https://api.equalifyapp.com/public/scanWebhook";
const RESULTS_ENDPOINT_STAGING = "https://api-staging.equalifyapp.com/public/scanWebhook";
const getResultsEndpoint = (isStaging?: boolean) => isStaging ? RESULTS_ENDPOINT_STAGING : RESULTS_ENDPOINT_PROD;
// Process a single SQS Record
const recordHandler = async (record: SQSRecord): Promise<void> => {
metrics.captureColdStartMetric();
const startTime = performance.now();
const payload = record.body;
const payloadParsed = JSON.parse(payload);
const job = payloadParsed.data;
logger.info(`Processing job: ${JSON.stringify(job)}`);
if (payload) {
try {
metrics.addMetric("scansStarted", MetricUnit.Count, 1);
// Wrap scan in timeout to prevent Lambda from hanging
const SCAN_TIMEOUT = 2*60*1000; // 2 minutes max for entire scan process
const scanPromise = scan(job).then((result) => {
const endTime = performance.now();
const executionDuration = endTime - startTime;
metrics.addMetric(
"ScanDuration",
MetricUnit.Milliseconds,
executionDuration
);
return result;
});
const timeoutPromise = new Promise<typeof scanPromise>((_, reject) =>
setTimeout(() => reject(new Error(`Scan timeout after ${SCAN_TIMEOUT}ms`)), SCAN_TIMEOUT)
);
const results = await Promise.race([scanPromise, timeoutPromise]);
if(results){
logger.info(`Job [auditId: ${job.auditId}, scanId: ${job.scanId}, urlId: ${job.urlId}] Scan Complete!`);
if(results.axeresults){
const convertedResults = convertToEqualifyV2(results.axeresults, job);
// shim the results payload object with status when we have results
convertedResults.status = results.status;
logger.info("Converted results:", JSON.stringify(convertedResults));
try {
const sendResultsResponse = await fetch(getResultsEndpoint(job.isStaging), {
method: 'post',
body: JSON.stringify(convertedResults),
headers: {'Content-Type': 'application/json'}
});
// FIX: Properly await the json() promise
const responseData = await sendResultsResponse.json() as any;
if (!sendResultsResponse.ok) {
// Log but don't throw - let the message be deleted if scan succeeded
logger.error(`Webhook failed with status ${sendResultsResponse.status}`, responseData);
} else {
logger.info("HTML-scan Results sent to API!", responseData);
}
} catch (webhookError) {
// Log webhook errors but don't fail the entire message
logger.error("Failed to send results to webhook", webhookError as Error);
// Decide: throw here if you want to retry, or continue to mark as processed
}
} else {
logger.error("Error converting to EqualifyV2 format:", JSON.stringify(results));
}
} else {
// Scan failed or returned no results - notify webhook of failure
logger.error(`Job [auditId: ${job.auditId}, scanId: ${job.scanId}, urlId: ${job.urlId}] Scan failed - no results returned`);
try {
const failurePayload = {
auditId: job.auditId,
scanId: job.scanId,
urlId: job.urlId,
url: job.url,
status: 'failed',
error: 'Scan failed to produce results',
blockers: []
};
const sendResultsResponse = await fetch(getResultsEndpoint(job.isStaging), {
method: 'post',
body: JSON.stringify(failurePayload),
headers: {'Content-Type': 'application/json'}
});
if (!sendResultsResponse.ok) {
logger.error(`Failed to send failure notification to webhook`);
}
} catch (webhookError) {
logger.error("Failed to send failure notification", webhookError as Error);
}
}
} catch (error) {
logger.error("Scan Error!", error as string);
throw error; // Only throw for actual scan failures
}
}
metrics.publishStoredMetrics();
return; // Success - message will be deleted
};
// handle batch
const batchHandler = async (event: SQSEvent, context: any) =>
processPartialResponse(event, recordHandler, processor, {
context,
throwOnFullBatchFailure: false,
processInParallel: false,
});
// finally, export the handler
export const handler = middy<SQSEvent, PartialItemFailureResponse>(
batchHandler
).use(logMetrics(metrics, { captureColdStartMetric: true }));