@checkstack/healthcheck-http-backend 0.0.3 → 0.1.0
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 +57 -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 +182 -324
- package/src/strategy.ts +106 -401
- package/src/transport-client.ts +29 -0
package/src/strategy.ts
CHANGED
|
@@ -1,134 +1,51 @@
|
|
|
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
51
|
statusCode: healthResultNumber({
|
|
@@ -139,14 +56,13 @@ const httpResultMetadataSchema = z.object({
|
|
|
139
56
|
"x-chart-type": "counter",
|
|
140
57
|
"x-chart-label": "Content Type",
|
|
141
58
|
}).optional(),
|
|
142
|
-
failedAssertion: httpAssertionSchema.optional(),
|
|
143
59
|
error: healthResultString({
|
|
144
60
|
"x-chart-type": "status",
|
|
145
61
|
"x-chart-label": "Error",
|
|
146
62
|
}).optional(),
|
|
147
63
|
});
|
|
148
64
|
|
|
149
|
-
|
|
65
|
+
type HttpResultMetadata = z.infer<typeof httpResultMetadataSchema>;
|
|
150
66
|
|
|
151
67
|
/** Aggregated metadata for buckets */
|
|
152
68
|
const httpAggregatedMetadataSchema = z.object({
|
|
@@ -154,35 +70,62 @@ const httpAggregatedMetadataSchema = z.object({
|
|
|
154
70
|
"x-chart-type": "pie",
|
|
155
71
|
"x-chart-label": "Status Code Distribution",
|
|
156
72
|
}),
|
|
73
|
+
successRate: healthResultNumber({
|
|
74
|
+
"x-chart-type": "gauge",
|
|
75
|
+
"x-chart-label": "Success Rate (%)",
|
|
76
|
+
}),
|
|
157
77
|
errorCount: healthResultNumber({
|
|
158
78
|
"x-chart-type": "counter",
|
|
159
79
|
"x-chart-label": "Errors",
|
|
160
80
|
}),
|
|
161
81
|
});
|
|
162
82
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
83
|
+
type HttpAggregatedMetadata = z.infer<typeof httpAggregatedMetadataSchema>;
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// STRATEGY
|
|
87
|
+
// ============================================================================
|
|
166
88
|
|
|
167
89
|
export class HttpHealthCheckStrategy
|
|
168
90
|
implements
|
|
169
91
|
HealthCheckStrategy<
|
|
170
92
|
HttpHealthCheckConfig,
|
|
93
|
+
HttpTransportClient,
|
|
171
94
|
HttpResultMetadata,
|
|
172
95
|
HttpAggregatedMetadata
|
|
173
96
|
>
|
|
174
97
|
{
|
|
175
98
|
id = "http";
|
|
176
99
|
displayName = "HTTP/HTTPS Health Check";
|
|
177
|
-
description = "HTTP endpoint health monitoring
|
|
100
|
+
description = "HTTP endpoint health monitoring";
|
|
178
101
|
|
|
179
102
|
config: Versioned<HttpHealthCheckConfig> = new Versioned({
|
|
180
|
-
version:
|
|
103
|
+
version: 3, // v3 for createClient pattern with action params moved to RequestCollector
|
|
181
104
|
schema: httpHealthCheckConfigSchema,
|
|
105
|
+
migrations: [
|
|
106
|
+
{
|
|
107
|
+
fromVersion: 1,
|
|
108
|
+
toVersion: 2,
|
|
109
|
+
description: "Add timeout field",
|
|
110
|
+
migrate: (data: HttpConfigV1): HttpConfigV2 => ({
|
|
111
|
+
...data,
|
|
112
|
+
timeout: 30_000,
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
fromVersion: 2,
|
|
117
|
+
toVersion: 3,
|
|
118
|
+
description:
|
|
119
|
+
"Remove url/method/headers/body (moved to RequestCollector)",
|
|
120
|
+
migrate: (data: HttpConfigV2): HttpHealthCheckConfig => ({
|
|
121
|
+
timeout: data.timeout,
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
182
125
|
});
|
|
183
126
|
|
|
184
127
|
result: Versioned<HttpResultMetadata> = new Versioned({
|
|
185
|
-
version:
|
|
128
|
+
version: 3,
|
|
186
129
|
schema: httpResultMetadataSchema,
|
|
187
130
|
});
|
|
188
131
|
|
|
@@ -196,10 +139,13 @@ export class HttpHealthCheckStrategy
|
|
|
196
139
|
): HttpAggregatedMetadata {
|
|
197
140
|
const statusCodeCounts: Record<string, number> = {};
|
|
198
141
|
let errorCount = 0;
|
|
142
|
+
let successCount = 0;
|
|
199
143
|
|
|
200
144
|
for (const run of runs) {
|
|
201
145
|
if (run.metadata?.error) {
|
|
202
146
|
errorCount++;
|
|
147
|
+
} else {
|
|
148
|
+
successCount++;
|
|
203
149
|
}
|
|
204
150
|
|
|
205
151
|
if (run.metadata?.statusCode !== undefined) {
|
|
@@ -207,307 +153,66 @@ export class HttpHealthCheckStrategy
|
|
|
207
153
|
statusCodeCounts[key] = (statusCodeCounts[key] || 0) + 1;
|
|
208
154
|
}
|
|
209
155
|
}
|
|
156
|
+
const successRate =
|
|
157
|
+
runs.length > 0 ? Math.round((successCount / runs.length) * 100) : 0;
|
|
210
158
|
|
|
211
|
-
return { statusCodeCounts, errorCount };
|
|
159
|
+
return { statusCodeCounts, successRate, errorCount };
|
|
212
160
|
}
|
|
213
161
|
|
|
214
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Create an HTTP transport client for one-shot requests.
|
|
164
|
+
* All request parameters come from the collector (RequestCollector).
|
|
165
|
+
*/
|
|
166
|
+
async createClient(
|
|
215
167
|
config: HttpHealthCheckConfig
|
|
216
|
-
): Promise<
|
|
217
|
-
// Validate and apply defaults from schema
|
|
168
|
+
): Promise<ConnectedClient<HttpTransportClient>> {
|
|
218
169
|
const validatedConfig = this.config.validate(config);
|
|
219
170
|
|
|
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
|
|
171
|
+
const client: HttpTransportClient = {
|
|
172
|
+
async exec(request: HttpRequest): Promise<HttpResponse> {
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
const timeoutId = setTimeout(
|
|
175
|
+
() => controller.abort(),
|
|
176
|
+
request.timeout ?? validatedConfig.timeout
|
|
306
177
|
);
|
|
307
178
|
|
|
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
179
|
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
|
-
}
|
|
357
|
-
|
|
358
|
-
const extractPath = (path: string, json: unknown) =>
|
|
359
|
-
JSONPath({ path, json: json as object, wrap: false });
|
|
360
|
-
|
|
361
|
-
for (const jsonPathAssertion of jsonPathAssertions) {
|
|
362
|
-
const actualValue = extractPath(jsonPathAssertion.path, responseData);
|
|
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
|
-
}
|
|
439
|
-
|
|
440
|
-
private evaluateJsonPathAssertion(
|
|
441
|
-
operator: string,
|
|
442
|
-
actual: unknown,
|
|
443
|
-
expected: string | undefined
|
|
444
|
-
): boolean {
|
|
445
|
-
// Existence checks
|
|
180
|
+
const response = await fetch(request.url, {
|
|
181
|
+
method: request.method,
|
|
182
|
+
headers: request.headers,
|
|
183
|
+
body: request.body,
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
});
|
|
446
186
|
|
|
447
|
-
|
|
187
|
+
clearTimeout(timeoutId);
|
|
448
188
|
|
|
449
|
-
|
|
450
|
-
|
|
189
|
+
const body = await response.text();
|
|
190
|
+
const headers: Record<string, string> = {};
|
|
451
191
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"lessThanOrEqual",
|
|
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;
|
|
192
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
193
|
+
response.headers.forEach((value, key) => {
|
|
194
|
+
headers[key.toLowerCase()] = value;
|
|
195
|
+
});
|
|
464
196
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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;
|
|
197
|
+
return {
|
|
198
|
+
statusCode: response.status,
|
|
199
|
+
statusText: response.statusText,
|
|
200
|
+
headers,
|
|
201
|
+
body,
|
|
202
|
+
contentType: headers["content-type"],
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
clearTimeout(timeoutId);
|
|
206
|
+
throw error;
|
|
506
207
|
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
client,
|
|
213
|
+
close: () => {
|
|
214
|
+
// HTTP is stateless, nothing to close
|
|
215
|
+
},
|
|
216
|
+
};
|
|
512
217
|
}
|
|
513
218
|
}
|
|
@@ -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>;
|