@orpc/server 2.0.0-beta.4 → 2.0.0-beta.40

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 (60) hide show
  1. package/README.md +97 -94
  2. package/dist/adapters/aws-lambda/index.d.mts +50 -0
  3. package/dist/adapters/aws-lambda/index.d.ts +50 -0
  4. package/dist/adapters/aws-lambda/index.mjs +39 -0
  5. package/dist/adapters/crossws/index.d.mts +21 -12
  6. package/dist/adapters/crossws/index.d.ts +21 -12
  7. package/dist/adapters/crossws/index.mjs +14 -10
  8. package/dist/adapters/fastify/index.d.mts +42 -0
  9. package/dist/adapters/fastify/index.d.ts +42 -0
  10. package/dist/adapters/fastify/index.mjs +39 -0
  11. package/dist/adapters/fetch/index.d.mts +21 -114
  12. package/dist/adapters/fetch/index.d.ts +21 -114
  13. package/dist/adapters/fetch/index.mjs +20 -182
  14. package/dist/adapters/message-port/index.d.mts +23 -20
  15. package/dist/adapters/message-port/index.d.ts +23 -20
  16. package/dist/adapters/message-port/index.mjs +31 -34
  17. package/dist/adapters/node/index.d.mts +19 -96
  18. package/dist/adapters/node/index.d.ts +19 -96
  19. package/dist/adapters/node/index.mjs +19 -157
  20. package/dist/adapters/standard/index.d.mts +91 -17
  21. package/dist/adapters/standard/index.d.ts +91 -17
  22. package/dist/adapters/standard/index.mjs +4 -4
  23. package/dist/adapters/standard-peer/index.d.mts +6 -11
  24. package/dist/adapters/standard-peer/index.d.ts +6 -11
  25. package/dist/adapters/standard-peer/index.mjs +10 -2
  26. package/dist/adapters/websocket/index.d.mts +45 -29
  27. package/dist/adapters/websocket/index.d.ts +45 -29
  28. package/dist/adapters/websocket/index.mjs +37 -28
  29. package/dist/extensions/callable.d.mts +5 -5
  30. package/dist/extensions/callable.d.ts +5 -5
  31. package/dist/extensions/callable.mjs +2 -2
  32. package/dist/helpers/index.d.mts +23 -6
  33. package/dist/helpers/index.d.ts +23 -6
  34. package/dist/helpers/index.mjs +13 -5
  35. package/dist/index.d.mts +332 -37
  36. package/dist/index.d.ts +332 -37
  37. package/dist/index.mjs +131 -49
  38. package/dist/plugins/index.d.mts +243 -31
  39. package/dist/plugins/index.d.ts +243 -31
  40. package/dist/plugins/index.mjs +527 -30
  41. package/dist/shared/server.1ExWFMze.mjs +69 -0
  42. package/dist/shared/{server.CjOb6ItT.mjs → server.24nXyhbl.mjs} +1 -1
  43. package/dist/shared/{server.DBANIheG.d.ts → server.B5K1CViq.d.mts} +24 -13
  44. package/dist/shared/{server.BsNNjG5J.d.mts → server.BpmUhems.d.mts} +32 -20
  45. package/dist/shared/{server.BL22TloH.d.mts → server.BsujoJoF.d.mts} +38 -59
  46. package/dist/shared/{server.BL22TloH.d.ts → server.BsujoJoF.d.ts} +38 -59
  47. package/dist/shared/{server.BwHnWUuN.mjs → server.BynNaBBA.mjs} +24 -19
  48. package/dist/shared/{server.GDpX6Df8.mjs → server.C0lFtoHU.mjs} +162 -68
  49. package/dist/shared/server.C7xllBNN.d.mts +34 -0
  50. package/dist/shared/server.CFqSIFnT.d.mts +14 -0
  51. package/dist/shared/{server.Pa0F03f_.d.ts → server.CRwPbhLO.d.ts} +32 -20
  52. package/dist/shared/{server.DJIkub_t.d.mts → server.Cxranw-8.d.ts} +24 -13
  53. package/dist/shared/server.D0dr-F8u.d.ts +34 -0
  54. package/dist/shared/{server.CrlKQucM.mjs → server.Djb_ebJT.mjs} +25 -53
  55. package/dist/shared/server.RRVK1xTV.d.ts +14 -0
  56. package/package.json +47 -14
  57. package/dist/shared/server.CX4vUnDk.mjs +0 -11
  58. package/dist/shared/server.Cj5lgPuG.d.mts +0 -66
  59. package/dist/shared/server.D_QauotT.mjs +0 -30
  60. package/dist/shared/server.d_2NS3g5.d.ts +0 -66
