@schmock/core 2.2.3 → 2.3.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +804 -203
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +52 -5
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +153 -37
- package/dist/index.d.ts +381 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +354 -156
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +117 -39
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +85 -18
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -701
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.ts +0 -113
- package/src/index.ts +0 -151
- package/src/interceptor.test.ts +0 -291
- package/src/interceptor.ts +0 -320
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -594
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -103
- package/src/plugin-system.test.ts +0 -602
- package/src/response-parser.ts +0 -75
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -165
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -400
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -99
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -345
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -331
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/lifecycle-events.steps.ts +0 -142
- package/src/steps/performance-reliability.steps.ts +0 -423
- package/src/steps/plugin-integration.steps.ts +0 -280
- package/src/steps/request-history.steps.ts +0 -276
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -233
- package/src/steps/state-concurrency.steps.ts +0 -739
- package/src/steps/stateful-workflows.steps.ts +0 -353
- package/src/types.ts +0 -36
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
interface PipelineLogger {
|
|
3
3
|
log(category: string, message: string, data?: unknown): void;
|
|
4
4
|
}
|
|
5
|
+
interface PipelineResult {
|
|
6
|
+
context: Schmock.PluginContext;
|
|
7
|
+
response?: unknown;
|
|
8
|
+
recoveredFromError?: boolean;
|
|
9
|
+
requestShortCircuited?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Run request guards before route code is allowed to execute. */
|
|
12
|
+
export declare function runPluginBeforeRequest(plugins: readonly Schmock.Plugin[], context: Schmock.PluginContext, logger: PipelineLogger, signal?: AbortSignal | undefined): Promise<PipelineResult>;
|
|
13
|
+
/** Give pipeline error handlers a chance to recover a route-generator error. */
|
|
14
|
+
export declare function recoverGeneratorError(plugins: readonly Schmock.Plugin[], context: Schmock.PluginContext, error: unknown, logger: PipelineLogger, signal?: AbortSignal | undefined): Promise<PipelineResult>;
|
|
5
15
|
/**
|
|
6
16
|
* Run all registered plugins in sequence
|
|
7
17
|
* First plugin to set response becomes generator, subsequent plugins transform
|
|
8
18
|
* Handles plugin errors via onError hooks
|
|
9
19
|
*/
|
|
10
|
-
export declare function runPluginPipeline(plugins: Schmock.Plugin[], context: Schmock.PluginContext, initialResponse: unknown, logger: PipelineLogger): Promise<
|
|
11
|
-
context: Schmock.PluginContext;
|
|
12
|
-
response?: unknown;
|
|
13
|
-
}>;
|
|
20
|
+
export declare function runPluginPipeline(plugins: readonly Schmock.Plugin[], context: Schmock.PluginContext, initialResponse: unknown, logger: PipelineLogger, signal?: AbortSignal | undefined): Promise<PipelineResult>;
|
|
14
21
|
export {};
|
|
15
22
|
//# sourceMappingURL=plugin-pipeline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-pipeline.d.ts","sourceRoot":"","sources":["../src/plugin-pipeline.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,UAAU,cAAc;IACtB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED
|
|
1
|
+
{"version":3,"file":"plugin-pipeline.d.ts","sourceRoot":"","sources":["../src/plugin-pipeline.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,UAAU,cAAc;IACtB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9D;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAmED,kEAAkE;AAClE,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAwDzB;AAED,gFAAgF;AAChF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAmBzB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,EAClC,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,cAAc,EACtB,MAAM,0BAAiB,GACtB,OAAO,CAAC,cAAc,CAAC,CAiEzB"}
|
package/dist/plugin-pipeline.js
CHANGED
|
@@ -1,22 +1,123 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { awaitWithAbort, throwIfAborted } from "./abort.js";
|
|
2
2
|
import { errorMessage, PluginError } from "./errors.js";
|
|
3
|
+
function isPluginResult(value) {
|
|
4
|
+
return (typeof value === "object" &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
"context" in value &&
|
|
7
|
+
typeof value.context === "object" &&
|
|
8
|
+
value.context !== null);
|
|
9
|
+
}
|
|
10
|
+
function preserveRequestSignal(context, signal) {
|
|
11
|
+
return context.signal === signal ? context : { ...context, signal };
|
|
12
|
+
}
|
|
13
|
+
async function recoverPluginError(plugins, startIndex, error, context, logger, signal) {
|
|
14
|
+
let currentError = error instanceof Error ? error : new Error(errorMessage(error));
|
|
15
|
+
for (let index = startIndex; index < plugins.length; index += 1) {
|
|
16
|
+
throwIfAborted(signal);
|
|
17
|
+
const plugin = plugins[index];
|
|
18
|
+
if (!plugin.onError)
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
const errorResult = await awaitWithAbort(plugin.onError(currentError, context), signal);
|
|
22
|
+
throwIfAborted(signal);
|
|
23
|
+
if (errorResult instanceof Error) {
|
|
24
|
+
currentError = errorResult;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (errorResult !== undefined) {
|
|
28
|
+
logger.log("pipeline", `Plugin ${plugin.name} handled error`);
|
|
29
|
+
return { handled: true, response: errorResult };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (hookError) {
|
|
33
|
+
throwIfAborted(signal);
|
|
34
|
+
currentError =
|
|
35
|
+
hookError instanceof Error
|
|
36
|
+
? hookError
|
|
37
|
+
: new Error(errorMessage(hookError));
|
|
38
|
+
logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { handled: false, error: currentError };
|
|
42
|
+
}
|
|
43
|
+
/** Run request guards before route code is allowed to execute. */
|
|
44
|
+
export async function runPluginBeforeRequest(plugins, context, logger, signal = context.signal) {
|
|
45
|
+
let currentContext = preserveRequestSignal(context, signal);
|
|
46
|
+
for (let index = 0; index < plugins.length; index += 1) {
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
const plugin = plugins[index];
|
|
49
|
+
if (!plugin.beforeRequest)
|
|
50
|
+
continue;
|
|
51
|
+
logger.log("pipeline", `Running beforeRequest: ${plugin.name}`);
|
|
52
|
+
try {
|
|
53
|
+
const result = await awaitWithAbort(plugin.beforeRequest(currentContext), signal);
|
|
54
|
+
throwIfAborted(signal);
|
|
55
|
+
if (result === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
if (!isPluginResult(result)) {
|
|
58
|
+
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
59
|
+
}
|
|
60
|
+
currentContext = preserveRequestSignal(result.context, signal);
|
|
61
|
+
if (result.response !== undefined) {
|
|
62
|
+
logger.log("pipeline", `Plugin ${plugin.name} rejected request`);
|
|
63
|
+
return {
|
|
64
|
+
context: currentContext,
|
|
65
|
+
response: result.response,
|
|
66
|
+
requestShortCircuited: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throwIfAborted(signal);
|
|
72
|
+
logger.log("pipeline", `Plugin ${plugin.name} beforeRequest failed: ${errorMessage(error)}`);
|
|
73
|
+
const recovery = await recoverPluginError(plugins, index, error, currentContext, logger, signal);
|
|
74
|
+
if (recovery.handled) {
|
|
75
|
+
return {
|
|
76
|
+
context: currentContext,
|
|
77
|
+
response: recovery.response,
|
|
78
|
+
recoveredFromError: true,
|
|
79
|
+
requestShortCircuited: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw new PluginError(plugin.name, recovery.error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { context: currentContext };
|
|
86
|
+
}
|
|
87
|
+
/** Give pipeline error handlers a chance to recover a route-generator error. */
|
|
88
|
+
export async function recoverGeneratorError(plugins, context, error, logger, signal = context.signal) {
|
|
89
|
+
throwIfAborted(signal);
|
|
90
|
+
const recovery = await recoverPluginError(plugins, 0, error, context, logger, signal);
|
|
91
|
+
throwIfAborted(signal);
|
|
92
|
+
if (recovery.handled) {
|
|
93
|
+
return {
|
|
94
|
+
context,
|
|
95
|
+
response: recovery.response,
|
|
96
|
+
recoveredFromError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
throw recovery.error;
|
|
100
|
+
}
|
|
3
101
|
/**
|
|
4
102
|
* Run all registered plugins in sequence
|
|
5
103
|
* First plugin to set response becomes generator, subsequent plugins transform
|
|
6
104
|
* Handles plugin errors via onError hooks
|
|
7
105
|
*/
|
|
8
|
-
export async function runPluginPipeline(plugins, context, initialResponse, logger) {
|
|
9
|
-
let currentContext = context;
|
|
106
|
+
export async function runPluginPipeline(plugins, context, initialResponse, logger, signal = context.signal) {
|
|
107
|
+
let currentContext = preserveRequestSignal(context, signal);
|
|
10
108
|
let response = initialResponse;
|
|
11
109
|
logger.log("pipeline", `Running plugin pipeline for ${plugins.length} plugins`);
|
|
12
|
-
for (
|
|
110
|
+
for (let index = 0; index < plugins.length; index += 1) {
|
|
111
|
+
throwIfAborted(signal);
|
|
112
|
+
const plugin = plugins[index];
|
|
13
113
|
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
14
114
|
try {
|
|
15
|
-
const result = await plugin.process(currentContext, response);
|
|
16
|
-
|
|
115
|
+
const result = await awaitWithAbort(plugin.process(currentContext, response), signal);
|
|
116
|
+
throwIfAborted(signal);
|
|
117
|
+
if (!isPluginResult(result)) {
|
|
17
118
|
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
18
119
|
}
|
|
19
|
-
currentContext = result.context;
|
|
120
|
+
currentContext = preserveRequestSignal(result.context, signal);
|
|
20
121
|
// First plugin to set response becomes the generator
|
|
21
122
|
if (result.response !== undefined &&
|
|
22
123
|
(response === undefined || response === null)) {
|
|
@@ -29,40 +130,17 @@ export async function runPluginPipeline(plugins, context, initialResponse, logge
|
|
|
29
130
|
}
|
|
30
131
|
}
|
|
31
132
|
catch (error) {
|
|
133
|
+
throwIfAborted(signal);
|
|
32
134
|
logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Error return → transform the thrown error
|
|
41
|
-
if (errorResult instanceof Error) {
|
|
42
|
-
throw new PluginError(plugin.name, errorResult);
|
|
43
|
-
}
|
|
44
|
-
// ResponseResult return → recover, stop pipeline
|
|
45
|
-
// Accept both object-with-status and status tuple [code, body] / [code, body, headers]
|
|
46
|
-
if (isStatusTuple(errorResult) ||
|
|
47
|
-
(typeof errorResult === "object" &&
|
|
48
|
-
errorResult !== null &&
|
|
49
|
-
"status" in errorResult)) {
|
|
50
|
-
response = errorResult;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// void/falsy return → propagate original error below
|
|
55
|
-
}
|
|
56
|
-
catch (hookError) {
|
|
57
|
-
// If the hook itself threw (including our PluginError above), re-throw it
|
|
58
|
-
if (hookError instanceof PluginError) {
|
|
59
|
-
throw hookError;
|
|
60
|
-
}
|
|
61
|
-
logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
|
|
62
|
-
}
|
|
135
|
+
const recovery = await recoverPluginError(plugins, index, error, currentContext, logger, signal);
|
|
136
|
+
if (recovery.handled) {
|
|
137
|
+
return {
|
|
138
|
+
context: currentContext,
|
|
139
|
+
response: recovery.response,
|
|
140
|
+
recoveredFromError: true,
|
|
141
|
+
};
|
|
63
142
|
}
|
|
64
|
-
|
|
65
|
-
throw new PluginError(plugin.name, cause);
|
|
143
|
+
throw new PluginError(plugin.name, recovery.error);
|
|
66
144
|
}
|
|
67
145
|
}
|
|
68
146
|
return { context: currentContext, response };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type OwnedBytes = ReturnType<typeof Uint8Array.of>;
|
|
2
|
+
interface NormalizableResponse {
|
|
3
|
+
status: number;
|
|
4
|
+
body: unknown;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validate and stabilize a response before it reaches a transport adapter.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeResponse(response: NormalizableResponse, method: string): Schmock.Response;
|
|
11
|
+
/**
|
|
12
|
+
* Encode a response body using its normalized content type semantics.
|
|
13
|
+
*/
|
|
14
|
+
export declare function serializeResponseBody(response: Schmock.Response): OwnedBytes | undefined;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=response-normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-normalizer.d.ts","sourceRoot":"","sources":["../src/response-normalizer.ts"],"names":[],"mappings":"AAwBA,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,EAAE,CAAC,CAAC;AAEnD,UAAU,oBAAoB;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAmSD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,QAAQ,CAmClB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ,GACzB,UAAU,GAAG,SAAS,CAcxB"}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { isBinaryBody } from "./binary.js";
|
|
2
|
+
import { errorMessage, InvalidResponseError } from "./errors.js";
|
|
3
|
+
const BODY_FORBIDDEN_STATUSES = new Set([204, 205, 304]);
|
|
4
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
5
|
+
const FRAMING_HEADERS = new Set([
|
|
6
|
+
"content-length",
|
|
7
|
+
"trailer",
|
|
8
|
+
"transfer-encoding",
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Headers that describe the connection rather than the message (RFC 9110 §7.6.1).
|
|
12
|
+
* Kept separate from {@link FRAMING_HEADERS}, which is parameterised by
|
|
13
|
+
* `preserveContentLength`: these are never a route's to send, so the strip is
|
|
14
|
+
* unconditional.
|
|
15
|
+
*/
|
|
16
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
17
|
+
"connection",
|
|
18
|
+
"keep-alive",
|
|
19
|
+
"proxy-authenticate",
|
|
20
|
+
"proxy-authorization",
|
|
21
|
+
"te",
|
|
22
|
+
"upgrade",
|
|
23
|
+
]);
|
|
24
|
+
function hasInvalidHeaderValue(value) {
|
|
25
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
26
|
+
const code = value.charCodeAt(index);
|
|
27
|
+
if (code <= 8 || (code >= 10 && code <= 31) || code === 127)
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function validateStatus(status) {
|
|
33
|
+
if (typeof status !== "number" ||
|
|
34
|
+
!Number.isFinite(status) ||
|
|
35
|
+
!Number.isInteger(status) ||
|
|
36
|
+
status < 200 ||
|
|
37
|
+
status > 599) {
|
|
38
|
+
throw new InvalidResponseError("status must be a finite integer from 200 through 599", { status });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function headerEntries(headers) {
|
|
42
|
+
if (typeof headers !== "object" ||
|
|
43
|
+
headers === null ||
|
|
44
|
+
Array.isArray(headers)) {
|
|
45
|
+
throw new InvalidResponseError("headers must be a string record");
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const entries = [];
|
|
49
|
+
for (const name of Reflect.ownKeys(headers)) {
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(headers, name);
|
|
51
|
+
if (!descriptor?.enumerable)
|
|
52
|
+
continue;
|
|
53
|
+
if (typeof name !== "string") {
|
|
54
|
+
throw new InvalidResponseError("header names must be strings");
|
|
55
|
+
}
|
|
56
|
+
const value = headers[name];
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
throw new InvalidResponseError("header values must be strings", {
|
|
59
|
+
headerName: name,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
entries.push([name, value]);
|
|
63
|
+
}
|
|
64
|
+
return entries;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof InvalidResponseError)
|
|
68
|
+
throw error;
|
|
69
|
+
throw new InvalidResponseError("headers could not be read", {
|
|
70
|
+
cause: errorMessage(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function normalizeHeadersWithPlatform(entries, PlatformHeaders) {
|
|
75
|
+
try {
|
|
76
|
+
const normalized = {};
|
|
77
|
+
for (const [name, value] of entries) {
|
|
78
|
+
try {
|
|
79
|
+
const headers = new PlatformHeaders([[name, value]]);
|
|
80
|
+
normalized[name] = headers.get(name) ?? value;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new InvalidResponseError("header is invalid", {
|
|
84
|
+
cause: errorMessage(error),
|
|
85
|
+
headerName: name,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof InvalidResponseError)
|
|
93
|
+
throw error;
|
|
94
|
+
throw new InvalidResponseError("headers could not be normalized", {
|
|
95
|
+
cause: errorMessage(error),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function normalizeHeadersWithoutPlatform(entries) {
|
|
100
|
+
const normalized = {};
|
|
101
|
+
for (const [name, value] of entries) {
|
|
102
|
+
if (!HEADER_NAME_PATTERN.test(name) || hasInvalidHeaderValue(value)) {
|
|
103
|
+
throw new InvalidResponseError("header is invalid", {
|
|
104
|
+
headerName: name,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
normalized[name] = value.replace(/^[\t ]+|[\t ]+$/g, "");
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
function normalizeHeaders(headers) {
|
|
112
|
+
const entries = headerEntries(headers);
|
|
113
|
+
const seenNames = new Set();
|
|
114
|
+
for (const [name, value] of entries) {
|
|
115
|
+
const normalizedName = name.toLowerCase();
|
|
116
|
+
if (seenNames.has(normalizedName)) {
|
|
117
|
+
throw new InvalidResponseError("header names must be unique", {
|
|
118
|
+
headerName: name,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (!HEADER_NAME_PATTERN.test(name) || hasInvalidHeaderValue(value)) {
|
|
122
|
+
throw new InvalidResponseError("header is invalid", {
|
|
123
|
+
headerName: name,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
seenNames.add(normalizedName);
|
|
127
|
+
}
|
|
128
|
+
const PlatformHeaders = globalThis.Headers;
|
|
129
|
+
return typeof PlatformHeaders === "function"
|
|
130
|
+
? normalizeHeadersWithPlatform(entries, PlatformHeaders)
|
|
131
|
+
: normalizeHeadersWithoutPlatform(entries);
|
|
132
|
+
}
|
|
133
|
+
function copyBinaryBody(body) {
|
|
134
|
+
try {
|
|
135
|
+
if (body instanceof Uint8Array) {
|
|
136
|
+
// Uint8Array.prototype.slice species-creates, so subclasses such as
|
|
137
|
+
// Node's Buffer keep their type while the bytes are still copied.
|
|
138
|
+
// body.slice() would not: Buffer overrides slice() to return a view.
|
|
139
|
+
return Uint8Array.prototype.slice.call(body);
|
|
140
|
+
}
|
|
141
|
+
const source = body instanceof ArrayBuffer
|
|
142
|
+
? new Uint8Array(body)
|
|
143
|
+
: new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
144
|
+
return Uint8Array.from(source);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new InvalidResponseError("binary body could not be copied", {
|
|
148
|
+
cause: errorMessage(error),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function bodyType(value) {
|
|
153
|
+
if (value === null)
|
|
154
|
+
return "null";
|
|
155
|
+
if (typeof value !== "object")
|
|
156
|
+
return typeof value;
|
|
157
|
+
try {
|
|
158
|
+
return Object.prototype.toString.call(value);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return "object";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function hasFunctionProperty(value, property) {
|
|
165
|
+
return typeof Reflect.get(value, property) === "function";
|
|
166
|
+
}
|
|
167
|
+
function assertJsonCompatible(value) {
|
|
168
|
+
if (value === null)
|
|
169
|
+
return;
|
|
170
|
+
switch (typeof value) {
|
|
171
|
+
case "boolean":
|
|
172
|
+
case "string":
|
|
173
|
+
return;
|
|
174
|
+
case "number":
|
|
175
|
+
if (Number.isFinite(value))
|
|
176
|
+
return;
|
|
177
|
+
throw new InvalidResponseError("body contains a non-finite number", {
|
|
178
|
+
bodyType: "number",
|
|
179
|
+
});
|
|
180
|
+
case "undefined":
|
|
181
|
+
case "function":
|
|
182
|
+
case "symbol":
|
|
183
|
+
case "bigint":
|
|
184
|
+
throw new InvalidResponseError(`body contains an unsupported ${typeof value} value`, { bodyType: typeof value });
|
|
185
|
+
case "object":
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
if (isBinaryBody(value)) {
|
|
190
|
+
throw new InvalidResponseError("binary values are supported only as the top-level body", { bodyType: bodyType(value) });
|
|
191
|
+
}
|
|
192
|
+
if (hasFunctionProperty(value, "then") ||
|
|
193
|
+
hasFunctionProperty(value, "getReader") ||
|
|
194
|
+
hasFunctionProperty(value, "getWriter") ||
|
|
195
|
+
(hasFunctionProperty(value, "pipe") &&
|
|
196
|
+
hasFunctionProperty(value, "on")) ||
|
|
197
|
+
hasFunctionProperty(value, Symbol.asyncIterator)) {
|
|
198
|
+
throw new InvalidResponseError("promise, iterable, and stream bodies are unsupported", { bodyType: bodyType(value) });
|
|
199
|
+
}
|
|
200
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
201
|
+
if (typeof key === "symbol" &&
|
|
202
|
+
Object.getOwnPropertyDescriptor(value, key)?.enumerable) {
|
|
203
|
+
throw new InvalidResponseError("body contains an enumerable symbol property", { bodyType: bodyType(value) });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(value) &&
|
|
207
|
+
Object.prototype.toString.call(value) !== "[object Object]") {
|
|
208
|
+
throw new InvalidResponseError("body contains an unsupported object", {
|
|
209
|
+
bodyType: bodyType(value),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof InvalidResponseError)
|
|
215
|
+
throw error;
|
|
216
|
+
throw new InvalidResponseError("body could not be inspected", {
|
|
217
|
+
bodyType: bodyType(value),
|
|
218
|
+
cause: errorMessage(error),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function stringifyJsonBody(body) {
|
|
223
|
+
try {
|
|
224
|
+
const serialized = JSON.stringify(body, (_key, value) => {
|
|
225
|
+
assertJsonCompatible(value);
|
|
226
|
+
return value;
|
|
227
|
+
});
|
|
228
|
+
if (serialized !== undefined)
|
|
229
|
+
return serialized;
|
|
230
|
+
throw new InvalidResponseError("body did not produce JSON", {
|
|
231
|
+
bodyType: bodyType(body),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof InvalidResponseError)
|
|
236
|
+
throw error;
|
|
237
|
+
throw new InvalidResponseError("body is not JSON-serializable", {
|
|
238
|
+
bodyType: bodyType(body),
|
|
239
|
+
cause: errorMessage(error),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function normalizeBody(body) {
|
|
244
|
+
if (body === undefined || typeof body === "string")
|
|
245
|
+
return body;
|
|
246
|
+
if (isBinaryBody(body))
|
|
247
|
+
return copyBinaryBody(body);
|
|
248
|
+
return JSON.parse(stringifyJsonBody(body));
|
|
249
|
+
}
|
|
250
|
+
function removeFramingHeaders(headers, preserveContentLength = false) {
|
|
251
|
+
for (const name of Object.keys(headers)) {
|
|
252
|
+
const normalizedName = name.toLowerCase();
|
|
253
|
+
if (FRAMING_HEADERS.has(normalizedName) &&
|
|
254
|
+
!(preserveContentLength && normalizedName === "content-length")) {
|
|
255
|
+
delete headers[name];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function removeHopByHopHeaders(headers) {
|
|
260
|
+
for (const name of Object.keys(headers)) {
|
|
261
|
+
if (HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
|
|
262
|
+
delete headers[name];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Validate and stabilize a response before it reaches a transport adapter.
|
|
267
|
+
*/
|
|
268
|
+
export function normalizeResponse(response, method) {
|
|
269
|
+
const status = response.status;
|
|
270
|
+
validateStatus(status);
|
|
271
|
+
const headers = normalizeHeaders(response.headers ?? {});
|
|
272
|
+
const normalizedMethod = method.toUpperCase();
|
|
273
|
+
const body = normalizedMethod === "HEAD" || BODY_FORBIDDEN_STATUSES.has(status)
|
|
274
|
+
? undefined
|
|
275
|
+
: normalizeBody(response.body);
|
|
276
|
+
// HEAD and 304 may keep an entity Content-Length (RFC 9110), but 204/205
|
|
277
|
+
// never carry one. Trailer and Transfer-Encoding are always removed —
|
|
278
|
+
// Node's writeHead rejects them on bodyless responses, killing the socket
|
|
279
|
+
// before any bytes reach the client.
|
|
280
|
+
if (status === 204 || status === 205) {
|
|
281
|
+
removeFramingHeaders(headers);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
removeFramingHeaders(headers, normalizedMethod === "HEAD" || status === 304);
|
|
285
|
+
}
|
|
286
|
+
// A hop-by-hop header belongs to the connection, which the transport owns:
|
|
287
|
+
// a route that emits `Connection: close` announces a close the server will
|
|
288
|
+
// not perform, and a conformant client then discards the next response on
|
|
289
|
+
// that socket. Adapters that genuinely mean it (an ingress rejection) add it
|
|
290
|
+
// after normalization, through their own extra-headers channel.
|
|
291
|
+
removeHopByHopHeaders(headers);
|
|
292
|
+
return {
|
|
293
|
+
status,
|
|
294
|
+
body,
|
|
295
|
+
headers,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Encode a response body using its normalized content type semantics.
|
|
300
|
+
*/
|
|
301
|
+
export function serializeResponseBody(response) {
|
|
302
|
+
const status = response.status;
|
|
303
|
+
validateStatus(status);
|
|
304
|
+
if (BODY_FORBIDDEN_STATUSES.has(status))
|
|
305
|
+
return undefined;
|
|
306
|
+
const body = response.body;
|
|
307
|
+
if (body === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (isBinaryBody(body))
|
|
310
|
+
return copyBinaryBody(body);
|
|
311
|
+
// A string body is treated as pre-serialized wire bytes regardless of
|
|
312
|
+
// content type: quoting it under application/json would double-encode
|
|
313
|
+
// routes that return JSON.stringify(...) themselves.
|
|
314
|
+
const serialized = typeof body === "string" ? body : stringifyJsonBody(body);
|
|
315
|
+
return new TextEncoder().encode(serialized);
|
|
316
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-parser.d.ts","sourceRoot":"","sources":["../src/response-parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"response-parser.d.ts","sourceRoot":"","sources":["../src/response-parser.ts"],"names":[],"mappings":"AAwEA;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,OAAO,CAAC,WAAW,GAC/B,OAAO,CAAC,QAAQ,CAuElB"}
|