@rexeus/typeweaver-server 0.10.2 → 0.10.4
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/dist/index.cjs +3 -2
- package/dist/index.mjs +4 -3
- package/dist/index.mjs.map +1 -1
- package/dist/lib/BodyLimitPolicy.ts +118 -0
- package/dist/lib/Errors.ts +16 -0
- package/dist/lib/FetchApiAdapter.ts +54 -22
- package/dist/lib/NodeAdapter.ts +570 -36
- package/dist/lib/PathMatcher.ts +54 -10
- package/dist/lib/Router.ts +16 -4
- package/dist/lib/TypeweaverApp.ts +32 -9
- package/dist/lib/TypeweaverAppRuntime.ts +37 -0
- package/dist/lib/TypeweaverInternals.ts +45 -0
- package/dist/lib/index.ts +1 -0
- package/dist/lib/middleware/basicAuth.ts +11 -2
- package/dist/lib/middleware/bearerAuth.ts +11 -2
- package/dist/lib/middleware/cors.ts +120 -12
- package/dist/lib/middleware/header.ts +59 -0
- package/dist/lib/middleware/logger.ts +4 -2
- package/dist/lib/middleware/poweredBy.ts +6 -1
- package/dist/lib/middleware/requestId.ts +8 -8
- package/dist/lib/middleware/scoped.ts +27 -12
- package/dist/lib/middleware/secureHeaders.ts +3 -1
- package/package.json +8 -5
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ node_path = __toESM(node_path);
|
|
|
26
26
|
let node_url = require("node:url");
|
|
27
27
|
let _rexeus_typeweaver_gen = require("@rexeus/typeweaver-gen");
|
|
28
28
|
let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
|
|
29
|
+
let polycase = require("polycase");
|
|
29
30
|
//#region src/routerGenerator.ts
|
|
30
31
|
/**
|
|
31
32
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
@@ -44,7 +45,7 @@ function generate(context) {
|
|
|
44
45
|
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
45
46
|
}
|
|
46
47
|
function writeRouter(resource, templateFile, context) {
|
|
47
|
-
const pascalCaseEntityName = (0,
|
|
48
|
+
const pascalCaseEntityName = (0, polycase.pascalCase)(resource.name);
|
|
48
49
|
const outputDir = context.getResourceOutputDir(resource.name);
|
|
49
50
|
const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
50
51
|
const operations = resource.operations.filter((operation) => operation.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => (0, _rexeus_typeweaver_gen.compareRoutes)(a, b));
|
|
@@ -59,7 +60,7 @@ function writeRouter(resource, templateFile, context) {
|
|
|
59
60
|
}
|
|
60
61
|
function createOperationData(operation) {
|
|
61
62
|
const operationId = operation.operationId;
|
|
62
|
-
const className = (0,
|
|
63
|
+
const className = (0, polycase.pascalCase)(operationId);
|
|
63
64
|
return {
|
|
64
65
|
operationId,
|
|
65
66
|
className,
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { BasePlugin, compareRoutes, relative
|
|
3
|
+
import { BasePlugin, compareRoutes, relative } from "@rexeus/typeweaver-gen";
|
|
4
4
|
import { HttpMethod } from "@rexeus/typeweaver-core";
|
|
5
|
+
import { pascalCase } from "polycase";
|
|
5
6
|
//#region src/routerGenerator.ts
|
|
6
7
|
/**
|
|
7
8
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
@@ -20,7 +21,7 @@ function generate(context) {
|
|
|
20
21
|
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
21
22
|
}
|
|
22
23
|
function writeRouter(resource, templateFile, context) {
|
|
23
|
-
const pascalCaseEntityName =
|
|
24
|
+
const pascalCaseEntityName = pascalCase(resource.name);
|
|
24
25
|
const outputDir = context.getResourceOutputDir(resource.name);
|
|
25
26
|
const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
26
27
|
const operations = resource.operations.filter((operation) => operation.method !== HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => compareRoutes(a, b));
|
|
@@ -35,7 +36,7 @@ function writeRouter(resource, templateFile, context) {
|
|
|
35
36
|
}
|
|
36
37
|
function createOperationData(operation) {
|
|
37
38
|
const operationId = operation.operationId;
|
|
38
|
-
const className =
|
|
39
|
+
const className = pascalCase(operationId);
|
|
39
40
|
return {
|
|
40
41
|
operationId,
|
|
41
42
|
className,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport { pascalCase } from \"polycase\";\n\nexport type RouterGenerationContext = Pick<\n GeneratorContext,\n | \"normalizedSpec\"\n | \"outputDir\"\n | \"getResourceOutputDir\"\n | \"renderTemplate\"\n | \"writeFile\"\n>;\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\n\n/**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\nexport function generate(context: RouterGenerationContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeRouter(resource, templateFile, context);\n }\n}\n\nfunction writeRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: RouterGenerationContext\n): void {\n const pascalCaseEntityName = pascalCase(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = resource.operations\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation): OperationData {\n const operationId = operation.operationId;\n const className = pascalCase(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateRouters } from \"./routerGenerator.js\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport class ServerPlugin extends BasePlugin {\n public name = \"server\";\n public override depends = [\"types\"];\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateRouters(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAwCA,SAAgB,SAAS,SAAwC;CAC/D,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,aAAY,UAAU,cAAc,QAAQ;;AAIhD,SAAS,YACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,WAAW,SAAS,KAAK;CACtD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;CAE3E,MAAM,aAAa,SAAS,WACzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,SAAS,WAAW,QAAQ,UAAU;EAC/C,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAA+C;CAC1E,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,WAAW,YAAY;AAEzC,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;AC9EH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAa,eAAb,cAAkC,WAAW;CAC3C,OAAc;CACd,UAA0B,CAAC,QAAQ;;;;;;CAOnC,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAgB,QAAQ"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
|
|
9
|
+
|
|
10
|
+
export type BodyLimitCapability =
|
|
11
|
+
| "unvalidated-request-body"
|
|
12
|
+
| "prevalidated-request-body";
|
|
13
|
+
|
|
14
|
+
export type BodyLimitPolicy = {
|
|
15
|
+
readonly maxBodySize: number;
|
|
16
|
+
readonly capability: BodyLimitCapability;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type RequestBodyPrevalidation = {
|
|
20
|
+
readonly maxBodySize: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// NodeAdapter reads and caps IncomingMessage bodies before creating Fetch
|
|
24
|
+
// Requests. This WeakMap records that Node→Fetch handoff so FetchApiAdapter
|
|
25
|
+
// skips re-reading only when the Node policy was at least as strict.
|
|
26
|
+
const requestBodyPrevalidations = new WeakMap<
|
|
27
|
+
Request,
|
|
28
|
+
RequestBodyPrevalidation
|
|
29
|
+
>();
|
|
30
|
+
|
|
31
|
+
const FETCH_BODY_LIMIT_CAPABILITY: BodyLimitCapability =
|
|
32
|
+
"unvalidated-request-body";
|
|
33
|
+
|
|
34
|
+
const NODE_BODY_LIMIT_CAPABILITY: BodyLimitCapability =
|
|
35
|
+
"prevalidated-request-body";
|
|
36
|
+
|
|
37
|
+
const CONTENT_LENGTH_HEADER_PATTERN = /^\d+$/;
|
|
38
|
+
|
|
39
|
+
export function createFetchBodyLimitPolicy(
|
|
40
|
+
maxBodySize?: number
|
|
41
|
+
): BodyLimitPolicy {
|
|
42
|
+
return {
|
|
43
|
+
maxBodySize: resolveMaxBodySize(maxBodySize),
|
|
44
|
+
capability: FETCH_BODY_LIMIT_CAPABILITY,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createNodeBodyLimitPolicy(
|
|
49
|
+
maxBodySize?: number
|
|
50
|
+
): BodyLimitPolicy {
|
|
51
|
+
return {
|
|
52
|
+
maxBodySize: resolveMaxBodySize(maxBodySize),
|
|
53
|
+
capability: NODE_BODY_LIMIT_CAPABILITY,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveMaxBodySize(maxBodySize?: number): number {
|
|
58
|
+
return maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseContentLength(
|
|
62
|
+
value: string | string[] | null | undefined
|
|
63
|
+
): number | undefined {
|
|
64
|
+
const rawValue = Array.isArray(value) ? value[0] : value;
|
|
65
|
+
if (rawValue === undefined || rawValue === null) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const trimmedValue = rawValue.trim();
|
|
70
|
+
if (!CONTENT_LENGTH_HEADER_PATTERN.test(trimmedValue)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contentLength = Number(trimmedValue);
|
|
75
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return contentLength;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isBodySizeOverLimit(
|
|
83
|
+
bodySize: number,
|
|
84
|
+
maxBodySize: number
|
|
85
|
+
): boolean {
|
|
86
|
+
return bodySize > maxBodySize;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function markRequestBodyPrevalidated(
|
|
90
|
+
request: Request,
|
|
91
|
+
policy: BodyLimitPolicy
|
|
92
|
+
): void {
|
|
93
|
+
if (policy.capability !== "prevalidated-request-body") {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
requestBodyPrevalidations.set(request, {
|
|
98
|
+
maxBodySize: policy.maxBodySize,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getRequestBodyPrevalidation(
|
|
103
|
+
request: Request
|
|
104
|
+
): RequestBodyPrevalidation | undefined {
|
|
105
|
+
return requestBodyPrevalidations.get(request);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hasSatisfiedBodyLimitPolicy(
|
|
109
|
+
request: Request,
|
|
110
|
+
policy: BodyLimitPolicy
|
|
111
|
+
): boolean {
|
|
112
|
+
const prevalidation = getRequestBodyPrevalidation(request);
|
|
113
|
+
if (!prevalidation) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return prevalidation.maxBodySize <= policy.maxBodySize;
|
|
118
|
+
}
|
package/dist/lib/Errors.ts
CHANGED
|
@@ -29,6 +29,22 @@ export class PayloadTooLargeError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Error thrown when a skipped request body cannot be drained before timeout.
|
|
34
|
+
* Caught by NodeAdapter to return a 413 Payload Too Large response.
|
|
35
|
+
*/
|
|
36
|
+
export class RequestBodyDrainTimeoutError extends Error {
|
|
37
|
+
public override readonly name = "RequestBodyDrainTimeoutError";
|
|
38
|
+
public constructor(
|
|
39
|
+
public readonly maxBodySize: number,
|
|
40
|
+
public readonly timeoutMs: number
|
|
41
|
+
) {
|
|
42
|
+
super(
|
|
43
|
+
`Request body drain timed out after ${timeoutMs}ms while enforcing ${maxBodySize} byte limit`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Error thrown when the response body cannot be serialized to JSON.
|
|
34
50
|
* Typically caused by circular references or non-serializable values.
|
|
@@ -13,14 +13,22 @@ import type {
|
|
|
13
13
|
IHttpRequest,
|
|
14
14
|
IHttpResponse,
|
|
15
15
|
} from "@rexeus/typeweaver-core";
|
|
16
|
+
import {
|
|
17
|
+
createFetchBodyLimitPolicy,
|
|
18
|
+
hasSatisfiedBodyLimitPolicy,
|
|
19
|
+
isBodySizeOverLimit,
|
|
20
|
+
parseContentLength,
|
|
21
|
+
} from "./BodyLimitPolicy.js";
|
|
16
22
|
import {
|
|
17
23
|
BodyParseError,
|
|
18
24
|
PayloadTooLargeError,
|
|
19
25
|
ResponseSerializationError,
|
|
20
26
|
} from "./Errors.js";
|
|
27
|
+
import type { BodyLimitPolicy } from "./BodyLimitPolicy.js";
|
|
21
28
|
|
|
22
29
|
export type FetchApiAdapterOptions = {
|
|
23
30
|
readonly maxBodySize?: number;
|
|
31
|
+
readonly bodyLimitPolicy?: BodyLimitPolicy;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
/**
|
|
@@ -35,13 +43,12 @@ export type FetchApiAdapterOptions = {
|
|
|
35
43
|
* Bun, Deno, Node.js (>=18), Cloudflare Workers.
|
|
36
44
|
*/
|
|
37
45
|
export class FetchApiAdapter {
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
private readonly maxBodySize: number;
|
|
46
|
+
private readonly bodyLimitPolicy: BodyLimitPolicy;
|
|
41
47
|
|
|
42
48
|
public constructor(options?: FetchApiAdapterOptions) {
|
|
43
|
-
this.
|
|
44
|
-
options?.
|
|
49
|
+
this.bodyLimitPolicy =
|
|
50
|
+
options?.bodyLimitPolicy ??
|
|
51
|
+
createFetchBodyLimitPolicy(options?.maxBodySize);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/**
|
|
@@ -234,21 +241,24 @@ export class FetchApiAdapter {
|
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
private async enforceBodySizeLimit(request: Request): Promise<Request> {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return this.readBodyWithLimit(request);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const contentLength = Number(contentLengthHeader);
|
|
243
|
-
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
244
|
-
return this.readBodyWithLimit(request);
|
|
244
|
+
if (hasSatisfiedBodyLimitPolicy(request, this.bodyLimitPolicy)) {
|
|
245
|
+
return request;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
const contentLength = parseContentLength(
|
|
249
|
+
request.headers.get("content-length")
|
|
250
|
+
);
|
|
251
|
+
if (
|
|
252
|
+
contentLength !== undefined &&
|
|
253
|
+
isBodySizeOverLimit(contentLength, this.bodyLimitPolicy.maxBodySize)
|
|
254
|
+
) {
|
|
255
|
+
throw new PayloadTooLargeError(
|
|
256
|
+
contentLength,
|
|
257
|
+
this.bodyLimitPolicy.maxBodySize
|
|
258
|
+
);
|
|
249
259
|
}
|
|
250
260
|
|
|
251
|
-
return request;
|
|
261
|
+
return this.readBodyWithLimit(request);
|
|
252
262
|
}
|
|
253
263
|
|
|
254
264
|
private async readBodyWithLimit(request: Request): Promise<Request> {
|
|
@@ -264,14 +274,27 @@ export class FetchApiAdapter {
|
|
|
264
274
|
if (done) break;
|
|
265
275
|
|
|
266
276
|
totalBytes += value.byteLength;
|
|
267
|
-
if (totalBytes
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
if (isBodySizeOverLimit(totalBytes, this.bodyLimitPolicy.maxBodySize)) {
|
|
278
|
+
throw new PayloadTooLargeError(
|
|
279
|
+
totalBytes,
|
|
280
|
+
this.bodyLimitPolicy.maxBodySize
|
|
281
|
+
);
|
|
270
282
|
}
|
|
271
283
|
chunks.push(value);
|
|
272
284
|
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
try {
|
|
287
|
+
await reader.cancel();
|
|
288
|
+
} catch {
|
|
289
|
+
// Preserve the original read failure if stream cleanup also fails.
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
273
292
|
} finally {
|
|
274
|
-
|
|
293
|
+
try {
|
|
294
|
+
reader.releaseLock();
|
|
295
|
+
} catch {
|
|
296
|
+
// Some runtimes may report release errors after stream termination.
|
|
297
|
+
}
|
|
275
298
|
}
|
|
276
299
|
|
|
277
300
|
return new Request(request.url, {
|
|
@@ -299,10 +322,15 @@ export class FetchApiAdapter {
|
|
|
299
322
|
): string | ArrayBuffer | Blob | null {
|
|
300
323
|
if (body === undefined || body === null) return null;
|
|
301
324
|
if (typeof body === "string") return body;
|
|
302
|
-
if (body instanceof
|
|
325
|
+
if (body instanceof ArrayBuffer) return body;
|
|
326
|
+
if (body instanceof Blob) return body;
|
|
303
327
|
|
|
304
328
|
try {
|
|
305
|
-
|
|
329
|
+
const serializedBody = JSON.stringify(body);
|
|
330
|
+
if (serializedBody === undefined) {
|
|
331
|
+
throw new TypeError("Response body cannot be serialized to JSON");
|
|
332
|
+
}
|
|
333
|
+
return serializedBody;
|
|
306
334
|
} catch (error) {
|
|
307
335
|
throw new ResponseSerializationError(
|
|
308
336
|
"Failed to serialize response body to JSON",
|
|
@@ -332,6 +360,10 @@ export class FetchApiAdapter {
|
|
|
332
360
|
headers.set("content-type", "application/json");
|
|
333
361
|
}
|
|
334
362
|
|
|
363
|
+
if (!headers.has("content-type") && body instanceof Blob && body.type) {
|
|
364
|
+
headers.set("content-type", body.type);
|
|
365
|
+
}
|
|
366
|
+
|
|
335
367
|
return headers;
|
|
336
368
|
}
|
|
337
369
|
|