@@ -1,34 +1,44 @@
1
- import { toArray, value } from '@orpc/shared';
2
- import { flattenStandardHeader, parseStandardUrl, mergeStandardHeaders } from '@standardserver/core';
3
- import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
- export { C as CSRFGuardHandlerPlugin } from '../shared/server.D_QauotT.mjs';
5
- import { toFetchHeaders, toStandardHeaders } from '@standardserver/fetch';
6
- import '@orpc/client';
1
+ import { toArray, value, isPlainObject, isTypescriptObject, isAsyncIteratorObject, override, wrapAsyncIterator, isCompressibleContentType, isNoTransformCacheControl, parseAcceptEncodingQualities, varyByAcceptEncoding, stringifyJSON, anyAbortSignal, wrapReadableStream, AbortError } from '@orpc/shared';
2
+ import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standard-server/core';
3
+ import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standard-server/peer';
4
+ import { ORPCError } from '@orpc/client';
5
+ import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standard-server/fetch';
6
+ export { R as RequestLimitHandlerPlugin } from '../shared/server.1ExWFMze.mjs';
7
7
 
8
+ const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
8
9
  class BatchHandlerPlugin {
9
10
  name = "~batch";
10
11
  /**
11
- * Run batch interceptors before OpenTelemetry interceptors
12
+ * Run batch interceptors inside the tracing interceptors
12
13
  * so each subrequest gets its own span instead of sharing one batch-level span.
13
14
  */
14
- after = ["~opentelemetry"];
15
+ after = ["~tracing"];
15
16
  maxSize;
16
17
  mapSubrequest;
17
18
  successStatus;
18
19
  headers;
20
+ keepAliveEnabled;
21
+ keepAliveInterval;
19
22
  constructor(options = {}) {
20
23
  this.maxSize = options.maxSize ?? 10;
21
24
  this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
22
25
  ...subRequest,
23
26
  headers: {
24
- ...batchRequest.headers,
25
27
  ...subRequest.headers,
28
+ /**
29
+ * The batch request headers win over the subrequest ones. They are the only ones the
30
+ * transport actually saw, so headers the browser injects on its own, such as `cookie`
31
+ * or `origin`, cannot be overridden by a subrequest, which is just request payload.
32
+ */
33
+ ...batchRequest.headers,
26
34
  "orpc-batch": void 0
27
35
  // useful in case batch plugin is used multiple times
28
36
  }
29
37
  }));
30
38
  this.successStatus = options.successStatus ?? 207;
31
39
  this.headers = options.headers ?? {};
40
+ this.keepAliveEnabled = options.keepAlive?.enabled ?? true;
41
+ this.keepAliveInterval = options.keepAlive?.interval ?? 15e3;
32
42
  }
