@orpc/server 2.0.0-beta.3 → 2.0.0-beta.30
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/README.md +71 -101
- package/dist/adapters/aws-lambda/index.d.mts +93 -0
- package/dist/adapters/aws-lambda/index.d.ts +93 -0
- package/dist/adapters/aws-lambda/index.mjs +66 -0
- package/dist/adapters/crossws/index.d.mts +17 -8
- package/dist/adapters/crossws/index.d.ts +17 -8
- package/dist/adapters/crossws/index.mjs +10 -7
- package/dist/adapters/fastify/index.d.mts +85 -0
- package/dist/adapters/fastify/index.d.ts +85 -0
- package/dist/adapters/fastify/index.mjs +66 -0
- package/dist/adapters/fetch/index.d.mts +16 -67
- package/dist/adapters/fetch/index.d.ts +16 -67
- package/dist/adapters/fetch/index.mjs +8 -144
- package/dist/adapters/message-port/index.d.mts +19 -16
- package/dist/adapters/message-port/index.d.ts +19 -16
- package/dist/adapters/message-port/index.mjs +28 -32
- package/dist/adapters/node/index.d.mts +15 -49
- package/dist/adapters/node/index.d.ts +15 -49
- package/dist/adapters/node/index.mjs +9 -120
- package/dist/adapters/standard/index.d.mts +6 -6
- package/dist/adapters/standard/index.d.ts +6 -6
- package/dist/adapters/standard/index.mjs +3 -3
- package/dist/adapters/standard-peer/index.d.mts +2 -2
- package/dist/adapters/standard-peer/index.d.ts +2 -2
- package/dist/adapters/websocket/index.d.mts +42 -26
- package/dist/adapters/websocket/index.d.ts +42 -26
- package/dist/adapters/websocket/index.mjs +33 -25
- package/dist/extensions/callable.d.mts +2 -2
- package/dist/extensions/callable.d.ts +2 -2
- package/dist/extensions/callable.mjs +2 -2
- package/dist/helpers/index.d.mts +23 -6
- package/dist/helpers/index.d.ts +23 -6
- package/dist/helpers/index.mjs +13 -5
- package/dist/index.d.mts +311 -16
- package/dist/index.d.ts +311 -16
- package/dist/index.mjs +126 -34
- package/dist/plugins/index.d.mts +236 -18
- package/dist/plugins/index.d.ts +236 -18
- package/dist/plugins/index.mjs +474 -15
- package/dist/shared/server.BhHrioCw.d.ts +34 -0
- package/dist/shared/{server.T9F3bzZx.d.ts → server.C2n16pp0.d.ts} +34 -9
- package/dist/shared/server.Cd4Z1hpV.mjs +69 -0
- package/dist/shared/{server.CrlKQucM.mjs → server.CkButhNT.mjs} +21 -38
- package/dist/shared/{server.B_U9y00a.d.mts → server.CkNnZ5F-.d.mts} +34 -9
- package/dist/shared/{server.BL22TloH.d.mts → server.CwrYlF72.d.mts} +31 -41
- package/dist/shared/{server.BL22TloH.d.ts → server.CwrYlF72.d.ts} +31 -41
- package/dist/shared/server.D0Ipbmdv.d.mts +34 -0
- package/dist/shared/{server.GDpX6Df8.mjs → server.D9VprDph.mjs} +82 -49
- package/dist/shared/{server.BwHnWUuN.mjs → server.Dh77P3ii.mjs} +17 -10
- package/dist/shared/{server.Pa0F03f_.d.ts → server.Dm0os-OP.d.mts} +24 -12
- package/dist/shared/{server.EOHJ3NJr.d.ts → server.DrN1Pj1-.d.ts} +3 -3
- package/dist/shared/{server.BsNNjG5J.d.mts → server.DtfwuV6U.d.ts} +24 -12
- package/dist/shared/{server.BB_Ik9Ph.d.mts → server.JbCIPL4P.d.mts} +3 -3
- package/dist/shared/{server.CjOb6ItT.mjs → server.eCTV8Vpp.mjs} +1 -1
- package/package.json +46 -12
- package/dist/shared/server.D_QauotT.mjs +0 -30
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { toArray, value } from '@orpc/shared';
|
|
2
|
-
import { flattenStandardHeader, parseStandardUrl, mergeStandardHeaders } from '@standardserver/core';
|
|
1
|
+
import { toArray, value, isNoTransformCacheControl, parseAcceptEncodingQualities, isCompressibleContentType, varyByAcceptEncoding, isAsyncIteratorObject, stringifyJSON, anyAbortSignal, override, wrapAsyncIterator, wrapReadableStream, AbortError } from '@orpc/shared';
|
|
2
|
+
import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
|
|
3
3
|
import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
|
|
5
|
+
export { R as RequestLimitHandlerPlugin } from '../shared/server.Cd4Z1hpV.mjs';
|
|
6
6
|
import '@orpc/client';
|
|
7
7
|
|
|
8
|
+
const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
|
|
8
9
|
class BatchHandlerPlugin {
|
|
9
10
|
name = "~batch";
|
|
10
11
|
/**
|
|
@@ -16,6 +17,8 @@ class BatchHandlerPlugin {
|
|
|
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 }) => ({
|
|
@@ -29,6 +32,8 @@ class BatchHandlerPlugin {
|
|
|
29
32
|
}));
|
|
30
33
|
this.successStatus = options.successStatus ?? 207;
|
|
31
34
|
this.headers = options.headers ?? {};
|
|
35
|
+
this.keepAliveEnabled = options.keepAlive?.enabled ?? true;
|
|
36
|
+
this.keepAliveInterval = options.keepAlive?.interval ?? 15e3;
|
|
32
37
|
}
|
|
33
38
|
init(options) {
|
|
34
39
|
const routingInterceptor = async (interceptorOptions) => {
|
|
@@ -59,7 +64,7 @@ class BatchHandlerPlugin {
|
|
|
59
64
|
messages = mightBeMessages;
|
|
60
65
|
} else {
|
|
61
66
|
const mightBeMessages = await interceptorOptions.request.resolveBody();
|
|
62
|
-
if (!Array.isArray(mightBeMessages)) {
|
|
67
|
+
if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
|
|
63
68
|
return {
|
|
64
69
|
matched: true,
|
|
65
70
|
response: { status: 400, headers: {}, body: "Invalid batch request body" }
|
|
@@ -73,6 +78,13 @@ class BatchHandlerPlugin {
|
|
|
73
78
|
response: { status: 400, headers: {}, body: "Invalid batch request" }
|
|
74
79
|
};
|
|
75
80
|
}
|
|
81
|
+
const outerMethod = interceptorOptions.request.method;
|
|
82
|
+
if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) {
|
|
83
|
+
return {
|
|
84
|
+
matched: true,
|
|
85
|
+
response: { status: 400, headers: {}, body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests` }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
76
88
|
const maxSize = await value(this.maxSize, interceptorOptions);
|
|
77
89
|
if (messages.length > maxSize) {
|
|
78
90
|
return {
|
|
@@ -117,7 +129,7 @@ class BatchHandlerPlugin {
|
|
|
117
129
|
response: {
|
|
118
130
|
status,
|
|
119
131
|
headers,
|
|
120
|
-
body: new Blob(chunks, { type:
|
|
132
|
+
body: new Blob(chunks, { type: BATCH_CONTENT_TYPE })
|
|
121
133
|
}
|
|
122
134
|
};
|
|
123
135
|
}
|
|
@@ -127,9 +139,35 @@ class BatchHandlerPlugin {
|
|
|
127
139
|
};
|
|
128
140
|
}
|
|
129
141
|
let streamController;
|
|
142
|
+
let keepAliveTimer;
|
|
143
|
+
const clearKeepAlive = () => {
|
|
144
|
+
if (keepAliveTimer !== void 0) {
|
|
145
|
+
clearInterval(keepAliveTimer);
|
|
146
|
+
keepAliveTimer = void 0;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const scheduleKeepAlive = () => {
|
|
150
|
+
if (!this.keepAliveEnabled) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
clearKeepAlive();
|
|
154
|
+
keepAliveTimer = setInterval(() => {
|
|
155
|
+
try {
|
|
156
|
+
const lengthBuffer = new ArrayBuffer(4);
|
|
157
|
+
new DataView(lengthBuffer).setUint32(0, 0, false);
|
|
158
|
+
streamController.enqueue(new Uint8Array(lengthBuffer));
|
|
159
|
+
} catch {
|
|
160
|
+
clearKeepAlive();
|
|
161
|
+
}
|
|
162
|
+
}, this.keepAliveInterval);
|
|
163
|
+
};
|
|
130
164
|
const stream = new ReadableStream({
|
|
131
165
|
start(controller) {
|
|
132
166
|
streamController = controller;
|
|
167
|
+
scheduleKeepAlive();
|
|
168
|
+
},
|
|
169
|
+
cancel() {
|
|
170
|
+
clearKeepAlive();
|
|
133
171
|
}
|
|
134
172
|
});
|
|
135
173
|
const peer = new ServerPeer(async (message) => {
|
|
@@ -139,17 +177,24 @@ class BatchHandlerPlugin {
|
|
|
139
177
|
new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
|
|
140
178
|
streamController.enqueue(new Uint8Array(lengthBuffer));
|
|
141
179
|
streamController.enqueue(bytes);
|
|
180
|
+
scheduleKeepAlive();
|
|
142
181
|
});
|
|
143
182
|
Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
|
|
183
|
+
clearKeepAlive();
|
|
144
184
|
streamController.close();
|
|
145
185
|
await peer.close();
|
|
146
186
|
}).catch(async (error) => {
|
|
187
|
+
clearKeepAlive();
|
|
147
188
|
streamController.error(error);
|
|
148
189
|
await peer.close(error);
|
|
149
190
|
});
|
|
150
191
|
return {
|
|
151
192
|
matched: true,
|
|
152
|
-
response: {
|
|
193
|
+
response: {
|
|
194
|
+
status,
|
|
195
|
+
headers: { ...headers, "content-type": BATCH_CONTENT_TYPE },
|
|
196
|
+
body: stream
|
|
197
|
+
}
|
|
153
198
|
};
|
|
154
199
|
};
|
|
155
200
|
return {
|
|
@@ -171,7 +216,7 @@ class CORSHandlerPlugin {
|
|
|
171
216
|
constructor(options = {}) {
|
|
172
217
|
const defaults = {
|
|
173
218
|
origin: (origin) => origin,
|
|
174
|
-
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
|
|
219
|
+
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "QUERY"]
|
|
175
220
|
};
|
|
176
221
|
this.options = {
|
|
177
222
|
...defaults,
|
|
@@ -185,20 +230,23 @@ class CORSHandlerPlugin {
|
|
|
185
230
|
return result;
|
|
186
231
|
}
|
|
187
232
|
const resHeaders = { ...result.response.headers };
|
|
188
|
-
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin)
|
|
189
|
-
const allowedOrigins = toArray(
|
|
233
|
+
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
|
|
234
|
+
const allowedOrigins = toArray(value(this.options.origin, origin, interceptorOptions));
|
|
190
235
|
if (allowedOrigins.includes("*")) {
|
|
191
236
|
resHeaders["access-control-allow-origin"] = "*";
|
|
192
237
|
} else {
|
|
193
|
-
if (allowedOrigins.includes(origin)) {
|
|
238
|
+
if (origin !== void 0 && allowedOrigins.includes(origin)) {
|
|
194
239
|
resHeaders["access-control-allow-origin"] = origin;
|
|
195
240
|
}
|
|
196
|
-
|
|
241
|
+
const existingVary = flattenStandardHeader(resHeaders.vary);
|
|
242
|
+
if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
|
|
243
|
+
resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
|
|
244
|
+
}
|
|
197
245
|
}
|
|
198
|
-
const allowedTimingOrigins = toArray(
|
|
246
|
+
const allowedTimingOrigins = toArray(value(this.options.timingOrigin, origin, interceptorOptions));
|
|
199
247
|
if (allowedTimingOrigins.includes("*")) {
|
|
200
248
|
resHeaders["timing-allow-origin"] = "*";
|
|
201
|
-
} else if (allowedTimingOrigins.includes(origin)) {
|
|
249
|
+
} else if (origin !== void 0 && allowedTimingOrigins.includes(origin)) {
|
|
202
250
|
resHeaders["timing-allow-origin"] = origin;
|
|
203
251
|
}
|
|
204
252
|
if (this.options.credentials) {
|
|
@@ -244,6 +292,147 @@ class CORSHandlerPlugin {
|
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
|
|
295
|
+
class GetMethodCsrfProtectionHandlerPlugin {
|
|
296
|
+
name = "~get-method-csrf-protection";
|
|
297
|
+
/** Judge the real request, before batch splits it into client-authored sub-requests. */
|
|
298
|
+
after = ["~batch"];
|
|
299
|
+
init(options) {
|
|
300
|
+
const routingInterceptor = (interceptorOptions) => {
|
|
301
|
+
if (this.isAllowed(interceptorOptions)) {
|
|
302
|
+
return interceptorOptions.next();
|
|
303
|
+
}
|
|
304
|
+
return Promise.resolve({
|
|
305
|
+
matched: true,
|
|
306
|
+
response: { status: 403, headers: {}, body: "Request blocked by CSRF protection." }
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
...options,
|
|
311
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
isAllowed({ request }) {
|
|
315
|
+
if (request.method !== "GET") {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
const site = flattenStandardHeader(request.headers["sec-fetch-site"])?.toLowerCase();
|
|
319
|
+
if (site === void 0 || site === "same-origin" || site === "same-site") {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const mode = flattenStandardHeader(request.headers["sec-fetch-mode"])?.toLowerCase();
|
|
323
|
+
const dest = flattenStandardHeader(request.headers["sec-fetch-dest"])?.toLowerCase();
|
|
324
|
+
if (mode === void 0 || dest === void 0) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return mode !== "navigate" || dest !== "document";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class MethodOverrideHandlerPlugin {
|
|
332
|
+
name = "~method-override";
|
|
333
|
+
/**
|
|
334
|
+
* Should override batch sub-request methods, not the original batch request.
|
|
335
|
+
*/
|
|
336
|
+
before = ["~batch"];
|
|
337
|
+
param;
|
|
338
|
+
methods;
|
|
339
|
+
constructor(options = {}) {
|
|
340
|
+
this.param = options.param ?? "method";
|
|
341
|
+
this.methods = new Set((options.methods ?? ["PUT", "PATCH", "DELETE"]).map((method) => method.toUpperCase()));
|
|
342
|
+
}
|
|
343
|
+
init(options) {
|
|
344
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
345
|
+
const { request } = interceptorOptions;
|
|
346
|
+
if (request.method !== "POST") {
|
|
347
|
+
return next();
|
|
348
|
+
}
|
|
349
|
+
const [pathname, search, hash] = parseStandardUrl(request.url);
|
|
350
|
+
const params = new URLSearchParams(search);
|
|
351
|
+
const raw = params.getAll(this.param).at(-1);
|
|
352
|
+
if (raw === void 0) {
|
|
353
|
+
return next();
|
|
354
|
+
}
|
|
355
|
+
const method = raw.toUpperCase();
|
|
356
|
+
if (!this.methods.has(method)) {
|
|
357
|
+
return next();
|
|
358
|
+
}
|
|
359
|
+
params.delete(this.param);
|
|
360
|
+
const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
|
|
361
|
+
return next({
|
|
362
|
+
...interceptorOptions,
|
|
363
|
+
request: { ...request, method, url }
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
...options,
|
|
368
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
class RequestCompressionHandlerPlugin {
|
|
374
|
+
name = "~request-compression";
|
|
375
|
+
/**
|
|
376
|
+
* Should decompress the original batch request body instead of sub-requests.
|
|
377
|
+
*/
|
|
378
|
+
after = ["~batch"];
|
|
379
|
+
init(options) {
|
|
380
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
381
|
+
const encodings = parseContentEncodings(
|
|
382
|
+
flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
|
|
383
|
+
);
|
|
384
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
385
|
+
return next();
|
|
386
|
+
}
|
|
387
|
+
const decompressedHeaders = {
|
|
388
|
+
...interceptorOptions.request.headers,
|
|
389
|
+
"content-length": void 0,
|
|
390
|
+
"content-encoding": void 0
|
|
391
|
+
};
|
|
392
|
+
return next({
|
|
393
|
+
...interceptorOptions,
|
|
394
|
+
request: {
|
|
395
|
+
...interceptorOptions.request,
|
|
396
|
+
headers: decompressedHeaders,
|
|
397
|
+
async resolveBody(hint) {
|
|
398
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
399
|
+
if (!(stream instanceof ReadableStream)) {
|
|
400
|
+
return stream;
|
|
401
|
+
}
|
|
402
|
+
let decompressedStream = stream;
|
|
403
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
404
|
+
decompressedStream = decompressedStream.pipeThrough(
|
|
405
|
+
new DecompressionStream(encodings[i])
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const response = new Response(decompressedStream, {
|
|
409
|
+
headers: toFetchHeaders(decompressedHeaders)
|
|
410
|
+
});
|
|
411
|
+
return toStandardBody(response, { hint });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
...options,
|
|
418
|
+
routingInterceptors: [
|
|
419
|
+
routingInterceptor,
|
|
420
|
+
...toArray(options.routingInterceptors)
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
|
|
426
|
+
function isSupportedEncoding(encoding) {
|
|
427
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
428
|
+
}
|
|
429
|
+
function parseContentEncodings(header) {
|
|
430
|
+
if (header === void 0) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
return header.split(",").map((part) => part.trim().toLowerCase());
|
|
434
|
+
}
|
|
435
|
+
|
|
247
436
|
class RequestHeadersHandlerPlugin {
|
|
248
437
|
name = "~request-headers";
|
|
249
438
|
init(options) {
|
|
@@ -266,6 +455,175 @@ class RequestHeadersHandlerPlugin {
|
|
|
266
455
|
}
|
|
267
456
|
}
|
|
268
457
|
|
|
458
|
+
const AVG_BYTES_PER_CHAR = 1.2;
|
|
459
|
+
class ResponseCompressionHandlerPlugin {
|
|
460
|
+
name = "~response-compression";
|
|
461
|
+
/**
|
|
462
|
+
* Compression should be done after batching, to compress the final response.
|
|
463
|
+
* Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
|
|
464
|
+
*/
|
|
465
|
+
after = ["~batch", "~response-headers"];
|
|
466
|
+
encodings;
|
|
467
|
+
threshold;
|
|
468
|
+
constructor(options = {}) {
|
|
469
|
+
this.encodings = options.encodings ?? ["gzip", "deflate"];
|
|
470
|
+
this.threshold = options.threshold ?? 1024;
|
|
471
|
+
}
|
|
472
|
+
init(options) {
|
|
473
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
474
|
+
const result = await next();
|
|
475
|
+
if (!result.matched) {
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
const response = result.response;
|
|
479
|
+
const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
|
|
480
|
+
if (contentEncoding !== void 0) {
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
if (response.status === 206 || response.headers["content-range"] !== void 0) {
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
const acceptEncodings = parseAcceptEncodingQualities(
|
|
490
|
+
flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
|
|
491
|
+
);
|
|
492
|
+
const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
|
|
493
|
+
if (encoding === void 0) {
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
const body = response.body;
|
|
497
|
+
const headers = response.headers;
|
|
498
|
+
if (body instanceof ReadableStream) {
|
|
499
|
+
const contentLength = Number(flattenStandardHeader(headers["content-length"]));
|
|
500
|
+
if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
|
|
501
|
+
return {
|
|
502
|
+
...result,
|
|
503
|
+
response: {
|
|
504
|
+
...response,
|
|
505
|
+
body: body.pipeThrough(new CompressionStream(encoding)),
|
|
506
|
+
headers: {
|
|
507
|
+
...headers,
|
|
508
|
+
"standard-server": "octet-stream",
|
|
509
|
+
"content-length": [],
|
|
510
|
+
"content-encoding": encoding,
|
|
511
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
} else if (body instanceof Blob) {
|
|
517
|
+
if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
|
|
518
|
+
const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
|
|
519
|
+
body instanceof File ? body.name : "blob"
|
|
520
|
+
);
|
|
521
|
+
return {
|
|
522
|
+
...result,
|
|
523
|
+
response: {
|
|
524
|
+
...response,
|
|
525
|
+
body: body.stream().pipeThrough(new CompressionStream(encoding)),
|
|
526
|
+
headers: {
|
|
527
|
+
...headers,
|
|
528
|
+
"standard-server": "file",
|
|
529
|
+
"content-type": body.type,
|
|
530
|
+
"content-length": [],
|
|
531
|
+
"content-disposition": contentDisposition,
|
|
532
|
+
"content-encoding": encoding,
|
|
533
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
} else if (body instanceof FormData) {
|
|
539
|
+
const PART_OVERHEAD = 64;
|
|
540
|
+
let contentLength = 0;
|
|
541
|
+
for (const [key, value] of body) {
|
|
542
|
+
contentLength += PART_OVERHEAD + key.length;
|
|
543
|
+
if (value instanceof Blob) {
|
|
544
|
+
if (!Number.isFinite(value.size)) {
|
|
545
|
+
if (!isCompressibleContentType(value.type)) {
|
|
546
|
+
contentLength = -Infinity;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
contentLength = Infinity;
|
|
550
|
+
} else {
|
|
551
|
+
contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
contentLength += value.length * AVG_BYTES_PER_CHAR;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (contentLength >= this.threshold) {
|
|
558
|
+
const res = new Response(body);
|
|
559
|
+
const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
|
|
560
|
+
return {
|
|
561
|
+
...result,
|
|
562
|
+
response: {
|
|
563
|
+
...response,
|
|
564
|
+
body: compressedStream,
|
|
565
|
+
headers: {
|
|
566
|
+
...headers,
|
|
567
|
+
"standard-server": [],
|
|
568
|
+
"content-type": res.headers.get("content-type"),
|
|
569
|
+
"content-length": [],
|
|
570
|
+
"content-encoding": encoding,
|
|
571
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
} else if (body instanceof URLSearchParams) {
|
|
577
|
+
const string = body.toString();
|
|
578
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
579
|
+
return {
|
|
580
|
+
...result,
|
|
581
|
+
response: {
|
|
582
|
+
...response,
|
|
583
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
584
|
+
headers: {
|
|
585
|
+
...headers,
|
|
586
|
+
"standard-server": [],
|
|
587
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
588
|
+
"content-length": [],
|
|
589
|
+
"content-encoding": encoding,
|
|
590
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
} else if (body !== void 0 && !isAsyncIteratorObject(body)) {
|
|
596
|
+
const string = stringifyJSON(body);
|
|
597
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
598
|
+
return {
|
|
599
|
+
...result,
|
|
600
|
+
response: {
|
|
601
|
+
...response,
|
|
602
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
603
|
+
headers: {
|
|
604
|
+
...headers,
|
|
605
|
+
"standard-server": [],
|
|
606
|
+
"content-type": "application/json",
|
|
607
|
+
"content-length": [],
|
|
608
|
+
"content-encoding": encoding,
|
|
609
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
...options,
|
|
619
|
+
routingInterceptors: [
|
|
620
|
+
routingInterceptor,
|
|
621
|
+
...toArray(options.routingInterceptors)
|
|
622
|
+
]
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
269
627
|
class ResponseHeadersHandlerPlugin {
|
|
270
628
|
name = "~response-headers";
|
|
271
629
|
/**
|
|
@@ -302,4 +660,105 @@ class ResponseHeadersHandlerPlugin {
|
|
|
302
660
|
}
|
|
303
661
|
}
|
|
304
662
|
|
|
305
|
-
|
|
663
|
+
class RethrowHandlerPlugin {
|
|
664
|
+
name = "~rethrow";
|
|
665
|
+
filter;
|
|
666
|
+
CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
|
|
667
|
+
constructor(options) {
|
|
668
|
+
this.filter = options.filter;
|
|
669
|
+
}
|
|
670
|
+
init(options) {
|
|
671
|
+
const routingInterceptor = async (options2) => {
|
|
672
|
+
const pluginContext = {};
|
|
673
|
+
const result = await options2.next({
|
|
674
|
+
...options2,
|
|
675
|
+
context: {
|
|
676
|
+
...options2.context,
|
|
677
|
+
[this.CONTEXT_SYMBOL]: pluginContext
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
if (pluginContext.error) {
|
|
681
|
+
throw pluginContext.error.value;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
};
|
|
685
|
+
const interceptor = async (options2) => {
|
|
686
|
+
const pluginContext = options2.context[this.CONTEXT_SYMBOL];
|
|
687
|
+
if (!pluginContext) {
|
|
688
|
+
throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
return await options2.next();
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (this.filter(error, options2)) {
|
|
694
|
+
pluginContext.error = { value: error };
|
|
695
|
+
}
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
return {
|
|
700
|
+
...options,
|
|
701
|
+
routingInterceptors: [
|
|
702
|
+
...toArray(options.routingInterceptors),
|
|
703
|
+
routingInterceptor
|
|
704
|
+
// rethrow as early as possible and keep it close to `interceptor` to avoid a corrupted or modified context
|
|
705
|
+
],
|
|
706
|
+
interceptors: [
|
|
707
|
+
interceptor,
|
|
708
|
+
// catch as many errors as possible
|
|
709
|
+
...toArray(options.interceptors)
|
|
710
|
+
]
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
class TimeoutHandlerPlugin {
|
|
716
|
+
timeout;
|
|
717
|
+
streamingTimeout;
|
|
718
|
+
name = "~timeout";
|
|
719
|
+
constructor(options) {
|
|
720
|
+
this.timeout = options.timeout;
|
|
721
|
+
this.streamingTimeout = options.streamingTimeout;
|
|
722
|
+
}
|
|
723
|
+
init(options) {
|
|
724
|
+
const interceptor = async (interceptorOptions) => {
|
|
725
|
+
const timeoutMs = value(this.timeout, interceptorOptions);
|
|
726
|
+
const streamingTimeoutMs = value(this.streamingTimeout, interceptorOptions);
|
|
727
|
+
const hasTimeout = timeoutMs !== null && timeoutMs !== void 0;
|
|
728
|
+
const hasStreamingTimeout = streamingTimeoutMs !== null && streamingTimeoutMs !== void 0;
|
|
729
|
+
if (!hasTimeout && !hasStreamingTimeout) {
|
|
730
|
+
return interceptorOptions.next();
|
|
731
|
+
}
|
|
732
|
+
const controller = new AbortController();
|
|
733
|
+
const abortWithTimeout = (ms) => {
|
|
734
|
+
controller.abort(new AbortError(`Request timed out after ${ms}ms`));
|
|
735
|
+
};
|
|
736
|
+
const timeoutId = hasTimeout ? setTimeout(abortWithTimeout, timeoutMs, timeoutMs) : void 0;
|
|
737
|
+
const signal = anyAbortSignal([interceptorOptions.request.signal, controller.signal]);
|
|
738
|
+
try {
|
|
739
|
+
const response = await interceptorOptions.next({
|
|
740
|
+
...interceptorOptions,
|
|
741
|
+
request: { ...interceptorOptions.request, signal }
|
|
742
|
+
});
|
|
743
|
+
if (hasStreamingTimeout) {
|
|
744
|
+
const body = response.body;
|
|
745
|
+
const isIterator = isAsyncIteratorObject(body);
|
|
746
|
+
if (isIterator || body instanceof ReadableStream) {
|
|
747
|
+
const streamingTimeoutId = setTimeout(abortWithTimeout, streamingTimeoutMs, streamingTimeoutMs);
|
|
748
|
+
const onFinish = () => clearTimeout(streamingTimeoutId);
|
|
749
|
+
return {
|
|
750
|
+
...response,
|
|
751
|
+
body: isIterator ? override(body, wrapAsyncIterator(body, { onFinish })) : override(body, wrapReadableStream(body, { onFinish }))
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return response;
|
|
756
|
+
} finally {
|
|
757
|
+
clearTimeout(timeoutId);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { C as Context } from './server.CwrYlF72.js';
|
|
2
|
+
import { S as StandardHandlerPlugin, a as StandardHandlerOptions } from './server.DrN1Pj1-.js';
|
|
3
|
+
|
|
4
|
+
interface RequestLimitHandlerPluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The maximum allowed request body size in bytes.
|
|
7
|
+
*/
|
|
8
|
+
maxBodySize: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Rejects requests whose body exceeds `maxBodySize`.
|
|
12
|
+
*
|
|
13
|
+
* When used with the request compression plugin, the limit applies to the
|
|
14
|
+
* decompressed payload rather than the compressed wire size.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link https://orpc.dev/docs/plugins/request-limit | Request Limit Plugin}
|
|
17
|
+
*/
|
|
18
|
+
declare class RequestLimitHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
19
|
+
name: string;
|
|
20
|
+
/**
|
|
21
|
+
* Should limit the original batch request body instead of sub-requests.
|
|
22
|
+
*/
|
|
23
|
+
after: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Should limit the final body size instead of the compressed one.
|
|
26
|
+
*/
|
|
27
|
+
before: string[];
|
|
28
|
+
private readonly maxBodySize;
|
|
29
|
+
constructor(options: RequestLimitHandlerPluginOptions);
|
|
30
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { RequestLimitHandlerPlugin as R };
|
|
34
|
+
export type { RequestLimitHandlerPluginOptions as a };
|