@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 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, _rexeus_typeweaver_gen.toPascalCase)(resource.name);
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, _rexeus_typeweaver_gen.toPascalCase)(operationId);
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, toPascalCase } from "@rexeus/typeweaver-gen";
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 = toPascalCase(resource.name);
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 = toPascalCase(operationId);
39
+ const className = pascalCase(operationId);
39
40
  return {
40
41
  operationId,
41
42
  className,
@@ -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, toPascalCase } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\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: GeneratorContext): 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: GeneratorContext\n): void {\n const pascalCaseEntityName = toPascalCase(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 = toPascalCase(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":";;;;;;;;;;;;;;;;AA8BA,SAAgB,SAAS,SAAiC;CACxD,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,aAAa,SAAS,KAAK;CACxD,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,aAAa,YAAY;AAE3C,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACpEH,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"}
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
+ }
@@ -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 static readonly DEFAULT_MAX_BODY_SIZE = 1_048_576; // 1 MB
39
-
40
- private readonly maxBodySize: number;
46
+ private readonly bodyLimitPolicy: BodyLimitPolicy;
41
47
 
42
48
  public constructor(options?: FetchApiAdapterOptions) {
43
- this.maxBodySize =
44
- options?.maxBodySize ?? FetchApiAdapter.DEFAULT_MAX_BODY_SIZE;
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
- const contentLengthHeader = request.headers.get("content-length");
238
- if (contentLengthHeader === null) {
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
- if (contentLength > this.maxBodySize) {
248
- throw new PayloadTooLargeError(contentLength, this.maxBodySize);
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 > this.maxBodySize) {
268
- await reader.cancel();
269
- throw new PayloadTooLargeError(totalBytes, this.maxBodySize);
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
- reader.releaseLock();
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 Blob || body instanceof ArrayBuffer) return body;
325
+ if (body instanceof ArrayBuffer) return body;
326
+ if (body instanceof Blob) return body;
303
327
 
304
328
  try {
305
- return JSON.stringify(body);
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