33
43
  init(options) {
34
44
  const routingInterceptor = async (interceptorOptions) => {
@@ -59,7 +69,7 @@ class BatchHandlerPlugin {
59
69
  messages = mightBeMessages;
60
70
  } else {
61
71
  const mightBeMessages = await interceptorOptions.request.resolveBody();
62
- if (!Array.isArray(mightBeMessages)) {
72
+ if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
63
73
  return {
64
74
  matched: true,
65
75
  response: { status: 400, headers: {}, body: "Invalid batch request body" }
@@ -73,6 +83,13 @@ class BatchHandlerPlugin {
73
83
  response: { status: 400, headers: {}, body: "Invalid batch request" }
74
84
  };
75
85
  }
86
+ const outerMethod = interceptorOptions.request.method;
87
+ if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) {
88
+ return {
89
+ matched: true,
90
+ response: { status: 400, headers: {}, body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests` }
91
+ };
92
+ }
76
93
  const maxSize = await value(this.maxSize, interceptorOptions);
77
94
  if (messages.length > maxSize) {
78
95
  return {
@@ -117,7 +134,7 @@ class BatchHandlerPlugin {
117
134
  response: {
118
135
  status,
119
136
  headers,
120
- body: new Blob(chunks, { type: "application/octet-stream" })
137
+ body: new Blob(chunks, { type: BATCH_CONTENT_TYPE })
121
138
  }
122
139
  };
123
140
  }
@@ -127,9 +144,35 @@ class BatchHandlerPlugin {
127
144
  };
128
145
  }
129
146
  let streamController;
147
+ let keepAliveTimer;
148
+ const clearKeepAlive = () => {
149
+ if (keepAliveTimer !== void 0) {
150
+ clearInterval(keepAliveTimer);
151
+ keepAliveTimer = void 0;
152
+ }
153
+ };
154
+ const scheduleKeepAlive = () => {
155
+ if (!this.keepAliveEnabled) {
156
+ return;
157
+ }
158
+ clearKeepAlive();
159
+ keepAliveTimer = setInterval(() => {
160
+ try {
161
+ const lengthBuffer = new ArrayBuffer(4);
162
+ new DataView(lengthBuffer).setUint32(0, 0, false);
163
+ streamController.enqueue(new Uint8Array(lengthBuffer));
164
+ } catch {
165
+ clearKeepAlive();
166
+ }
167
+ }, this.keepAliveInterval);
168
+ };
130
169
  const stream = new ReadableStream({
131
170
  start(controller) {
132
171
  streamController = controller;
172
+ scheduleKeepAlive();
173
+ },
174
+ cancel() {
175
+ clearKeepAlive();
133
176
  }
134
177
  });
135
178
  const peer = new ServerPeer(async (message) => {
@@ -139,17 +182,24 @@ class BatchHandlerPlugin {
139
182
  new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
140
183
  streamController.enqueue(new Uint8Array(lengthBuffer));
141
184
  streamController.enqueue(bytes);
185
+ scheduleKeepAlive();
142
186
  });
143
187
  Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
188
+ clearKeepAlive();
144
189
  streamController.close();
145
190
  await peer.close();
146
191
  }).catch(async (error) => {
192
+ clearKeepAlive();
147
193
  streamController.error(error);
148
194
  await peer.close(error);
149
195
  });
150
196
  return {
151
197
  matched: true,
152
- response: { status, headers, body: stream }
198
+ response: {
199
+ status,
200
+ headers: { ...headers, "content-type": BATCH_CONTENT_TYPE },
201
+ body: stream
202
+ }
153
203
  };
154
204
  };
155
205
  return {
@@ -167,11 +217,11 @@ class CORSHandlerPlugin {
167
217
  * - Run CORS interceptors before batch interceptors so headers are applied to
168
218
  * the actual response rather than sub-responses.
169
219
  */
170
- after = ["~opentelemetry", "~batch"];
220
+ after = ["~tracing", "~batch"];
171
221
  constructor(options = {}) {
172
222
  const defaults = {
173
- origin: (origin) => origin,
174
- allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
223
+ origin: "*",
224
+ allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "QUERY"]
175
225
  };
176
226
  this.options = {
177
227
  ...defaults,
@@ -185,21 +235,20 @@ class CORSHandlerPlugin {
185
235
  return result;
186
236
  }
187
237
  const resHeaders = { ...result.response.headers };
188
- const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
189
- const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
190
- if (allowedOrigins.includes("*")) {
191
- resHeaders["access-control-allow-origin"] = "*";
192
- } else {
193
- if (allowedOrigins.includes(origin)) {
194
- resHeaders["access-control-allow-origin"] = origin;
195
- }
196
- resHeaders.vary = interceptorOptions.request.headers.vary ?? "origin";
238
+ const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
239
+ const allowOrigin = await this.resolveOrigin(this.options.origin, origin, interceptorOptions);
240
+ const timingAllowOrigin = await this.resolveOrigin(this.options.timingOrigin, origin, interceptorOptions);
241
+ if (allowOrigin.value !== void 0) {
242
+ resHeaders["access-control-allow-origin"] = allowOrigin.value;
197
243
  }
198
- const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
199
- if (allowedTimingOrigins.includes("*")) {
200
- resHeaders["timing-allow-origin"] = "*";
201
- } else if (allowedTimingOrigins.includes(origin)) {
202
- resHeaders["timing-allow-origin"] = origin;
244
+ if (timingAllowOrigin.value !== void 0) {
245
+ resHeaders["timing-allow-origin"] = timingAllowOrigin.value;
246
+ }
247
+ if (allowOrigin.varies || timingAllowOrigin.varies) {
248
+ const existingVary = flattenStandardHeader(resHeaders.vary);
249
+ if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
250
+ resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
251
+ }
203
252
  }
204
253
  if (this.options.credentials) {
205
254
  resHeaders["access-control-allow-credentials"] = "true";
@@ -242,6 +291,234 @@ class CORSHandlerPlugin {
242
291
  ]
243
292
  };
244
293
  }
294
+ async resolveOrigin(option, origin, interceptorOptions) {
295
+ if (typeof option === "function") {
296
+ if (origin === void 0) {
297
+ return { value: void 0, varies: true };
298
+ }
299
+ const allowed2 = toArray(await option(origin, interceptorOptions));
300
+ if (allowed2.includes("*")) {
301
+ return { value: "*", varies: true };
302
+ }
303
+ return { value: allowed2.includes(origin) ? origin : void 0, varies: true };
304
+ }
305
+ const allowed = toArray(await option);
306
+ if (allowed.includes("*")) {
307
+ return { value: "*", varies: false };
308
+ }
309
+ if (origin !== void 0 && allowed.includes(origin)) {
310
+ return { value: origin, varies: true };
311
+ }
312
+ return { value: void 0, varies: allowed.length > 0 };
313
+ }
314
+ }
315
+
316
+ class GetMethodCsrfProtectionHandlerPlugin {
317
+ name = "~get-method-csrf-protection";
318
+ init(options) {
319
+ const interceptor = (interceptorOptions) => {
320
+ if (this.isAllowed(interceptorOptions)) {
321
+ return interceptorOptions.next();
322
+ }
323
+ throw new ORPCError("FORBIDDEN", { message: "Request blocked by CSRF protection." });
324
+ };
325
+ return {
326
+ ...options,
327
+ interceptors: [interceptor, ...toArray(options.interceptors)]
328
+ };
329
+ }
330
+ isAllowed({ request }) {
331
+ if (request.method !== "GET") {
332
+ return true;
333
+ }
334
+ const site = flattenStandardHeader(request.headers["sec-fetch-site"])?.toLowerCase();
335
+ if (site === void 0 || site === "same-origin" || site === "same-site") {
336
+ return true;
337
+ }
338
+ const mode = flattenStandardHeader(request.headers["sec-fetch-mode"])?.toLowerCase();
339
+ const dest = flattenStandardHeader(request.headers["sec-fetch-dest"])?.toLowerCase();
340
+ if (mode === void 0 || dest === void 0) {
341
+ return false;
342
+ }
343
+ return mode !== "navigate" || dest !== "document";
344
+ }
345
+ }
346
+
347
+ class MethodOverrideHandlerPlugin {
348
+ name = "~method-override";
349
+ /**
350
+ * Should override batch sub-request methods, not the original batch request.
351
+ */
352
+ before = ["~batch"];
353
+ param;
354
+ methods;
355
+ constructor(options = {}) {
356
+ this.param = options.param ?? "method";
357
+ this.methods = new Set((options.methods ?? ["PUT", "PATCH", "DELETE"]).map((method) => method.toUpperCase()));
358
+ }
359
+ init(options) {
360
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
361
+ const { request } = interceptorOptions;
362
+ if (request.method !== "POST") {
363
+ return next();
364
+ }
365
+ const [pathname, search, hash] = parseStandardUrl(request.url);
366
+ const params = new URLSearchParams(search);
367
+ const raw = params.getAll(this.param).at(-1);
368
+ if (raw === void 0) {
369
+ return next();
370
+ }
371
+ const method = raw.toUpperCase();
372
+ if (!this.methods.has(method)) {
373
+ return next();
374
+ }
375
+ params.delete(this.param);
376
+ const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
377
+ return next({
378
+ ...interceptorOptions,
379
+ request: { ...request, method, url }
380
+ });
381
+ };
382
+ return {
383
+ ...options,
384
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
385
+ };
386
+ }
387
+ }
388
+
389
+ class PrototypePollutionProtectionHandlerPlugin {
390
+ name = "~prototype-pollution-protection";
391
+ init(options) {
392
+ const interceptor = (interceptorOptions) => {
393
+ const input = interceptorOptions.input;
394
+ if (isAsyncIteratorObject(input)) {
395
+ const guardedInput = override(input, wrapAsyncIterator(input, {
396
+ mapResult: (result) => {
397
+ this.rejectPollutingInput(result.value);
398
+ return result;
399
+ }
400
+ }));
401
+ return interceptorOptions.next({ ...interceptorOptions, input: guardedInput });
402
+ }
403
+ this.rejectPollutingInput(input);
404
+ return interceptorOptions.next();
405
+ };
406
+ return {
407
+ ...options,
408
+ clientInterceptors: [interceptor, ...toArray(options.clientInterceptors)]
409
+ };
410
+ }
411
+ rejectPollutingInput(input) {
412
+ if (this.containsPollutingKey(input)) {
413
+ throw new ORPCError("BAD_REQUEST", { message: "Request blocked by prototype pollution protection." });
414
+ }
415
+ }
416
+ /**
417
+ * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
418
+ * objects, including null-prototype ones. Other objects, such as files and dates, carry
419
+ * no attacker-authored keys and are left alone. The walk is iterative, so input nested
420
+ * deeper than the call stack allows still gets the intended verdict instead of a
421
+ * `RangeError`.
422
+ */
423
+ containsPollutingKey(root) {
424
+ const visited = /* @__PURE__ */ new WeakSet();
425
+ const stack = [root];
426
+ while (stack.length !== 0) {
427
+ const value = stack.pop();
428
+ if (typeof value !== "object" || value === null || visited.has(value)) {
429
+ continue;
430
+ }
431
+ visited.add(value);
432
+ if (Array.isArray(value)) {
433
+ for (const item of value) {
434
+ stack.push(item);
435
+ }
436
+ continue;
437
+ }
438
+ if (value instanceof Map || value instanceof Set) {
439
+ for (const entry of value) {
440
+ stack.push(entry);
441
+ }
442
+ continue;
443
+ }
444
+ if (!isPlainObject(value)) {
445
+ continue;
446
+ }
447
+ if (Object.hasOwn(value, "__proto__")) {
448
+ return true;
449
+ }
450
+ if (Object.hasOwn(value, "constructor") && isTypescriptObject(value.constructor) && Object.hasOwn(value.constructor, "prototype")) {
451
+ return true;
452
+ }
453
+ for (const key of Object.keys(value)) {
454
+ stack.push(value[key]);
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+ }
460
+
461
+ class RequestCompressionHandlerPlugin {
462
+ name = "~request-compression";
463
+ /**
464
+ * Should decompress the original batch request body instead of sub-requests.
465
+ */
466
+ after = ["~batch"];
467
+ init(options) {
468
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
469
+ const encodings = parseContentEncodings(
470
+ flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
471
+ );
472
+ if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
473
+ return next();
474
+ }
475
+ const decompressedHeaders = {
476
+ ...interceptorOptions.request.headers,
477
+ "content-length": void 0,
478
+ "content-encoding": void 0
479
+ };
480
+ return next({
481
+ ...interceptorOptions,
482
+ request: {
483
+ ...interceptorOptions.request,
484
+ headers: decompressedHeaders,
485
+ async resolveBody(hint) {
486
+ const stream = await interceptorOptions.request.resolveBody("octet-stream");
487
+ if (!(stream instanceof ReadableStream)) {
488
+ return stream;
489
+ }
490
+ let decompressedStream = stream;
491
+ for (let i = encodings.length - 1; i >= 0; i--) {
492
+ decompressedStream = decompressedStream.pipeThrough(
493
+ new DecompressionStream(encodings[i])
494
+ );
495
+ }
496
+ const response = new Response(decompressedStream, {
497
+ headers: toFetchHeaders(decompressedHeaders)
498
+ });
499
+ return toStandardBody(response, { hint });
500
+ }
501
+ }
502
+ });
503
+ };
504
+ return {
505
+ ...options,
506
+ routingInterceptors: [
507
+ routingInterceptor,
508
+ ...toArray(options.routingInterceptors)
509
+ ]
510
+ };
511
+ }
512
+ }
513
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
514
+ function isSupportedEncoding(encoding) {
515
+ return SUPPORTED_ENCODINGS.includes(encoding);
516
+ }
517
+ function parseContentEncodings(header) {
518
+ if (header === void 0) {
519
+ return [];
520
+ }
521
+ return header.split(",").map((part) => part.trim().toLowerCase());
245
522
  }
246
523
 
247
524
  class RequestHeadersHandlerPlugin {
@@ -266,6 +543,177 @@ class RequestHeadersHandlerPlugin {
266
543
  }
267
544
  }
268
545
 
546
+ const AVG_BYTES_PER_CHAR = 1.2;
547
+ class ResponseCompressionHandlerPlugin {
548
+ name = "~response-compression";
549
+ /**
550
+ * Compression should be done after batching, to compress the final response.
551
+ * Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
552
+ */
553
+ after = ["~batch", "~response-headers"];
554
+ encodings;
555
+ threshold;
556
+ isCompressibleContentType;
557
+ constructor(options = {}) {
558
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
559
+ this.threshold = options.threshold ?? 1024;
560
+ this.isCompressibleContentType = options.isCompressibleContentType ?? isCompressibleContentType;
561
+ }
562
+ init(options) {
563
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
564
+ const result = await next();
565
+ if (!result.matched) {
566
+ return result;
567
+ }
568
+ const response = result.response;
569
+ const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
570
+ if (contentEncoding !== void 0) {
571
+ return result;
572
+ }
573
+ if (response.status === 206 || response.headers["content-range"] !== void 0) {
574
+ return result;
575
+ }
576
+ if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
577
+ return result;
578
+ }
579
+ const acceptEncodings = parseAcceptEncodingQualities(
580
+ flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
581
+ );
582
+ const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
583
+ if (encoding === void 0) {
584
+ return result;
585
+ }
586
+ const body = response.body;
587
+ const headers = response.headers;
588
+ if (body instanceof ReadableStream) {
589
+ const contentLength = Number(flattenStandardHeader(headers["content-length"]));
590
+ if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && this.isCompressibleContentType(flattenStandardHeader(headers["content-type"]), interceptorOptions)) {
591
+ return {
592
+ ...result,
593
+ response: {
594
+ ...response,
595
+ body: body.pipeThrough(new CompressionStream(encoding)),
596
+ headers: {
597
+ ...headers,
598
+ "standard-server": "octet-stream",
599
+ "content-length": [],
600
+ "content-encoding": encoding,
601
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
602
+ }
603
+ }
604
+ };
605
+ }
606
+ } else if (body instanceof Blob) {
607
+ if ((!Number.isFinite(body.size) || body.size >= this.threshold) && this.isCompressibleContentType(body.type, interceptorOptions)) {
608
+ const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
609
+ body instanceof File ? body.name : "blob"
610
+ );
611
+ return {
612
+ ...result,
613
+ response: {
614
+ ...response,
615
+ body: body.stream().pipeThrough(new CompressionStream(encoding)),
616
+ headers: {
617
+ ...headers,
618
+ "standard-server": "file",
619
+ "content-type": body.type,
620
+ "content-length": [],
621
+ "content-disposition": contentDisposition,
622
+ "content-encoding": encoding,
623
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
624
+ }
625
+ }
626
+ };
627
+ }
628
+ } else if (body instanceof FormData) {
629
+ const PART_OVERHEAD = 64;
630
+ let contentLength = 0;
631
+ for (const [key, value] of body) {
632
+ contentLength += PART_OVERHEAD + key.length;
633
+ if (value instanceof Blob) {
634
+ if (!Number.isFinite(value.size)) {
635
+ if (!this.isCompressibleContentType(value.type, interceptorOptions)) {
636
+ contentLength = -Infinity;
637
+ break;
638
+ }
639
+ contentLength = Infinity;
640
+ } else {
641
+ contentLength += this.isCompressibleContentType(value.type, interceptorOptions) ? value.size : -value.size;
642
+ }
643
+ } else {
644
+ contentLength += value.length * AVG_BYTES_PER_CHAR;
645
+ }
646
+ }
647
+ if (contentLength >= this.threshold) {
648
+ const res = new Response(body);
649
+ const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
650
+ return {
651
+ ...result,
652
+ response: {
653
+ ...response,
654
+ body: compressedStream,
655
+ headers: {
656
+ ...headers,
657
+ "standard-server": [],
658
+ "content-type": res.headers.get("content-type"),
659
+ "content-length": [],
660
+ "content-encoding": encoding,
661
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
662
+ }
663
+ }
664
+ };
665
+ }
666
+ } else if (body instanceof URLSearchParams) {
667
+ const string = body.toString();
668
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
669
+ return {
670
+ ...result,
671
+ response: {
672
+ ...response,
673
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
674
+ headers: {
675
+ ...headers,
676
+ "standard-server": [],
677
+ "content-type": "application/x-www-form-urlencoded",
678
+ "content-length": [],
679
+ "content-encoding": encoding,
680
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
681
+ }
682
+ }
683
+ };
684
+ }
685
+ } else if (body !== void 0 && !isAsyncIteratorObject(body)) {
686
+ const string = stringifyJSON(body);
687
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
688
+ return {
689
+ ...result,
690
+ response: {
691
+ ...response,
692
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
693
+ headers: {
694
+ ...headers,
695
+ "standard-server": [],
696
+ "content-type": "application/json",
697
+ "content-length": [],
698
+ "content-encoding": encoding,
699
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
700
+ }
701
+ }
702
+ };
703
+ }
704
+ }
705
+ return result;
706
+ };
707
+ return {
708
+ ...options,
709
+ routingInterceptors: [
710
+ routingInterceptor,
711
+ ...toArray(options.routingInterceptors)
712
+ ]
713
+ };
714
+ }
715
+ }
716
+
269
717
  class ResponseHeadersHandlerPlugin {
270
718
  name = "~response-headers";
271
719
  /**
@@ -354,4 +802,53 @@ class RethrowHandlerPlugin {
354
802
  }
355
803
  }
356
804
 
357
- export { BatchHandlerPlugin, CORSHandlerPlugin, RequestHeadersHandlerPlugin, ResponseHeadersHandlerPlugin, RethrowHandlerPlugin };
805
+ class TimeoutHandlerPlugin {
806
+ timeout;
807
+ streamingTimeout;
808
+ name = "~timeout";
809
+ constructor(options) {
810
+ this.timeout = options.timeout;
811
+ this.streamingTimeout = options.streamingTimeout;
812
+ }
813
+ init(options) {
814
+ const interceptor = async (interceptorOptions) => {
815
+ const timeoutMs = value(this.timeout, interceptorOptions);
816
+ const streamingTimeoutMs = value(this.streamingTimeout, interceptorOptions);
817
+ const hasTimeout = timeoutMs !== null && timeoutMs !== void 0;
818
+ const hasStreamingTimeout = streamingTimeoutMs !== null && streamingTimeoutMs !== void 0;
819
+ if (!hasTimeout && !hasStreamingTimeout) {
820
+ return interceptorOptions.next();
821
+ }
822
+ const controller = new AbortController();
823
+ const abortWithTimeout = (ms) => {
824
+ controller.abort(new AbortError(`Request timed out after ${ms}ms`));
825
+ };
826
+ const timeoutId = hasTimeout ? setTimeout(abortWithTimeout, timeoutMs, timeoutMs) : void 0;
827
+ const signal = anyAbortSignal([interceptorOptions.request.signal, controller.signal]);
828
+ try {
829
+ const response = await interceptorOptions.next({
830
+ ...interceptorOptions,
831
+ request: { ...interceptorOptions.request, signal }
832
+ });
833
+ if (hasStreamingTimeout) {
834
+ const body = response.body;
835
+ const isIterator = isAsyncIteratorObject(body);
836
+ if (isIterator || body instanceof ReadableStream) {
837
+ const streamingTimeoutId = setTimeout(abortWithTimeout, streamingTimeoutMs, streamingTimeoutMs);
838
+ const onFinish = () => clearTimeout(streamingTimeoutId);
839
+ return {
840
+ ...response,
841
+ body: isIterator ? override(body, wrapAsyncIterator(body, { onFinish })) : override(body, wrapReadableStream(body, { onFinish }))
842
+ };
843
+ }
844
+ }
845
+ return response;
846
+ } finally {
847
+ clearTimeout(timeoutId);
848
+ }
849
+ };
850
+ return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
851
+ }
852
+ }
853
+
854
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, PrototypePollutionProtectionHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin };