@orpc/server 2.0.0-beta.3 → 2.0.0-beta.31
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 +241 -32
- package/dist/plugins/index.d.ts +241 -32
- package/dist/plugins/index.mjs +479 -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.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.GDpX6Df8.mjs → server.DpUGXBi2.mjs} +84 -51
- 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,19 +17,28 @@ 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 }) => ({
|
|
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:
|
|
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: {
|
|
198
|
+
response: {
|
|
199
|
+
status,
|
|
200
|
+
headers: { ...headers, "content-type": BATCH_CONTENT_TYPE },
|
|
201
|
+
body: stream
|
|
202
|
+
}
|
|
153
203
|
};
|
|
154
204
|
};
|
|
155
205
|
return {
|
|
@@ -170,8 +220,8 @@ class CORSHandlerPlugin {
|
|
|
170
220
|
after = ["~opentelemetry", "~batch"];
|
|
171
221
|
constructor(options = {}) {
|
|
172
222
|
const defaults = {
|
|
173
|
-
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,20 +235,23 @@ class CORSHandlerPlugin {
|
|
|
185
235
|
return result;
|
|
186
236
|
}
|
|
187
237
|
const resHeaders = { ...result.response.headers };
|
|
188
|
-
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin)
|
|
238
|
+
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
|
|
189
239
|
const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
|
|
190
240
|
if (allowedOrigins.includes("*")) {
|
|
191
241
|
resHeaders["access-control-allow-origin"] = "*";
|
|
192
242
|
} else {
|
|
193
|
-
if (allowedOrigins.includes(origin)) {
|
|
243
|
+
if (origin !== void 0 && allowedOrigins.includes(origin)) {
|
|
194
244
|
resHeaders["access-control-allow-origin"] = origin;
|
|
195
245
|
}
|
|
196
|
-
|
|
246
|
+
const existingVary = flattenStandardHeader(resHeaders.vary);
|
|
247
|
+
if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
|
|
248
|
+
resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
|
|
249
|
+
}
|
|
197
250
|
}
|
|
198
251
|
const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
|
|
199
252
|
if (allowedTimingOrigins.includes("*")) {
|
|
200
253
|
resHeaders["timing-allow-origin"] = "*";
|
|
201
|
-
} else if (allowedTimingOrigins.includes(origin)) {
|
|
254
|
+
} else if (origin !== void 0 && allowedTimingOrigins.includes(origin)) {
|
|
202
255
|
resHeaders["timing-allow-origin"] = origin;
|
|
203
256
|
}
|
|
204
257
|
if (this.options.credentials) {
|
|
@@ -244,6 +297,147 @@ class CORSHandlerPlugin {
|
|
|
244
297
|
}
|
|
245
298
|
}
|
|
246
299
|
|
|
300
|
+
class GetMethodCsrfProtectionHandlerPlugin {
|
|
301
|
+
name = "~get-method-csrf-protection";
|
|
302
|
+
/** Judge the real request, before batch splits it into client-authored sub-requests. */
|
|
303
|
+
after = ["~batch"];
|
|
304
|
+
init(options) {
|
|
305
|
+
const routingInterceptor = (interceptorOptions) => {
|
|
306
|
+
if (this.isAllowed(interceptorOptions)) {
|
|
307
|
+
return interceptorOptions.next();
|
|
308
|
+
}
|
|
309
|
+
return Promise.resolve({
|
|
310
|
+
matched: true,
|
|
311
|
+
response: { status: 403, headers: {}, body: "Request blocked by CSRF protection." }
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
...options,
|
|
316
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
isAllowed({ request }) {
|
|
320
|
+
if (request.method !== "GET") {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const site = flattenStandardHeader(request.headers["sec-fetch-site"])?.toLowerCase();
|
|
324
|
+
if (site === void 0 || site === "same-origin" || site === "same-site") {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
const mode = flattenStandardHeader(request.headers["sec-fetch-mode"])?.toLowerCase();
|
|
328
|
+
const dest = flattenStandardHeader(request.headers["sec-fetch-dest"])?.toLowerCase();
|
|
329
|
+
if (mode === void 0 || dest === void 0) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
return mode !== "navigate" || dest !== "document";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class MethodOverrideHandlerPlugin {
|
|
337
|
+
name = "~method-override";
|
|
338
|
+
/**
|
|
339
|
+
* Should override batch sub-request methods, not the original batch request.
|
|
340
|
+
*/
|
|
341
|
+
before = ["~batch"];
|
|
342
|
+
param;
|
|
343
|
+
methods;
|
|
344
|
+
constructor(options = {}) {
|
|
345
|
+
this.param = options.param ?? "method";
|
|
346
|
+
this.methods = new Set((options.methods ?? ["PUT", "PATCH", "DELETE"]).map((method) => method.toUpperCase()));
|
|
347
|
+
}
|
|
348
|
+
init(options) {
|
|
349
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
350
|
+
const { request } = interceptorOptions;
|
|
351
|
+
if (request.method !== "POST") {
|
|
352
|
+
return next();
|
|
353
|
+
}
|
|
354
|
+
const [pathname, search, hash] = parseStandardUrl(request.url);
|
|
355
|
+
const params = new URLSearchParams(search);
|
|
356
|
+
const raw = params.getAll(this.param).at(-1);
|
|
357
|
+
if (raw === void 0) {
|
|
358
|
+
return next();
|
|
359
|
+
}
|
|
360
|
+
const method = raw.toUpperCase();
|
|
361
|
+
if (!this.methods.has(method)) {
|
|
362
|
+
return next();
|
|
363
|
+
}
|
|
364
|
+
params.delete(this.param);
|
|
365
|
+
const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
|
|
366
|
+
return next({
|
|
367
|
+
...interceptorOptions,
|
|
368
|
+
request: { ...request, method, url }
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
...options,
|
|
373
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
class RequestCompressionHandlerPlugin {
|
|
379
|
+
name = "~request-compression";
|
|
380
|
+
/**
|
|
381
|
+
* Should decompress the original batch request body instead of sub-requests.
|
|
382
|
+
*/
|
|
383
|
+
after = ["~batch"];
|
|
384
|
+
init(options) {
|
|
385
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
386
|
+
const encodings = parseContentEncodings(
|
|
387
|
+
flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
|
|
388
|
+
);
|
|
389
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
390
|
+
return next();
|
|
391
|
+
}
|
|
392
|
+
const decompressedHeaders = {
|
|
393
|
+
...interceptorOptions.request.headers,
|
|
394
|
+
"content-length": void 0,
|
|
395
|
+
"content-encoding": void 0
|
|
396
|
+
};
|
|
397
|
+
return next({
|
|
398
|
+
...interceptorOptions,
|
|
399
|
+
request: {
|
|
400
|
+
...interceptorOptions.request,
|
|
401
|
+
headers: decompressedHeaders,
|
|
402
|
+
async resolveBody(hint) {
|
|
403
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
404
|
+
if (!(stream instanceof ReadableStream)) {
|
|
405
|
+
return stream;
|
|
406
|
+
}
|
|
407
|
+
let decompressedStream = stream;
|
|
408
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
409
|
+
decompressedStream = decompressedStream.pipeThrough(
|
|
410
|
+
new DecompressionStream(encodings[i])
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const response = new Response(decompressedStream, {
|
|
414
|
+
headers: toFetchHeaders(decompressedHeaders)
|
|
415
|
+
});
|
|
416
|
+
return toStandardBody(response, { hint });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
...options,
|
|
423
|
+
routingInterceptors: [
|
|
424
|
+
routingInterceptor,
|
|
425
|
+
...toArray(options.routingInterceptors)
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
|
|
431
|
+
function isSupportedEncoding(encoding) {
|
|
432
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
433
|
+
}
|
|
434
|
+
function parseContentEncodings(header) {
|
|
435
|
+
if (header === void 0) {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
return header.split(",").map((part) => part.trim().toLowerCase());
|
|
439
|
+
}
|
|
440
|
+
|
|
247
441
|
class RequestHeadersHandlerPlugin {
|
|
248
442
|
name = "~request-headers";
|
|
249
443
|
init(options) {
|
|
@@ -266,6 +460,175 @@ class RequestHeadersHandlerPlugin {
|
|
|
266
460
|
}
|
|
267
461
|
}
|
|
268
462
|
|
|
463
|
+
const AVG_BYTES_PER_CHAR = 1.2;
|
|
464
|
+
class ResponseCompressionHandlerPlugin {
|
|
465
|
+
name = "~response-compression";
|
|
466
|
+
/**
|
|
467
|
+
* Compression should be done after batching, to compress the final response.
|
|
468
|
+
* Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
|
|
469
|
+
*/
|
|
470
|
+
after = ["~batch", "~response-headers"];
|
|
471
|
+
encodings;
|
|
472
|
+
threshold;
|
|
473
|
+
constructor(options = {}) {
|
|
474
|
+
this.encodings = options.encodings ?? ["gzip", "deflate"];
|
|
475
|
+
this.threshold = options.threshold ?? 1024;
|
|
476
|
+
}
|
|
477
|
+
init(options) {
|
|
478
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
479
|
+
const result = await next();
|
|
480
|
+
if (!result.matched) {
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
const response = result.response;
|
|
484
|
+
const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
|
|
485
|
+
if (contentEncoding !== void 0) {
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
if (response.status === 206 || response.headers["content-range"] !== void 0) {
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
const acceptEncodings = parseAcceptEncodingQualities(
|
|
495
|
+
flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
|
|
496
|
+
);
|
|
497
|
+
const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
|
|
498
|
+
if (encoding === void 0) {
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
const body = response.body;
|
|
502
|
+
const headers = response.headers;
|
|
503
|
+
if (body instanceof ReadableStream) {
|
|
504
|
+
const contentLength = Number(flattenStandardHeader(headers["content-length"]));
|
|
505
|
+
if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
|
|
506
|
+
return {
|
|
507
|
+
...result,
|
|
508
|
+
response: {
|
|
509
|
+
...response,
|
|
510
|
+
body: body.pipeThrough(new CompressionStream(encoding)),
|
|
511
|
+
headers: {
|
|
512
|
+
...headers,
|
|
513
|
+
"standard-server": "octet-stream",
|
|
514
|
+
"content-length": [],
|
|
515
|
+
"content-encoding": encoding,
|
|
516
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
} else if (body instanceof Blob) {
|
|
522
|
+
if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
|
|
523
|
+
const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
|
|
524
|
+
body instanceof File ? body.name : "blob"
|
|
525
|
+
);
|
|
526
|
+
return {
|
|
527
|
+
...result,
|
|
528
|
+
response: {
|
|
529
|
+
...response,
|
|
530
|
+
body: body.stream().pipeThrough(new CompressionStream(encoding)),
|
|
531
|
+
headers: {
|
|
532
|
+
...headers,
|
|
533
|
+
"standard-server": "file",
|
|
534
|
+
"content-type": body.type,
|
|
535
|
+
"content-length": [],
|
|
536
|
+
"content-disposition": contentDisposition,
|
|
537
|
+
"content-encoding": encoding,
|
|
538
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
} else if (body instanceof FormData) {
|
|
544
|
+
const PART_OVERHEAD = 64;
|
|
545
|
+
let contentLength = 0;
|
|
546
|
+
for (const [key, value] of body) {
|
|
547
|
+
contentLength += PART_OVERHEAD + key.length;
|
|
548
|
+
if (value instanceof Blob) {
|
|
549
|
+
if (!Number.isFinite(value.size)) {
|
|
550
|
+
if (!isCompressibleContentType(value.type)) {
|
|
551
|
+
contentLength = -Infinity;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
contentLength = Infinity;
|
|
555
|
+
} else {
|
|
556
|
+
contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
contentLength += value.length * AVG_BYTES_PER_CHAR;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (contentLength >= this.threshold) {
|
|
563
|
+
const res = new Response(body);
|
|
564
|
+
const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
|
|
565
|
+
return {
|
|
566
|
+
...result,
|
|
567
|
+
response: {
|
|
568
|
+
...response,
|
|
569
|
+
body: compressedStream,
|
|
570
|
+
headers: {
|
|
571
|
+
...headers,
|
|
572
|
+
"standard-server": [],
|
|
573
|
+
"content-type": res.headers.get("content-type"),
|
|
574
|
+
"content-length": [],
|
|
575
|
+
"content-encoding": encoding,
|
|
576
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
} else if (body instanceof URLSearchParams) {
|
|
582
|
+
const string = body.toString();
|
|
583
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
584
|
+
return {
|
|
585
|
+
...result,
|
|
586
|
+
response: {
|
|
587
|
+
...response,
|
|
588
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
589
|
+
headers: {
|
|
590
|
+
...headers,
|
|
591
|
+
"standard-server": [],
|
|
592
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
593
|
+
"content-length": [],
|
|
594
|
+
"content-encoding": encoding,
|
|
595
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
} else if (body !== void 0 && !isAsyncIteratorObject(body)) {
|
|
601
|
+
const string = stringifyJSON(body);
|
|
602
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
603
|
+
return {
|
|
604
|
+
...result,
|
|
605
|
+
response: {
|
|
606
|
+
...response,
|
|
607
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
608
|
+
headers: {
|
|
609
|
+
...headers,
|
|
610
|
+
"standard-server": [],
|
|
611
|
+
"content-type": "application/json",
|
|
612
|
+
"content-length": [],
|
|
613
|
+
"content-encoding": encoding,
|
|
614
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
...options,
|
|
624
|
+
routingInterceptors: [
|
|
625
|
+
routingInterceptor,
|
|
626
|
+
...toArray(options.routingInterceptors)
|
|
627
|
+
]
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
269
632
|
class ResponseHeadersHandlerPlugin {
|
|
270
633
|
name = "~response-headers";
|
|
271
634
|
/**
|
|
@@ -302,4 +665,105 @@ class ResponseHeadersHandlerPlugin {
|
|
|
302
665
|
}
|
|
303
666
|
}
|
|
304
667
|
|
|
305
|
-
|
|
668
|
+
class RethrowHandlerPlugin {
|
|
669
|
+
name = "~rethrow";
|
|
670
|
+
filter;
|
|
671
|
+
CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
|
|
672
|
+
constructor(options) {
|
|
673
|
+
this.filter = options.filter;
|
|
674
|
+
}
|
|
675
|
+
init(options) {
|
|
676
|
+
const routingInterceptor = async (options2) => {
|
|
677
|
+
const pluginContext = {};
|
|
678
|
+
const result = await options2.next({
|
|
679
|
+
...options2,
|
|
680
|
+
context: {
|
|
681
|
+
...options2.context,
|
|
682
|
+
[this.CONTEXT_SYMBOL]: pluginContext
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
if (pluginContext.error) {
|
|
686
|
+
throw pluginContext.error.value;
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
};
|
|
690
|
+
const interceptor = async (options2) => {
|
|
691
|
+
const pluginContext = options2.context[this.CONTEXT_SYMBOL];
|
|
692
|
+
if (!pluginContext) {
|
|
693
|
+
throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
return await options2.next();
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (this.filter(error, options2)) {
|
|
699
|
+
pluginContext.error = { value: error };
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
return {
|
|
705
|
+
...options,
|
|
706
|
+
routingInterceptors: [
|
|
707
|
+
...toArray(options.routingInterceptors),
|
|
708
|
+
routingInterceptor
|
|
709
|
+
// rethrow as early as possible and keep it close to `interceptor` to avoid a corrupted or modified context
|
|
710
|
+
],
|
|
711
|
+
interceptors: [
|
|
712
|
+
interceptor,
|
|
713
|
+
// catch as many errors as possible
|
|
714
|
+
...toArray(options.interceptors)
|
|
715
|
+
]
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
class TimeoutHandlerPlugin {
|
|
721
|
+
timeout;
|
|
722
|
+
streamingTimeout;
|
|
723
|
+
name = "~timeout";
|
|
724
|
+
constructor(options) {
|
|
725
|
+
this.timeout = options.timeout;
|
|
726
|
+
this.streamingTimeout = options.streamingTimeout;
|
|
727
|
+
}
|
|
728
|
+
init(options) {
|
|
729
|
+
const interceptor = async (interceptorOptions) => {
|
|
730
|
+
const timeoutMs = value(this.timeout, interceptorOptions);
|
|
731
|
+
const streamingTimeoutMs = value(this.streamingTimeout, interceptorOptions);
|
|
732
|
+
const hasTimeout = timeoutMs !== null && timeoutMs !== void 0;
|
|
733
|
+
const hasStreamingTimeout = streamingTimeoutMs !== null && streamingTimeoutMs !== void 0;
|
|
734
|
+
if (!hasTimeout && !hasStreamingTimeout) {
|
|
735
|
+
return interceptorOptions.next();
|
|
736
|
+
}
|
|
737
|
+
const controller = new AbortController();
|
|
738
|
+
const abortWithTimeout = (ms) => {
|
|
739
|
+
controller.abort(new AbortError(`Request timed out after ${ms}ms`));
|
|
740
|
+
};
|
|
741
|
+
const timeoutId = hasTimeout ? setTimeout(abortWithTimeout, timeoutMs, timeoutMs) : void 0;
|
|
742
|
+
const signal = anyAbortSignal([interceptorOptions.request.signal, controller.signal]);
|
|
743
|
+
try {
|
|
744
|
+
const response = await interceptorOptions.next({
|
|
745
|
+
...interceptorOptions,
|
|
746
|
+
request: { ...interceptorOptions.request, signal }
|
|
747
|
+
});
|
|
748
|
+
if (hasStreamingTimeout) {
|
|
749
|
+
const body = response.body;
|
|
750
|
+
const isIterator = isAsyncIteratorObject(body);
|
|
751
|
+
if (isIterator || body instanceof ReadableStream) {
|
|
752
|
+
const streamingTimeoutId = setTimeout(abortWithTimeout, streamingTimeoutMs, streamingTimeoutMs);
|
|
753
|
+
const onFinish = () => clearTimeout(streamingTimeoutId);
|
|
754
|
+
return {
|
|
755
|
+
...response,
|
|
756
|
+
body: isIterator ? override(body, wrapAsyncIterator(body, { onFinish })) : override(body, wrapReadableStream(body, { onFinish }))
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return response;
|
|
761
|
+
} finally {
|
|
762
|
+
clearTimeout(timeoutId);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
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 };
|