@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/binary.d.ts +8 -0
  7. package/dist/binary.d.ts.map +1 -0
  8. package/dist/binary.js +9 -0
  9. package/dist/builder.d.ts +31 -10
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/builder.js +804 -203
  12. package/dist/constants.d.ts +38 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +123 -2
  15. package/dist/errors.d.ts +14 -3
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +41 -6
  18. package/dist/helpers.d.ts +9 -0
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js +18 -2
  21. package/dist/http-helpers.d.ts +52 -5
  22. package/dist/http-helpers.d.ts.map +1 -1
  23. package/dist/http-helpers.js +153 -37
  24. package/dist/index.d.ts +381 -60
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -3
  27. package/dist/interceptor.d.ts +11 -1
  28. package/dist/interceptor.d.ts.map +1 -1
  29. package/dist/interceptor.js +354 -156
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +12 -3
  32. package/dist/plugin-pipeline.d.ts +11 -4
  33. package/dist/plugin-pipeline.d.ts.map +1 -1
  34. package/dist/plugin-pipeline.js +117 -39
  35. package/dist/response-normalizer.d.ts +16 -0
  36. package/dist/response-normalizer.d.ts.map +1 -0
  37. package/dist/response-normalizer.js +316 -0
  38. package/dist/response-parser.d.ts.map +1 -1
  39. package/dist/response-parser.js +85 -18
  40. package/dist/route-matcher.d.ts +3 -0
  41. package/dist/route-matcher.d.ts.map +1 -1
  42. package/dist/route-matcher.js +12 -6
  43. package/dist/types.d.ts +7 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +18 -8
  46. package/src/audit-core-builder.test.ts +0 -218
  47. package/src/audit-response-guard.test.ts +0 -38
  48. package/src/builder.test.ts +0 -289
  49. package/src/builder.ts +0 -701
  50. package/src/constants.test.ts +0 -99
  51. package/src/constants.ts +0 -73
  52. package/src/debug.test.ts +0 -241
  53. package/src/delay.test.ts +0 -319
  54. package/src/dist-shape.test.ts +0 -35
  55. package/src/errors.test.ts +0 -223
  56. package/src/errors.ts +0 -130
  57. package/src/factory.test.ts +0 -133
  58. package/src/helpers.test.ts +0 -147
  59. package/src/helpers.ts +0 -58
  60. package/src/http-helpers.ts +0 -113
  61. package/src/index.ts +0 -151
  62. package/src/interceptor.test.ts +0 -291
  63. package/src/interceptor.ts +0 -320
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -594
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -103
  69. package/src/plugin-system.test.ts +0 -602
  70. package/src/response-parser.ts +0 -75
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -165
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -400
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -99
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -345
  82. package/src/steps/fluent-api.steps.ts +0 -255
  83. package/src/steps/http-methods.steps.ts +0 -331
  84. package/src/steps/interceptor.steps.ts +0 -260
  85. package/src/steps/lifecycle-events.steps.ts +0 -142
  86. package/src/steps/performance-reliability.steps.ts +0 -423
  87. package/src/steps/plugin-integration.steps.ts +0 -280
  88. package/src/steps/request-history.steps.ts +0 -276
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -233
  92. package/src/steps/state-concurrency.steps.ts +0 -739
  93. package/src/steps/stateful-workflows.steps.ts +0 -353
  94. 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;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EACzB,OAAO,EAAE,OAAO,CAAC,aAAa,EAC9B,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAoFjE"}
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"}
@@ -1,22 +1,123 @@
1
- import { isStatusTuple } from "./constants.js";
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 (const plugin of plugins) {
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
- if (!result?.context) {
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
- // Try error handling if plugin has onError hook
34
- if (plugin.onError) {
35
- try {
36
- const pluginError = error instanceof Error ? error : new Error(errorMessage(error));
37
- const errorResult = await plugin.onError(pluginError, currentContext);
38
- if (errorResult) {
39
- logger.log("pipeline", `Plugin ${plugin.name} handled error`);
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
- const cause = error instanceof Error ? error : new Error(errorMessage(error));
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":"AAgBA;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,OAAO,CAAC,WAAW,GAC/B,OAAO,CAAC,QAAQ,CAmDlB"}
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"}