@orpc/server 1.14.9 → 1.14.11
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 +52 -98
- package/dist/adapters/crossws/index.d.mts +45 -21
- package/dist/adapters/crossws/index.d.ts +45 -21
- package/dist/adapters/crossws/index.mjs +40 -18
- package/dist/adapters/fetch/index.d.mts +64 -96
- package/dist/adapters/fetch/index.d.ts +64 -96
- package/dist/adapters/fetch/index.mjs +34 -139
- package/dist/adapters/message-port/index.d.mts +49 -34
- package/dist/adapters/message-port/index.d.ts +49 -34
- package/dist/adapters/message-port/index.mjs +68 -37
- package/dist/adapters/node/index.d.mts +63 -71
- package/dist/adapters/node/index.d.ts +63 -71
- package/dist/adapters/node/index.mjs +36 -113
- package/dist/adapters/standard/index.d.mts +16 -18
- package/dist/adapters/standard/index.d.ts +16 -18
- package/dist/adapters/standard/index.mjs +5 -5
- package/dist/adapters/standard-peer/index.d.mts +12 -14
- package/dist/adapters/standard-peer/index.d.ts +12 -14
- package/dist/adapters/standard-peer/index.mjs +2 -21
- package/dist/adapters/websocket/index.d.mts +53 -38
- package/dist/adapters/websocket/index.d.ts +53 -38
- package/dist/adapters/websocket/index.mjs +59 -42
- package/dist/extensions/callable.d.mts +10 -0
- package/dist/extensions/callable.d.ts +10 -0
- package/dist/extensions/callable.mjs +11 -0
- package/dist/helpers/index.d.mts +4 -4
- package/dist/helpers/index.d.ts +4 -4
- package/dist/helpers/index.mjs +13 -5
- package/dist/index.d.mts +166 -770
- package/dist/index.d.ts +166 -770
- package/dist/index.mjs +306 -399
- package/dist/plugins/index.d.mts +214 -121
- package/dist/plugins/index.d.ts +214 -121
- package/dist/plugins/index.mjs +576 -214
- package/dist/shared/server.15O7oC1p.d.mts +61 -0
- package/dist/shared/server.B4BnEWWm.d.mts +67 -0
- package/dist/shared/server.BhsVw7m7.d.ts +104 -0
- package/dist/shared/server.BoiFpNT7.mjs +224 -0
- package/dist/shared/server.COPVXhDG.mjs +271 -0
- package/dist/shared/server.CX4vUnDk.mjs +11 -0
- package/dist/shared/server.CcR4kgje.d.mts +206 -0
- package/dist/shared/server.CcR4kgje.d.ts +206 -0
- package/dist/shared/server.CnN50DHH.d.ts +67 -0
- package/dist/shared/server.DBs0rrf9.mjs +41 -0
- package/dist/shared/server.DDFizwfU.d.mts +104 -0
- package/dist/shared/server.D_QauotT.mjs +30 -0
- package/dist/shared/server.DofEOwf-.mjs +237 -0
- package/dist/shared/{server.DZ5BIITo.mjs → server.W91HSRkE.mjs} +2 -2
- package/dist/shared/server.m3szVJGU.d.ts +61 -0
- package/package.json +26 -56
- package/dist/adapters/aws-lambda/index.d.mts +0 -46
- package/dist/adapters/aws-lambda/index.d.ts +0 -46
- package/dist/adapters/aws-lambda/index.mjs +0 -40
- package/dist/adapters/bun-ws/index.d.mts +0 -36
- package/dist/adapters/bun-ws/index.d.ts +0 -36
- package/dist/adapters/bun-ws/index.mjs +0 -47
- package/dist/adapters/fastify/index.d.mts +0 -53
- package/dist/adapters/fastify/index.d.ts +0 -53
- package/dist/adapters/fastify/index.mjs +0 -52
- package/dist/adapters/ws/index.d.mts +0 -31
- package/dist/adapters/ws/index.d.ts +0 -31
- package/dist/adapters/ws/index.mjs +0 -41
- package/dist/hibernation/index.d.mts +0 -44
- package/dist/hibernation/index.d.ts +0 -44
- package/dist/hibernation/index.mjs +0 -65
- package/dist/shared/server.7cEtMB30.d.ts +0 -74
- package/dist/shared/server.B8gYOD5g.d.mts +0 -12
- package/dist/shared/server.BqadksTP.d.mts +0 -74
- package/dist/shared/server.C8_sRzQB.d.mts +0 -42
- package/dist/shared/server.ChUyt5-i.d.mts +0 -32
- package/dist/shared/server.ChyoA9XY.d.ts +0 -42
- package/dist/shared/server.DEBcqOjg.mjs +0 -418
- package/dist/shared/server.EfTOZ2Q7.d.ts +0 -12
- package/dist/shared/server.TEVCLCFC.mjs +0 -39
- package/dist/shared/server.ZxHCEN1h.mjs +0 -226
- package/dist/shared/server.qKsRrdxW.d.mts +0 -193
- package/dist/shared/server.qKsRrdxW.d.ts +0 -193
- package/dist/shared/server.yoEB3Fx4.d.ts +0 -32
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,124 +1,206 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { toArray, value, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON } from '@orpc/shared';
|
|
2
|
+
import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, 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, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
|
|
5
6
|
import { ORPCError } from '@orpc/client';
|
|
6
|
-
export { S as StrictGetMethodPlugin } from '../shared/server.TEVCLCFC.mjs';
|
|
7
|
-
import '@orpc/contract';
|
|
8
7
|
|
|
9
8
|
class BatchHandlerPlugin {
|
|
9
|
+
name = "~batch";
|
|
10
|
+
/**
|
|
11
|
+
* Run batch interceptors before OpenTelemetry interceptors
|
|
12
|
+
* so each subrequest gets its own span instead of sharing one batch-level span.
|
|
13
|
+
*/
|
|
14
|
+
after = ["~opentelemetry"];
|
|
10
15
|
maxSize;
|
|
11
|
-
|
|
16
|
+
mapSubrequest;
|
|
12
17
|
successStatus;
|
|
13
18
|
headers;
|
|
14
|
-
|
|
19
|
+
keepAliveEnabled;
|
|
20
|
+
keepAliveInterval;
|
|
15
21
|
constructor(options = {}) {
|
|
16
22
|
this.maxSize = options.maxSize ?? 10;
|
|
17
|
-
this.
|
|
18
|
-
...
|
|
23
|
+
this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
|
|
24
|
+
...subRequest,
|
|
19
25
|
headers: {
|
|
20
26
|
...batchRequest.headers,
|
|
21
|
-
...
|
|
27
|
+
...subRequest.headers,
|
|
28
|
+
"orpc-batch": void 0
|
|
29
|
+
// useful in case batch plugin is used multiple times
|
|
22
30
|
}
|
|
23
31
|
}));
|
|
24
32
|
this.successStatus = options.successStatus ?? 207;
|
|
25
33
|
this.headers = options.headers ?? {};
|
|
34
|
+
this.keepAliveEnabled = options.keepAlive?.enabled ?? true;
|
|
35
|
+
this.keepAliveInterval = options.keepAlive?.interval ?? 15e3;
|
|
26
36
|
}
|
|
27
37
|
init(options) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return options2.next();
|
|
38
|
+
const routingInterceptor = async (interceptorOptions) => {
|
|
39
|
+
const batchHeader = flattenStandardHeader(interceptorOptions.request.headers["orpc-batch"]);
|
|
40
|
+
if (batchHeader === void 0) {
|
|
41
|
+
return interceptorOptions.next();
|
|
33
42
|
}
|
|
34
|
-
|
|
43
|
+
const mode = batchHeader === "buffered" ? "buffered" : "streaming";
|
|
44
|
+
let messages;
|
|
35
45
|
try {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
span?.setAttribute("batch.mode", mode);
|
|
42
|
-
span?.setAttribute("batch.size", parsed.length);
|
|
43
|
-
const maxSize = await value(this.maxSize, options2);
|
|
44
|
-
if (parsed.length > maxSize) {
|
|
45
|
-
const message = "Batch request size exceeds the maximum allowed size";
|
|
46
|
-
setSpanError(span, message);
|
|
46
|
+
if (interceptorOptions.request.method === "GET") {
|
|
47
|
+
const [, search] = parseStandardUrl(interceptorOptions.request.url);
|
|
48
|
+
const params = new URLSearchParams(search);
|
|
49
|
+
const data = params.getAll("data").at(-1);
|
|
50
|
+
if (!data) {
|
|
47
51
|
return {
|
|
48
52
|
matched: true,
|
|
49
|
-
response: {
|
|
50
|
-
status: 413,
|
|
51
|
-
headers: {},
|
|
52
|
-
body: message
|
|
53
|
-
}
|
|
53
|
+
response: { status: 400, headers: {}, body: "Missing data parameter for batch request" }
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
56
|
+
const mightBeMessages = JSON.parse(data);
|
|
57
|
+
if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
|
|
58
|
+
return {
|
|
59
|
+
matched: true,
|
|
60
|
+
response: { status: 400, headers: {}, body: "Invalid batch request data parameter" }
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
messages = mightBeMessages;
|
|
64
|
+
} else {
|
|
65
|
+
const mightBeMessages = await interceptorOptions.request.resolveBody();
|
|
66
|
+
if (!Array.isArray(mightBeMessages)) {
|
|
67
|
+
return {
|
|
68
|
+
matched: true,
|
|
69
|
+
response: { status: 400, headers: {}, body: "Invalid batch request body" }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
messages = mightBeMessages;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return {
|
|
76
|
+
matched: true,
|
|
77
|
+
response: { status: 400, headers: {}, body: "Invalid batch request" }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const maxSize = await value(this.maxSize, interceptorOptions);
|
|
81
|
+
if (messages.length > maxSize) {
|
|
82
|
+
return {
|
|
83
|
+
matched: true,
|
|
84
|
+
response: { status: 413, headers: {}, body: "Batch request size exceeds the maximum allowed size" }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const handleIndividualRequest = async (request) => {
|
|
88
|
+
try {
|
|
89
|
+
request = this.mapSubrequest(request, interceptorOptions);
|
|
90
|
+
const { matched, response } = await interceptorOptions.next({ ...interceptorOptions, request });
|
|
91
|
+
if (!matched) {
|
|
92
|
+
return { status: 404, headers: {}, body: "No procedure matched" };
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
Promise.reject(err);
|
|
97
|
+
return { status: 500, headers: {}, body: "Internal server error" };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const status = await value(this.successStatus, interceptorOptions);
|
|
101
|
+
const headers = await value(this.headers, interceptorOptions);
|
|
102
|
+
if (mode === "buffered") {
|
|
103
|
+
const responseMessages = [];
|
|
104
|
+
const peer2 = new ServerPeer(async (message) => {
|
|
105
|
+
responseMessages.push(message);
|
|
105
106
|
});
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
await Promise.all(messages.map((msg) => peer2.message(msg, handleIndividualRequest)));
|
|
108
|
+
await peer2.close();
|
|
109
|
+
if (responseMessages.some((msg) => msg.binary !== void 0)) {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
for (const message of responseMessages) {
|
|
112
|
+
const encoded = await encodePeerMessage(message);
|
|
113
|
+
const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
|
|
114
|
+
const lengthBuffer = new ArrayBuffer(4);
|
|
115
|
+
new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
|
|
116
|
+
chunks.push(new Uint8Array(lengthBuffer));
|
|
117
|
+
chunks.push(bytes);
|
|
118
|
+
}
|
|
108
119
|
return {
|
|
109
120
|
matched: true,
|
|
110
|
-
response: {
|
|
121
|
+
response: {
|
|
122
|
+
status,
|
|
123
|
+
headers,
|
|
124
|
+
body: new Blob(chunks, { type: "application/octet-stream" })
|
|
125
|
+
}
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
|
-
|
|
128
|
+
return {
|
|
129
|
+
matched: true,
|
|
130
|
+
response: { status, headers, body: responseMessages }
|
|
131
|
+
};
|
|
114
132
|
}
|
|
115
|
-
|
|
133
|
+
let streamController;
|
|
134
|
+
let keepAliveTimer;
|
|
135
|
+
const clearKeepAlive = () => {
|
|
136
|
+
if (keepAliveTimer !== void 0) {
|
|
137
|
+
clearInterval(keepAliveTimer);
|
|
138
|
+
keepAliveTimer = void 0;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const scheduleKeepAlive = () => {
|
|
142
|
+
if (!this.keepAliveEnabled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
clearKeepAlive();
|
|
146
|
+
keepAliveTimer = setInterval(() => {
|
|
147
|
+
try {
|
|
148
|
+
const lengthBuffer = new ArrayBuffer(4);
|
|
149
|
+
new DataView(lengthBuffer).setUint32(0, 0, false);
|
|
150
|
+
streamController.enqueue(new Uint8Array(lengthBuffer));
|
|
151
|
+
} catch {
|
|
152
|
+
clearKeepAlive();
|
|
153
|
+
}
|
|
154
|
+
}, this.keepAliveInterval);
|
|
155
|
+
};
|
|
156
|
+
const stream = new ReadableStream({
|
|
157
|
+
start(controller) {
|
|
158
|
+
streamController = controller;
|
|
159
|
+
scheduleKeepAlive();
|
|
160
|
+
},
|
|
161
|
+
cancel() {
|
|
162
|
+
clearKeepAlive();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
const peer = new ServerPeer(async (message) => {
|
|
166
|
+
const encoded = await encodePeerMessage(message);
|
|
167
|
+
const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
|
|
168
|
+
const lengthBuffer = new ArrayBuffer(4);
|
|
169
|
+
new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
|
|
170
|
+
streamController.enqueue(new Uint8Array(lengthBuffer));
|
|
171
|
+
streamController.enqueue(bytes);
|
|
172
|
+
scheduleKeepAlive();
|
|
173
|
+
});
|
|
174
|
+
Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
|
|
175
|
+
clearKeepAlive();
|
|
176
|
+
streamController.close();
|
|
177
|
+
await peer.close();
|
|
178
|
+
}).catch(async (error) => {
|
|
179
|
+
clearKeepAlive();
|
|
180
|
+
streamController.error(error);
|
|
181
|
+
await peer.close(error);
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
matched: true,
|
|
185
|
+
response: { status, headers, body: stream }
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
...options,
|
|
190
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
191
|
+
};
|
|
116
192
|
}
|
|
117
193
|
}
|
|
118
194
|
|
|
119
|
-
class
|
|
195
|
+
class CORSHandlerPlugin {
|
|
120
196
|
options;
|
|
121
|
-
|
|
197
|
+
name = "~cors";
|
|
198
|
+
/**
|
|
199
|
+
* - Do not create spans for CORS preflight requests.
|
|
200
|
+
* - Run CORS interceptors before batch interceptors so headers are applied to
|
|
201
|
+
* the actual response rather than sub-responses.
|
|
202
|
+
*/
|
|
203
|
+
after = ["~opentelemetry", "~batch"];
|
|
122
204
|
constructor(options = {}) {
|
|
123
205
|
const defaults = {
|
|
124
206
|
origin: (origin) => origin,
|
|
@@ -130,19 +212,51 @@ class CORSPlugin {
|
|
|
130
212
|
};
|
|
131
213
|
}
|
|
132
214
|
init(options) {
|
|
133
|
-
|
|
134
|
-
|
|
215
|
+
const corsHeadersInterceptor = async (interceptorOptions) => {
|
|
216
|
+
const result = await interceptorOptions.next();
|
|
217
|
+
if (!result.matched) {
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
const resHeaders = { ...result.response.headers };
|
|
221
|
+
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
|
|
222
|
+
const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
|
|
223
|
+
if (allowedOrigins.includes("*")) {
|
|
224
|
+
resHeaders["access-control-allow-origin"] = "*";
|
|
225
|
+
} else {
|
|
226
|
+
if (allowedOrigins.includes(origin)) {
|
|
227
|
+
resHeaders["access-control-allow-origin"] = origin;
|
|
228
|
+
}
|
|
229
|
+
const existingVary = flattenStandardHeader(resHeaders.vary);
|
|
230
|
+
if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
|
|
231
|
+
resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
|
|
235
|
+
if (allowedTimingOrigins.includes("*")) {
|
|
236
|
+
resHeaders["timing-allow-origin"] = "*";
|
|
237
|
+
} else if (allowedTimingOrigins.includes(origin)) {
|
|
238
|
+
resHeaders["timing-allow-origin"] = origin;
|
|
239
|
+
}
|
|
240
|
+
if (this.options.credentials) {
|
|
241
|
+
resHeaders["access-control-allow-credentials"] = "true";
|
|
242
|
+
}
|
|
243
|
+
if (this.options.exposeHeaders?.length) {
|
|
244
|
+
resHeaders["access-control-expose-headers"] = flattenStandardHeader(this.options.exposeHeaders);
|
|
245
|
+
}
|
|
246
|
+
return { ...result, response: { ...result.response, headers: resHeaders } };
|
|
247
|
+
};
|
|
248
|
+
const preflightInterceptor = async (interceptorOptions) => {
|
|
135
249
|
if (interceptorOptions.request.method === "OPTIONS") {
|
|
136
250
|
const resHeaders = {};
|
|
137
251
|
if (this.options.maxAge !== void 0) {
|
|
138
252
|
resHeaders["access-control-max-age"] = this.options.maxAge.toString();
|
|
139
253
|
}
|
|
140
254
|
if (this.options.allowMethods?.length) {
|
|
141
|
-
resHeaders["access-control-allow-methods"] =
|
|
255
|
+
resHeaders["access-control-allow-methods"] = flattenStandardHeader(this.options.allowMethods);
|
|
142
256
|
}
|
|
143
257
|
const allowHeaders = this.options.allowHeaders ?? interceptorOptions.request.headers["access-control-request-headers"];
|
|
144
258
|
if (typeof allowHeaders === "string" || allowHeaders?.length) {
|
|
145
|
-
resHeaders["access-control-allow-headers"] =
|
|
259
|
+
resHeaders["access-control-allow-headers"] = flattenStandardHeader(allowHeaders);
|
|
146
260
|
}
|
|
147
261
|
return {
|
|
148
262
|
matched: true,
|
|
@@ -154,106 +268,385 @@ class CORSPlugin {
|
|
|
154
268
|
};
|
|
155
269
|
}
|
|
156
270
|
return interceptorOptions.next();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (allowedOriginArr.includes("*")) {
|
|
167
|
-
result.response.headers["access-control-allow-origin"] = "*";
|
|
168
|
-
} else {
|
|
169
|
-
if (allowedOriginArr.includes(origin)) {
|
|
170
|
-
result.response.headers["access-control-allow-origin"] = origin;
|
|
171
|
-
}
|
|
172
|
-
const existingVary = flattenHeader(result.response.headers.vary);
|
|
173
|
-
if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
|
|
174
|
-
result.response.headers.vary = existingVary ? `${existingVary}, Origin` : "Origin";
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const allowedTimingOrigin = await value(this.options.timingOrigin, origin, interceptorOptions);
|
|
178
|
-
const allowedTimingOriginArr = Array.isArray(allowedTimingOrigin) ? allowedTimingOrigin : [allowedTimingOrigin];
|
|
179
|
-
if (allowedTimingOriginArr.includes("*")) {
|
|
180
|
-
result.response.headers["timing-allow-origin"] = "*";
|
|
181
|
-
} else if (allowedTimingOriginArr.includes(origin)) {
|
|
182
|
-
result.response.headers["timing-allow-origin"] = origin;
|
|
183
|
-
}
|
|
184
|
-
if (this.options.credentials) {
|
|
185
|
-
result.response.headers["access-control-allow-credentials"] = "true";
|
|
186
|
-
}
|
|
187
|
-
if (this.options.exposeHeaders?.length) {
|
|
188
|
-
result.response.headers["access-control-expose-headers"] = flattenHeader(this.options.exposeHeaders);
|
|
189
|
-
}
|
|
190
|
-
return result;
|
|
191
|
-
});
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
...options,
|
|
274
|
+
routingInterceptors: [
|
|
275
|
+
corsHeadersInterceptor,
|
|
276
|
+
preflightInterceptor,
|
|
277
|
+
...toArray(options.routingInterceptors)
|
|
278
|
+
]
|
|
279
|
+
};
|
|
192
280
|
}
|
|
193
281
|
}
|
|
194
282
|
|
|
195
|
-
class
|
|
283
|
+
class RequestCompressionHandlerPlugin {
|
|
284
|
+
name = "~request-compression";
|
|
285
|
+
/**
|
|
286
|
+
* Should decompress the original batch request body instead of sub-requests.
|
|
287
|
+
*/
|
|
288
|
+
after = ["~batch"];
|
|
196
289
|
init(options) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
291
|
+
const encodings = parseContentEncodings(
|
|
292
|
+
flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
|
|
293
|
+
);
|
|
294
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
295
|
+
return next();
|
|
296
|
+
}
|
|
297
|
+
const decompressedHeaders = {
|
|
298
|
+
...interceptorOptions.request.headers,
|
|
299
|
+
"content-length": void 0,
|
|
300
|
+
"content-encoding": void 0
|
|
301
|
+
};
|
|
302
|
+
return next({
|
|
201
303
|
...interceptorOptions,
|
|
202
|
-
|
|
203
|
-
...interceptorOptions.
|
|
204
|
-
|
|
304
|
+
request: {
|
|
305
|
+
...interceptorOptions.request,
|
|
306
|
+
headers: decompressedHeaders,
|
|
307
|
+
async resolveBody(hint) {
|
|
308
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
309
|
+
if (!(stream instanceof ReadableStream)) {
|
|
310
|
+
return stream;
|
|
311
|
+
}
|
|
312
|
+
let decompressedStream = stream;
|
|
313
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
314
|
+
decompressedStream = decompressedStream.pipeThrough(
|
|
315
|
+
new DecompressionStream(encodings[i])
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const response = new Response(decompressedStream, {
|
|
319
|
+
headers: toFetchHeaders(decompressedHeaders)
|
|
320
|
+
});
|
|
321
|
+
return toStandardBody(response, { hint });
|
|
322
|
+
}
|
|
205
323
|
}
|
|
206
324
|
});
|
|
207
|
-
}
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
...options,
|
|
328
|
+
routingInterceptors: [
|
|
329
|
+
routingInterceptor,
|
|
330
|
+
...toArray(options.routingInterceptors)
|
|
331
|
+
]
|
|
332
|
+
};
|
|
208
333
|
}
|
|
209
334
|
}
|
|
335
|
+
const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
|
|
336
|
+
function isSupportedEncoding(encoding) {
|
|
337
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
338
|
+
}
|
|
339
|
+
function parseContentEncodings(header) {
|
|
340
|
+
if (header === void 0) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
return header.split(",").map((part) => part.trim().toLowerCase());
|
|
344
|
+
}
|
|
210
345
|
|
|
211
|
-
class
|
|
346
|
+
class RequestHeadersHandlerPlugin {
|
|
347
|
+
name = "~request-headers";
|
|
212
348
|
init(options) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
349
|
+
return {
|
|
350
|
+
...options,
|
|
351
|
+
interceptors: [
|
|
352
|
+
(interceptorOptions) => {
|
|
353
|
+
const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
|
|
354
|
+
return interceptorOptions.next({
|
|
355
|
+
...interceptorOptions,
|
|
356
|
+
context: {
|
|
357
|
+
...interceptorOptions.context,
|
|
358
|
+
reqHeaders
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
...toArray(options.interceptors)
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
class RequestLimitHandlerPlugin {
|
|
369
|
+
name = "~request-limit";
|
|
370
|
+
/**
|
|
371
|
+
* Should limit the original batch request body instead of sub-requests.
|
|
372
|
+
*/
|
|
373
|
+
after = ["~batch"];
|
|
374
|
+
/**
|
|
375
|
+
* Should limit the final body size instead of the compressed one.
|
|
376
|
+
*/
|
|
377
|
+
before = ["~request-compression"];
|
|
378
|
+
maxBodySize;
|
|
379
|
+
constructor(options) {
|
|
380
|
+
this.maxBodySize = options.maxBodySize;
|
|
381
|
+
}
|
|
382
|
+
init(options) {
|
|
383
|
+
const maxBodySize = this.maxBodySize;
|
|
384
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
385
|
+
return next({
|
|
217
386
|
...interceptorOptions,
|
|
218
|
-
|
|
219
|
-
...interceptorOptions.
|
|
220
|
-
|
|
387
|
+
request: {
|
|
388
|
+
...interceptorOptions.request,
|
|
389
|
+
async resolveBody(hint) {
|
|
390
|
+
const contentLength = Number(
|
|
391
|
+
flattenStandardHeader(interceptorOptions.request.headers["content-length"])
|
|
392
|
+
);
|
|
393
|
+
if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
|
|
394
|
+
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
395
|
+
}
|
|
396
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
397
|
+
if (!(stream instanceof ReadableStream)) {
|
|
398
|
+
return stream;
|
|
399
|
+
}
|
|
400
|
+
let currentBodySize = 0;
|
|
401
|
+
const limitedStream = stream.pipeThrough(
|
|
402
|
+
new TransformStream({
|
|
403
|
+
transform(chunk, controller) {
|
|
404
|
+
currentBodySize += chunk.byteLength;
|
|
405
|
+
if (currentBodySize > maxBodySize) {
|
|
406
|
+
controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
controller.enqueue(chunk);
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
const response = new Response(limitedStream, {
|
|
414
|
+
headers: toFetchHeaders(interceptorOptions.request.headers)
|
|
415
|
+
});
|
|
416
|
+
return toStandardBody(response, { hint });
|
|
417
|
+
}
|
|
221
418
|
}
|
|
222
419
|
});
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
...options,
|
|
423
|
+
routingInterceptors: [
|
|
424
|
+
routingInterceptor,
|
|
425
|
+
...toArray(options.routingInterceptors)
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const AVG_BYTES_PER_CHAR = 1.2;
|
|
432
|
+
class ResponseCompressionHandlerPlugin {
|
|
433
|
+
name = "~response-compression";
|
|
434
|
+
/**
|
|
435
|
+
* Compression should be done after batching, to compress the final response.
|
|
436
|
+
* Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
|
|
437
|
+
*/
|
|
438
|
+
after = ["~batch", "~response-headers"];
|
|
439
|
+
encodings;
|
|
440
|
+
threshold;
|
|
441
|
+
constructor(options = {}) {
|
|
442
|
+
this.encodings = options.encodings ?? ["gzip", "deflate"];
|
|
443
|
+
this.threshold = options.threshold ?? 1024;
|
|
444
|
+
}
|
|
445
|
+
init(options) {
|
|
446
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
447
|
+
const result = await next();
|
|
223
448
|
if (!result.matched) {
|
|
224
449
|
return result;
|
|
225
450
|
}
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} else if (responseHeaders[key] !== void 0) {
|
|
231
|
-
responseHeaders[key] = [responseHeaders[key], value];
|
|
232
|
-
} else {
|
|
233
|
-
responseHeaders[key] = value;
|
|
234
|
-
}
|
|
451
|
+
const response = result.response;
|
|
452
|
+
const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
|
|
453
|
+
if (contentEncoding !== void 0) {
|
|
454
|
+
return result;
|
|
235
455
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
456
|
+
if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
const acceptEncodings = parseAcceptEncodings(
|
|
460
|
+
flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
|
|
461
|
+
);
|
|
462
|
+
const encoding = this.encodings.find((enc) => acceptEncodings.includes(enc));
|
|
463
|
+
if (encoding === void 0) {
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
const body = response.body;
|
|
467
|
+
const headers = response.headers;
|
|
468
|
+
if (body instanceof ReadableStream) {
|
|
469
|
+
const contentLength = Number(flattenStandardHeader(headers["content-length"]));
|
|
470
|
+
if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
|
|
471
|
+
return {
|
|
472
|
+
...result,
|
|
473
|
+
response: {
|
|
474
|
+
...response,
|
|
475
|
+
body: body.pipeThrough(new CompressionStream(encoding)),
|
|
476
|
+
headers: {
|
|
477
|
+
...headers,
|
|
478
|
+
"standard-server": "octet-stream",
|
|
479
|
+
"content-length": [],
|
|
480
|
+
"content-encoding": encoding
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
241
484
|
}
|
|
242
|
-
}
|
|
243
|
-
|
|
485
|
+
} else if (body instanceof Blob) {
|
|
486
|
+
if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
|
|
487
|
+
const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
|
|
488
|
+
body instanceof File ? body.name : "blob"
|
|
489
|
+
);
|
|
490
|
+
return {
|
|
491
|
+
...result,
|
|
492
|
+
response: {
|
|
493
|
+
...response,
|
|
494
|
+
body: body.stream().pipeThrough(new CompressionStream(encoding)),
|
|
495
|
+
headers: {
|
|
496
|
+
...headers,
|
|
497
|
+
"standard-server": "file",
|
|
498
|
+
"content-type": body.type,
|
|
499
|
+
"content-length": [],
|
|
500
|
+
"content-disposition": contentDisposition,
|
|
501
|
+
"content-encoding": encoding
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
} else if (body instanceof FormData) {
|
|
507
|
+
const PART_OVERHEAD = 64;
|
|
508
|
+
let contentLength = 0;
|
|
509
|
+
for (const [key, value] of body) {
|
|
510
|
+
contentLength += PART_OVERHEAD + key.length;
|
|
511
|
+
if (value instanceof Blob) {
|
|
512
|
+
if (!Number.isFinite(value.size)) {
|
|
513
|
+
if (!isCompressibleContentType(value.type)) {
|
|
514
|
+
contentLength = -Infinity;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
contentLength = Infinity;
|
|
518
|
+
} else {
|
|
519
|
+
contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
contentLength += value.length * AVG_BYTES_PER_CHAR;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (contentLength >= this.threshold) {
|
|
526
|
+
const res = new Response(body);
|
|
527
|
+
const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
|
|
528
|
+
return {
|
|
529
|
+
...result,
|
|
530
|
+
response: {
|
|
531
|
+
...response,
|
|
532
|
+
body: compressedStream,
|
|
533
|
+
headers: {
|
|
534
|
+
...headers,
|
|
535
|
+
"standard-server": [],
|
|
536
|
+
"content-type": res.headers.get("content-type"),
|
|
537
|
+
"content-length": [],
|
|
538
|
+
"content-encoding": encoding
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
} else if (body instanceof URLSearchParams) {
|
|
544
|
+
const string = body.toString();
|
|
545
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
546
|
+
return {
|
|
547
|
+
...result,
|
|
548
|
+
response: {
|
|
549
|
+
...response,
|
|
550
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
551
|
+
headers: {
|
|
552
|
+
...headers,
|
|
553
|
+
"standard-server": [],
|
|
554
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
555
|
+
"content-length": [],
|
|
556
|
+
"content-encoding": encoding
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
} else if (body !== void 0 && !isAsyncIteratorObject(body)) {
|
|
562
|
+
const string = stringifyJSON(body);
|
|
563
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
564
|
+
return {
|
|
565
|
+
...result,
|
|
566
|
+
response: {
|
|
567
|
+
...response,
|
|
568
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
569
|
+
headers: {
|
|
570
|
+
...headers,
|
|
571
|
+
"standard-server": [],
|
|
572
|
+
"content-type": "application/json",
|
|
573
|
+
"content-length": [],
|
|
574
|
+
"content-encoding": encoding
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
};
|
|
582
|
+
return {
|
|
583
|
+
...options,
|
|
584
|
+
routingInterceptors: [
|
|
585
|
+
routingInterceptor,
|
|
586
|
+
...toArray(options.routingInterceptors)
|
|
587
|
+
]
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function parseAcceptEncodings(header) {
|
|
592
|
+
if (header === void 0) {
|
|
593
|
+
return [];
|
|
244
594
|
}
|
|
595
|
+
return header.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase()).filter(Boolean);
|
|
596
|
+
}
|
|
597
|
+
const CACHE_CONTROL_NO_TRANSFORM_REGEX = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
|
|
598
|
+
function isNoTransformCacheControl(cacheControl) {
|
|
599
|
+
if (cacheControl === void 0) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
return CACHE_CONTROL_NO_TRANSFORM_REGEX.test(cacheControl);
|
|
245
603
|
}
|
|
246
604
|
|
|
247
|
-
class
|
|
605
|
+
class ResponseHeadersHandlerPlugin {
|
|
606
|
+
name = "~response-headers";
|
|
607
|
+
/**
|
|
608
|
+
* Interceptors should run after batch interceptors so headers are applied to each sub-response.
|
|
609
|
+
*/
|
|
610
|
+
before = ["~batch"];
|
|
611
|
+
init(options) {
|
|
612
|
+
return {
|
|
613
|
+
...options,
|
|
614
|
+
routingInterceptors: [
|
|
615
|
+
async (interceptorOptions) => {
|
|
616
|
+
const resHeaders = new Headers(interceptorOptions.context.resHeaders);
|
|
617
|
+
const result = await interceptorOptions.next({
|
|
618
|
+
...interceptorOptions,
|
|
619
|
+
context: {
|
|
620
|
+
...interceptorOptions.context,
|
|
621
|
+
resHeaders
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
if (!result.response) {
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
...result,
|
|
629
|
+
response: {
|
|
630
|
+
...result.response,
|
|
631
|
+
headers: mergeStandardHeaders(result.response.headers, toStandardHeaders(resHeaders))
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
},
|
|
635
|
+
...toArray(options.routingInterceptors)
|
|
636
|
+
]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
class RethrowHandlerPlugin {
|
|
642
|
+
name = "~rethrow";
|
|
248
643
|
filter;
|
|
249
644
|
CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
|
|
250
645
|
constructor(options) {
|
|
251
646
|
this.filter = options.filter;
|
|
252
647
|
}
|
|
253
648
|
init(options) {
|
|
254
|
-
|
|
255
|
-
options.interceptors ??= [];
|
|
256
|
-
options.rootInterceptors.push(async (options2) => {
|
|
649
|
+
const routingInterceptor = async (options2) => {
|
|
257
650
|
const pluginContext = {};
|
|
258
651
|
const result = await options2.next({
|
|
259
652
|
...options2,
|
|
@@ -266,8 +659,8 @@ class experimental_RethrowHandlerPlugin {
|
|
|
266
659
|
throw pluginContext.error.value;
|
|
267
660
|
}
|
|
268
661
|
return result;
|
|
269
|
-
}
|
|
270
|
-
|
|
662
|
+
};
|
|
663
|
+
const interceptor = async (options2) => {
|
|
271
664
|
const pluginContext = options2.context[this.CONTEXT_SYMBOL];
|
|
272
665
|
if (!pluginContext) {
|
|
273
666
|
throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
|
|
@@ -275,57 +668,26 @@ class experimental_RethrowHandlerPlugin {
|
|
|
275
668
|
try {
|
|
276
669
|
return await options2.next();
|
|
277
670
|
} catch (error) {
|
|
278
|
-
if (
|
|
671
|
+
if (this.filter(error, options2)) {
|
|
279
672
|
pluginContext.error = { value: error };
|
|
280
|
-
return { matched: false, response: void 0 };
|
|
281
673
|
}
|
|
282
674
|
throw error;
|
|
283
675
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.error = options.error ?? new ORPCError("CSRF_TOKEN_MISMATCH", {
|
|
299
|
-
status: 403,
|
|
300
|
-
message: "Invalid CSRF token"
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
order = 8e6;
|
|
304
|
-
init(options) {
|
|
305
|
-
options.rootInterceptors ??= [];
|
|
306
|
-
options.clientInterceptors ??= [];
|
|
307
|
-
options.rootInterceptors.unshift(async (options2) => {
|
|
308
|
-
const headerName = await value(this.headerName, options2);
|
|
309
|
-
const headerValue = await value(this.headerValue, options2);
|
|
310
|
-
return options2.next({
|
|
311
|
-
...options2,
|
|
312
|
-
context: {
|
|
313
|
-
...options2.context,
|
|
314
|
-
[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]: options2.request.headers[headerName] === headerValue
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
options.clientInterceptors.unshift(async (options2) => {
|
|
319
|
-
if (typeof options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL] !== "boolean") {
|
|
320
|
-
throw new TypeError("[SimpleCsrfProtectionHandlerPlugin] CSRF protection context has been corrupted or modified by another plugin or interceptor");
|
|
321
|
-
}
|
|
322
|
-
const excluded = await value(this.exclude, options2);
|
|
323
|
-
if (!excluded && !options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]) {
|
|
324
|
-
throw this.error;
|
|
325
|
-
}
|
|
326
|
-
return options2.next();
|
|
327
|
-
});
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
...options,
|
|
679
|
+
routingInterceptors: [
|
|
680
|
+
...toArray(options.routingInterceptors),
|
|
681
|
+
routingInterceptor
|
|
682
|
+
// rethrow as early as possible and keep it close to `interceptor` to avoid a corrupted or modified context
|
|
683
|
+
],
|
|
684
|
+
interceptors: [
|
|
685
|
+
interceptor,
|
|
686
|
+
// catch as many errors as possible
|
|
687
|
+
...toArray(options.interceptors)
|
|
688
|
+
]
|
|
689
|
+
};
|
|
328
690
|
}
|
|
329
691
|
}
|
|
330
692
|
|
|
331
|
-
export { BatchHandlerPlugin,
|
|
693
|
+
export { BatchHandlerPlugin, CORSHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestLimitHandlerPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, RethrowHandlerPlugin };
|