@checkstack/healthcheck-http-backend 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/package.json +1 -1
- package/src/index.ts +7 -5
- package/src/request-collector.test.ts +212 -0
- package/src/request-collector.ts +186 -0
- package/src/strategy.test.ts +174 -326
- package/src/strategy.ts +97 -419
- package/src/transport-client.ts +29 -0
package/src/strategy.ts
CHANGED
|
@@ -1,188 +1,115 @@
|
|
|
1
|
-
import { JSONPath } from "jsonpath-plus";
|
|
2
1
|
import {
|
|
3
2
|
HealthCheckStrategy,
|
|
4
|
-
HealthCheckResult,
|
|
5
3
|
HealthCheckRunForAggregation,
|
|
6
4
|
Versioned,
|
|
7
5
|
z,
|
|
8
|
-
|
|
9
|
-
timeThresholdField,
|
|
10
|
-
stringField,
|
|
11
|
-
evaluateAssertions,
|
|
6
|
+
type ConnectedClient,
|
|
12
7
|
} from "@checkstack/backend-api";
|
|
13
8
|
import {
|
|
14
9
|
healthResultNumber,
|
|
15
10
|
healthResultString,
|
|
16
11
|
} from "@checkstack/healthcheck-common";
|
|
12
|
+
import type {
|
|
13
|
+
HttpTransportClient,
|
|
14
|
+
HttpRequest,
|
|
15
|
+
HttpResponse,
|
|
16
|
+
} from "./transport-client";
|
|
17
17
|
|
|
18
18
|
// ============================================================================
|
|
19
19
|
// SCHEMAS
|
|
20
20
|
// ============================================================================
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Header configuration for custom HTTP headers.
|
|
24
|
-
*/
|
|
25
|
-
export const httpHeaderSchema = z.object({
|
|
26
|
-
name: z.string().min(1).describe("Header name"),
|
|
27
|
-
value: z.string().describe("Header value"),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* JSONPath assertion for response body validation.
|
|
32
|
-
* Supports dynamic operators with runtime type coercion.
|
|
33
|
-
*/
|
|
34
|
-
const jsonPathAssertionSchema = z.object({
|
|
35
|
-
field: z.literal("jsonPath"),
|
|
36
|
-
path: z
|
|
37
|
-
.string()
|
|
38
|
-
.describe("JSONPath expression (e.g. $.status, $.data[0].id)"),
|
|
39
|
-
operator: z.enum([
|
|
40
|
-
"equals",
|
|
41
|
-
"notEquals",
|
|
42
|
-
"contains",
|
|
43
|
-
"startsWith",
|
|
44
|
-
"endsWith",
|
|
45
|
-
"matches",
|
|
46
|
-
"exists",
|
|
47
|
-
"notExists",
|
|
48
|
-
"lessThan",
|
|
49
|
-
"lessThanOrEqual",
|
|
50
|
-
"greaterThan",
|
|
51
|
-
"greaterThanOrEqual",
|
|
52
|
-
]),
|
|
53
|
-
value: z
|
|
54
|
-
.string()
|
|
55
|
-
.optional()
|
|
56
|
-
.describe("Expected value (not needed for exists/notExists)"),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Response header assertion schema.
|
|
61
|
-
* Check for specific header values in the response.
|
|
62
|
-
*/
|
|
63
|
-
const headerAssertionSchema = z.object({
|
|
64
|
-
field: z.literal("header"),
|
|
65
|
-
headerName: z.string().describe("Response header name to check"),
|
|
66
|
-
operator: z.enum([
|
|
67
|
-
"equals",
|
|
68
|
-
"notEquals",
|
|
69
|
-
"contains",
|
|
70
|
-
"startsWith",
|
|
71
|
-
"endsWith",
|
|
72
|
-
"exists",
|
|
73
|
-
]),
|
|
74
|
-
value: z.string().optional().describe("Expected header value"),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* HTTP health check assertion schema using discriminated union.
|
|
79
|
-
*
|
|
80
|
-
* Assertions validate the result of a check:
|
|
81
|
-
* - statusCode: Validate HTTP status code
|
|
82
|
-
* - responseTime: Validate response latency
|
|
83
|
-
* - contentType: Validate Content-Type header
|
|
84
|
-
* - header: Validate any response header
|
|
85
|
-
* - jsonPath: Validate JSON response body content
|
|
86
|
-
*/
|
|
87
|
-
const httpAssertionSchema = z.discriminatedUnion("field", [
|
|
88
|
-
numericField("statusCode", { min: 100, max: 599 }),
|
|
89
|
-
timeThresholdField("responseTime"),
|
|
90
|
-
stringField("contentType"),
|
|
91
|
-
headerAssertionSchema,
|
|
92
|
-
jsonPathAssertionSchema,
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
export type HttpAssertion = z.infer<typeof httpAssertionSchema>;
|
|
96
|
-
|
|
97
22
|
/**
|
|
98
23
|
* HTTP health check configuration schema.
|
|
99
|
-
*
|
|
100
|
-
* Config defines HOW to run the check (connection details, request setup).
|
|
101
|
-
* Assertions define WHAT to validate in the result.
|
|
24
|
+
* Global defaults only - action params moved to RequestCollector.
|
|
102
25
|
*/
|
|
103
26
|
export const httpHealthCheckConfigSchema = z.object({
|
|
104
|
-
url: z.string().url().describe("The full URL of the endpoint to check."),
|
|
105
|
-
method: z
|
|
106
|
-
.enum(["GET", "POST", "PUT", "DELETE", "HEAD"])
|
|
107
|
-
.default("GET")
|
|
108
|
-
.describe("The HTTP method to use for the request."),
|
|
109
|
-
headers: z
|
|
110
|
-
.array(httpHeaderSchema)
|
|
111
|
-
.optional()
|
|
112
|
-
.describe("Custom HTTP headers to send with the request."),
|
|
113
27
|
timeout: z
|
|
114
28
|
.number()
|
|
29
|
+
.int()
|
|
115
30
|
.min(100)
|
|
116
|
-
.default(
|
|
117
|
-
.describe("
|
|
118
|
-
body: z
|
|
119
|
-
.string()
|
|
120
|
-
.optional()
|
|
121
|
-
.describe(
|
|
122
|
-
"Optional request payload body (e.g. JSON for POST requests). [textarea]"
|
|
123
|
-
),
|
|
124
|
-
assertions: z
|
|
125
|
-
.array(httpAssertionSchema)
|
|
126
|
-
.optional()
|
|
127
|
-
.describe("Validation conditions for the response."),
|
|
31
|
+
.default(30_000)
|
|
32
|
+
.describe("Default request timeout in milliseconds"),
|
|
128
33
|
});
|
|
129
34
|
|
|
130
35
|
export type HttpHealthCheckConfig = z.infer<typeof httpHealthCheckConfigSchema>;
|
|
131
36
|
|
|
37
|
+
// Legacy config types for migrations
|
|
38
|
+
interface HttpConfigV1 {
|
|
39
|
+
url: string;
|
|
40
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
|
|
41
|
+
headers?: { name: string; value: string }[];
|
|
42
|
+
body?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface HttpConfigV2 extends HttpConfigV1 {
|
|
46
|
+
timeout: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
132
49
|
/** Per-run result metadata */
|
|
133
50
|
const httpResultMetadataSchema = z.object({
|
|
134
|
-
statusCode: healthResultNumber({
|
|
135
|
-
"x-chart-type": "pie",
|
|
136
|
-
"x-chart-label": "Status Code",
|
|
137
|
-
}).optional(),
|
|
138
|
-
contentType: healthResultString({
|
|
139
|
-
"x-chart-type": "counter",
|
|
140
|
-
"x-chart-label": "Content Type",
|
|
141
|
-
}).optional(),
|
|
142
|
-
failedAssertion: httpAssertionSchema.optional(),
|
|
143
51
|
error: healthResultString({
|
|
144
52
|
"x-chart-type": "status",
|
|
145
53
|
"x-chart-label": "Error",
|
|
146
54
|
}).optional(),
|
|
147
55
|
});
|
|
148
56
|
|
|
149
|
-
|
|
57
|
+
type HttpResultMetadata = z.infer<typeof httpResultMetadataSchema>;
|
|
150
58
|
|
|
151
59
|
/** Aggregated metadata for buckets */
|
|
152
60
|
const httpAggregatedMetadataSchema = z.object({
|
|
153
|
-
statusCodeCounts: z.record(z.string(), z.number()).meta({
|
|
154
|
-
"x-chart-type": "pie",
|
|
155
|
-
"x-chart-label": "Status Code Distribution",
|
|
156
|
-
}),
|
|
157
61
|
errorCount: healthResultNumber({
|
|
158
62
|
"x-chart-type": "counter",
|
|
159
63
|
"x-chart-label": "Errors",
|
|
160
64
|
}),
|
|
161
65
|
});
|
|
162
66
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
67
|
+
type HttpAggregatedMetadata = z.infer<typeof httpAggregatedMetadataSchema>;
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// STRATEGY
|
|
71
|
+
// ============================================================================
|
|
166
72
|
|
|
167
73
|
export class HttpHealthCheckStrategy
|
|
168
74
|
implements
|
|
169
75
|
HealthCheckStrategy<
|
|
170
76
|
HttpHealthCheckConfig,
|
|
77
|
+
HttpTransportClient,
|
|
171
78
|
HttpResultMetadata,
|
|
172
79
|
HttpAggregatedMetadata
|
|
173
80
|
>
|
|
174
81
|
{
|
|
175
82
|
id = "http";
|
|
176
83
|
displayName = "HTTP/HTTPS Health Check";
|
|
177
|
-
description = "HTTP endpoint health monitoring
|
|
84
|
+
description = "HTTP endpoint health monitoring";
|
|
178
85
|
|
|
179
86
|
config: Versioned<HttpHealthCheckConfig> = new Versioned({
|
|
180
|
-
version:
|
|
87
|
+
version: 3, // v3 for createClient pattern with action params moved to RequestCollector
|
|
181
88
|
schema: httpHealthCheckConfigSchema,
|
|
89
|
+
migrations: [
|
|
90
|
+
{
|
|
91
|
+
fromVersion: 1,
|
|
92
|
+
toVersion: 2,
|
|
93
|
+
description: "Add timeout field",
|
|
94
|
+
migrate: (data: HttpConfigV1): HttpConfigV2 => ({
|
|
95
|
+
...data,
|
|
96
|
+
timeout: 30_000,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
fromVersion: 2,
|
|
101
|
+
toVersion: 3,
|
|
102
|
+
description:
|
|
103
|
+
"Remove url/method/headers/body (moved to RequestCollector)",
|
|
104
|
+
migrate: (data: HttpConfigV2): HttpHealthCheckConfig => ({
|
|
105
|
+
timeout: data.timeout,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
182
109
|
});
|
|
183
110
|
|
|
184
111
|
result: Versioned<HttpResultMetadata> = new Versioned({
|
|
185
|
-
version:
|
|
112
|
+
version: 3,
|
|
186
113
|
schema: httpResultMetadataSchema,
|
|
187
114
|
});
|
|
188
115
|
|
|
@@ -194,320 +121,71 @@ export class HttpHealthCheckStrategy
|
|
|
194
121
|
aggregateResult(
|
|
195
122
|
runs: HealthCheckRunForAggregation<HttpResultMetadata>[]
|
|
196
123
|
): HttpAggregatedMetadata {
|
|
197
|
-
const statusCodeCounts: Record<string, number> = {};
|
|
198
124
|
let errorCount = 0;
|
|
199
125
|
|
|
200
126
|
for (const run of runs) {
|
|
201
127
|
if (run.metadata?.error) {
|
|
202
128
|
errorCount++;
|
|
203
129
|
}
|
|
204
|
-
|
|
205
|
-
if (run.metadata?.statusCode !== undefined) {
|
|
206
|
-
const key = String(run.metadata.statusCode);
|
|
207
|
-
statusCodeCounts[key] = (statusCodeCounts[key] || 0) + 1;
|
|
208
|
-
}
|
|
209
130
|
}
|
|
210
131
|
|
|
211
|
-
return {
|
|
132
|
+
return { errorCount };
|
|
212
133
|
}
|
|
213
134
|
|
|
214
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Create an HTTP transport client for one-shot requests.
|
|
137
|
+
* All request parameters come from the collector (RequestCollector).
|
|
138
|
+
*/
|
|
139
|
+
async createClient(
|
|
215
140
|
config: HttpHealthCheckConfig
|
|
216
|
-
): Promise<
|
|
217
|
-
// Validate and apply defaults from schema
|
|
141
|
+
): Promise<ConnectedClient<HttpTransportClient>> {
|
|
218
142
|
const validatedConfig = this.config.validate(config);
|
|
219
143
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
headersRecord[header.name] = header.value;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const response = await fetch(validatedConfig.url, {
|
|
231
|
-
method: validatedConfig.method,
|
|
232
|
-
headers: headersRecord,
|
|
233
|
-
body: validatedConfig.body,
|
|
234
|
-
signal: AbortSignal.timeout(validatedConfig.timeout),
|
|
235
|
-
});
|
|
236
|
-
const end = performance.now();
|
|
237
|
-
const latencyMs = Math.round(end - start);
|
|
238
|
-
|
|
239
|
-
// Collect response data for assertions
|
|
240
|
-
const statusCode = response.status;
|
|
241
|
-
const contentType = response.headers.get("content-type") || "";
|
|
242
|
-
|
|
243
|
-
// Collect response headers for header assertions
|
|
244
|
-
// Note: We get headers directly in the assertion loop, not pre-collected
|
|
245
|
-
|
|
246
|
-
// Build values object for standard assertions
|
|
247
|
-
const assertionValues: Record<string, unknown> = {
|
|
248
|
-
statusCode,
|
|
249
|
-
responseTime: latencyMs,
|
|
250
|
-
contentType,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// Separate assertions by type
|
|
254
|
-
const standardAssertions: Array<{
|
|
255
|
-
field: string;
|
|
256
|
-
operator: string;
|
|
257
|
-
value?: unknown;
|
|
258
|
-
}> = [];
|
|
259
|
-
const headerAssertions: Array<z.infer<typeof headerAssertionSchema>> = [];
|
|
260
|
-
const jsonPathAssertions: Array<z.infer<typeof jsonPathAssertionSchema>> =
|
|
261
|
-
[];
|
|
262
|
-
|
|
263
|
-
for (const assertion of validatedConfig.assertions || []) {
|
|
264
|
-
if (assertion.field === "header") {
|
|
265
|
-
headerAssertions.push(
|
|
266
|
-
assertion as z.infer<typeof headerAssertionSchema>
|
|
267
|
-
);
|
|
268
|
-
} else if (assertion.field === "jsonPath") {
|
|
269
|
-
jsonPathAssertions.push(
|
|
270
|
-
assertion as z.infer<typeof jsonPathAssertionSchema>
|
|
271
|
-
);
|
|
272
|
-
} else {
|
|
273
|
-
standardAssertions.push(assertion);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Evaluate standard assertions (statusCode, responseTime, contentType)
|
|
278
|
-
const failedStandard = evaluateAssertions(
|
|
279
|
-
standardAssertions,
|
|
280
|
-
assertionValues
|
|
281
|
-
);
|
|
282
|
-
if (failedStandard) {
|
|
283
|
-
return {
|
|
284
|
-
status: "unhealthy",
|
|
285
|
-
latencyMs,
|
|
286
|
-
message: `Assertion failed: ${failedStandard.field} ${
|
|
287
|
-
failedStandard.operator
|
|
288
|
-
} ${"value" in failedStandard ? failedStandard.value : ""}`,
|
|
289
|
-
metadata: {
|
|
290
|
-
statusCode,
|
|
291
|
-
contentType,
|
|
292
|
-
failedAssertion: failedStandard as HttpAssertion,
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Evaluate header assertions
|
|
298
|
-
for (const headerAssertion of headerAssertions) {
|
|
299
|
-
// Get header value directly from the response
|
|
300
|
-
const headerValue =
|
|
301
|
-
response.headers.get(headerAssertion.headerName) ?? undefined;
|
|
302
|
-
const passed = this.evaluateHeaderAssertion(
|
|
303
|
-
headerAssertion.operator,
|
|
304
|
-
headerValue,
|
|
305
|
-
headerAssertion.value
|
|
144
|
+
const client: HttpTransportClient = {
|
|
145
|
+
async exec(request: HttpRequest): Promise<HttpResponse> {
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timeoutId = setTimeout(
|
|
148
|
+
() => controller.abort(),
|
|
149
|
+
request.timeout ?? validatedConfig.timeout
|
|
306
150
|
);
|
|
307
151
|
|
|
308
|
-
if (!passed) {
|
|
309
|
-
return {
|
|
310
|
-
status: "unhealthy",
|
|
311
|
-
latencyMs,
|
|
312
|
-
message: `Header assertion failed: ${headerAssertion.headerName} ${
|
|
313
|
-
headerAssertion.operator
|
|
314
|
-
} ${headerAssertion.value || ""}. Actual: ${
|
|
315
|
-
headerValue ?? "(missing)"
|
|
316
|
-
}`,
|
|
317
|
-
metadata: {
|
|
318
|
-
statusCode,
|
|
319
|
-
contentType,
|
|
320
|
-
failedAssertion: headerAssertion,
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Evaluate JSONPath assertions (only if present)
|
|
327
|
-
if (jsonPathAssertions.length > 0) {
|
|
328
|
-
let responseData: unknown;
|
|
329
|
-
|
|
330
152
|
try {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
} catch {
|
|
338
|
-
return {
|
|
339
|
-
status: "unhealthy",
|
|
340
|
-
latencyMs,
|
|
341
|
-
message:
|
|
342
|
-
"Response is not valid JSON, but JSONPath assertions are configured",
|
|
343
|
-
metadata: { statusCode, contentType },
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
} catch (error_: unknown) {
|
|
348
|
-
return {
|
|
349
|
-
status: "unhealthy",
|
|
350
|
-
latencyMs,
|
|
351
|
-
message: `Failed to parse response body: ${
|
|
352
|
-
(error_ as Error).message
|
|
353
|
-
}`,
|
|
354
|
-
metadata: { statusCode },
|
|
355
|
-
};
|
|
356
|
-
}
|
|
153
|
+
const response = await fetch(request.url, {
|
|
154
|
+
method: request.method,
|
|
155
|
+
headers: request.headers,
|
|
156
|
+
body: request.body,
|
|
157
|
+
signal: controller.signal,
|
|
158
|
+
});
|
|
357
159
|
|
|
358
|
-
|
|
359
|
-
JSONPath({ path, json: json as object, wrap: false });
|
|
160
|
+
clearTimeout(timeoutId);
|
|
360
161
|
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const passed = this.evaluateJsonPathAssertion(
|
|
364
|
-
jsonPathAssertion.operator,
|
|
365
|
-
actualValue,
|
|
366
|
-
jsonPathAssertion.value
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
if (!passed) {
|
|
370
|
-
return {
|
|
371
|
-
status: "unhealthy",
|
|
372
|
-
latencyMs,
|
|
373
|
-
message: `JSONPath assertion failed: [${
|
|
374
|
-
jsonPathAssertion.path
|
|
375
|
-
}] ${jsonPathAssertion.operator} ${
|
|
376
|
-
jsonPathAssertion.value || ""
|
|
377
|
-
}. Actual: ${JSON.stringify(actualValue)}`,
|
|
378
|
-
metadata: {
|
|
379
|
-
statusCode,
|
|
380
|
-
contentType,
|
|
381
|
-
failedAssertion: jsonPathAssertion,
|
|
382
|
-
},
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const assertionCount = validatedConfig.assertions?.length || 0;
|
|
389
|
-
return {
|
|
390
|
-
status: "healthy",
|
|
391
|
-
latencyMs,
|
|
392
|
-
message: `HTTP ${statusCode}${
|
|
393
|
-
assertionCount > 0 ? ` - passed ${assertionCount} assertion(s)` : ""
|
|
394
|
-
}`,
|
|
395
|
-
metadata: { statusCode, contentType },
|
|
396
|
-
};
|
|
397
|
-
} catch (error: unknown) {
|
|
398
|
-
const end = performance.now();
|
|
399
|
-
const isError = error instanceof Error;
|
|
400
|
-
return {
|
|
401
|
-
status: "unhealthy",
|
|
402
|
-
latencyMs: Math.round(end - start),
|
|
403
|
-
message: isError ? error.message : "Request failed",
|
|
404
|
-
metadata: { error: isError ? error.name : "UnknownError" },
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private evaluateHeaderAssertion(
|
|
410
|
-
operator: string,
|
|
411
|
-
actual: string | undefined,
|
|
412
|
-
expected = ""
|
|
413
|
-
): boolean {
|
|
414
|
-
if (operator === "exists") return actual !== undefined;
|
|
415
|
-
|
|
416
|
-
if (actual === undefined) return false;
|
|
417
|
-
|
|
418
|
-
switch (operator) {
|
|
419
|
-
case "equals": {
|
|
420
|
-
return actual === expected;
|
|
421
|
-
}
|
|
422
|
-
case "notEquals": {
|
|
423
|
-
return actual !== expected;
|
|
424
|
-
}
|
|
425
|
-
case "contains": {
|
|
426
|
-
return actual.includes(expected);
|
|
427
|
-
}
|
|
428
|
-
case "startsWith": {
|
|
429
|
-
return actual.startsWith(expected);
|
|
430
|
-
}
|
|
431
|
-
case "endsWith": {
|
|
432
|
-
return actual.endsWith(expected);
|
|
433
|
-
}
|
|
434
|
-
default: {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
162
|
+
const body = await response.text();
|
|
163
|
+
const headers: Record<string, string> = {};
|
|
439
164
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
): boolean {
|
|
445
|
-
// Existence checks
|
|
165
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
166
|
+
response.headers.forEach((value, key) => {
|
|
167
|
+
headers[key.toLowerCase()] = value;
|
|
168
|
+
});
|
|
446
169
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
"greaterThan",
|
|
458
|
-
"greaterThanOrEqual",
|
|
459
|
-
].includes(operator)
|
|
460
|
-
) {
|
|
461
|
-
const numActual = Number(actual);
|
|
462
|
-
const numExpected = Number(expected);
|
|
463
|
-
if (Number.isNaN(numActual) || Number.isNaN(numExpected)) return false;
|
|
464
|
-
|
|
465
|
-
switch (operator) {
|
|
466
|
-
case "lessThan": {
|
|
467
|
-
return numActual < numExpected;
|
|
468
|
-
}
|
|
469
|
-
case "lessThanOrEqual": {
|
|
470
|
-
return numActual <= numExpected;
|
|
471
|
-
}
|
|
472
|
-
case "greaterThan": {
|
|
473
|
-
return numActual > numExpected;
|
|
474
|
-
}
|
|
475
|
-
case "greaterThanOrEqual": {
|
|
476
|
-
return numActual >= numExpected;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// String operators
|
|
482
|
-
const strActual = String(actual ?? "");
|
|
483
|
-
const strExpected = expected || "";
|
|
484
|
-
|
|
485
|
-
switch (operator) {
|
|
486
|
-
case "equals": {
|
|
487
|
-
return actual === expected || strActual === strExpected;
|
|
488
|
-
}
|
|
489
|
-
case "notEquals": {
|
|
490
|
-
return actual !== expected && strActual !== strExpected;
|
|
491
|
-
}
|
|
492
|
-
case "contains": {
|
|
493
|
-
return strActual.includes(strExpected);
|
|
494
|
-
}
|
|
495
|
-
case "startsWith": {
|
|
496
|
-
return strActual.startsWith(strExpected);
|
|
497
|
-
}
|
|
498
|
-
case "endsWith": {
|
|
499
|
-
return strActual.endsWith(strExpected);
|
|
500
|
-
}
|
|
501
|
-
case "matches": {
|
|
502
|
-
try {
|
|
503
|
-
return new RegExp(strExpected).test(strActual);
|
|
504
|
-
} catch {
|
|
505
|
-
return false;
|
|
170
|
+
return {
|
|
171
|
+
statusCode: response.status,
|
|
172
|
+
statusText: response.statusText,
|
|
173
|
+
headers,
|
|
174
|
+
body,
|
|
175
|
+
contentType: headers["content-type"],
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
throw error;
|
|
506
180
|
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
client,
|
|
186
|
+
close: () => {
|
|
187
|
+
// HTTP is stateless, nothing to close
|
|
188
|
+
},
|
|
189
|
+
};
|
|
512
190
|
}
|
|
513
191
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TransportClient } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP request configuration.
|
|
5
|
+
*/
|
|
6
|
+
export interface HttpRequest {
|
|
7
|
+
url: string;
|
|
8
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
body?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HTTP response result.
|
|
16
|
+
*/
|
|
17
|
+
export interface HttpResponse {
|
|
18
|
+
statusCode: number;
|
|
19
|
+
statusText: string;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
body: string;
|
|
22
|
+
contentType?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HTTP transport client for collector execution.
|
|
27
|
+
* Each exec() performs a fresh HTTP request.
|
|
28
|
+
*/
|
|
29
|
+
export type HttpTransportClient = TransportClient<HttpRequest, HttpResponse>;
|