@ailura/nestjs-hono-adapter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/body.d.ts +35 -0
  4. package/dist/body.d.ts.map +1 -0
  5. package/dist/body.js +180 -0
  6. package/dist/body.js.map +1 -0
  7. package/dist/bridge.d.ts +64 -0
  8. package/dist/bridge.d.ts.map +1 -0
  9. package/dist/bridge.js +168 -0
  10. package/dist/bridge.js.map +1 -0
  11. package/dist/closing.d.ts +13 -0
  12. package/dist/closing.d.ts.map +1 -0
  13. package/dist/closing.js +30 -0
  14. package/dist/closing.js.map +1 -0
  15. package/dist/context.d.ts +22 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +2 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/cors-middleware.d.ts +64 -0
  20. package/dist/cors-middleware.d.ts.map +1 -0
  21. package/dist/cors-middleware.js +211 -0
  22. package/dist/cors-middleware.js.map +1 -0
  23. package/dist/handler-bridge.d.ts +51 -0
  24. package/dist/handler-bridge.d.ts.map +1 -0
  25. package/dist/handler-bridge.js +122 -0
  26. package/dist/handler-bridge.js.map +1 -0
  27. package/dist/hono-lifecycle.d.ts +90 -0
  28. package/dist/hono-lifecycle.d.ts.map +1 -0
  29. package/dist/hono-lifecycle.js +169 -0
  30. package/dist/hono-lifecycle.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/path.d.ts +3 -0
  36. package/dist/path.d.ts.map +1 -0
  37. package/dist/path.js +144 -0
  38. package/dist/path.js.map +1 -0
  39. package/dist/query.d.ts +19 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +238 -0
  42. package/dist/query.js.map +1 -0
  43. package/dist/response-helpers.d.ts +16 -0
  44. package/dist/response-helpers.d.ts.map +1 -0
  45. package/dist/response-helpers.js +45 -0
  46. package/dist/response-helpers.js.map +1 -0
  47. package/dist/response-writer.d.ts +28 -0
  48. package/dist/response-writer.d.ts.map +1 -0
  49. package/dist/response-writer.js +52 -0
  50. package/dist/response-writer.js.map +1 -0
  51. package/dist/route-adapter.d.ts +53 -0
  52. package/dist/route-adapter.d.ts.map +1 -0
  53. package/dist/route-adapter.js +141 -0
  54. package/dist/route-adapter.js.map +1 -0
  55. package/dist/server-adapter.d.ts +106 -0
  56. package/dist/server-adapter.d.ts.map +1 -0
  57. package/dist/server-adapter.js +153 -0
  58. package/dist/server-adapter.js.map +1 -0
  59. package/dist/sse.d.ts +67 -0
  60. package/dist/sse.d.ts.map +1 -0
  61. package/dist/sse.js +211 -0
  62. package/dist/sse.js.map +1 -0
  63. package/dist/static-assets.d.ts +39 -0
  64. package/dist/static-assets.d.ts.map +1 -0
  65. package/dist/static-assets.js +155 -0
  66. package/dist/static-assets.js.map +1 -0
  67. package/dist/version-filter.d.ts +24 -0
  68. package/dist/version-filter.d.ts.map +1 -0
  69. package/dist/version-filter.js +107 -0
  70. package/dist/version-filter.js.map +1 -0
  71. package/dist/versioned-route.d.ts +21 -0
  72. package/dist/versioned-route.d.ts.map +1 -0
  73. package/dist/versioned-route.js +15 -0
  74. package/dist/versioned-route.js.map +1 -0
  75. package/dist/views.d.ts +42 -0
  76. package/dist/views.d.ts.map +1 -0
  77. package/dist/views.js +110 -0
  78. package/dist/views.js.map +1 -0
  79. package/dist/ws-adapter.d.ts +81 -0
  80. package/dist/ws-adapter.d.ts.map +1 -0
  81. package/dist/ws-adapter.js +214 -0
  82. package/dist/ws-adapter.js.map +1 -0
  83. package/dist/ws-client.d.ts +68 -0
  84. package/dist/ws-client.d.ts.map +1 -0
  85. package/dist/ws-client.js +135 -0
  86. package/dist/ws-client.js.map +1 -0
  87. package/dist/ws-server.d.ts +23 -0
  88. package/dist/ws-server.d.ts.map +1 -0
  89. package/dist/ws-server.js +37 -0
  90. package/dist/ws-server.js.map +1 -0
  91. package/dist/ws.d.ts +14 -0
  92. package/dist/ws.d.ts.map +1 -0
  93. package/dist/ws.js +12 -0
  94. package/dist/ws.js.map +1 -0
  95. package/package.json +99 -0
  96. package/src/body.ts +251 -0
  97. package/src/bridge.ts +308 -0
  98. package/src/closing.ts +38 -0
  99. package/src/context.ts +25 -0
  100. package/src/cors-middleware.ts +347 -0
  101. package/src/handler-bridge.ts +226 -0
  102. package/src/hono-lifecycle.ts +259 -0
  103. package/src/index.ts +27 -0
  104. package/src/path.ts +169 -0
  105. package/src/query.ts +304 -0
  106. package/src/response-helpers.ts +60 -0
  107. package/src/response-writer.ts +100 -0
  108. package/src/route-adapter.ts +261 -0
  109. package/src/server-adapter.ts +294 -0
  110. package/src/sse.ts +274 -0
  111. package/src/static-assets.ts +247 -0
  112. package/src/version-filter.ts +170 -0
  113. package/src/versioned-route.ts +30 -0
  114. package/src/views.ts +188 -0
  115. package/src/ws-adapter.ts +329 -0
  116. package/src/ws-client.ts +190 -0
  117. package/src/ws-server.ts +40 -0
  118. package/src/ws.ts +13 -0
@@ -0,0 +1,347 @@
1
+ import { promisify } from 'node:util';
2
+
3
+ import type { MiddlewareHandler } from 'hono';
4
+ import { cors } from 'hono/cors';
5
+
6
+ import type { NestContext, NodeEnv } from './context.ts';
7
+
8
+ /** The origins a deployment may allow. */
9
+ type AllowedOrigins =
10
+ | boolean
11
+ | string
12
+ | RegExp
13
+ | readonly (string | RegExp)[];
14
+
15
+ /**
16
+ * The callback Nest's own options also allow, which decides an
17
+ * origin per request. It is the same shape the `cors` package
18
+ * takes, and the bridge awaits its answer before the middleware
19
+ * runs.
20
+ */
21
+ type OriginCallback = (
22
+ requestOrigin: string | undefined,
23
+ answer: OriginAnswer,
24
+ ) => void;
25
+
26
+ /** What a callback reports back: an error, or what it allows. */
27
+ type OriginAnswer = (
28
+ error?: Error | null,
29
+ origin?: AllowedOrigins,
30
+ ) => void;
31
+
32
+ /**
33
+ * The CORS options the adapter honours.
34
+ *
35
+ * They are declared here because Nest types the parameter as
36
+ * `any` on both the application and the adapter, so nothing can
37
+ * be taken from a signature. `optionsSuccessStatus` is accepted
38
+ * only when it names the status a preflight is answered with,
39
+ * because the middleware answers it the way the current
40
+ * specification asks for; an application that continues a
41
+ * preflight answers it itself, so the option says nothing
42
+ * then.
43
+ */
44
+ interface CorsOptions {
45
+ readonly allowedHeaders?: string | readonly string[];
46
+ readonly credentials?: boolean;
47
+ readonly exposedHeaders?: string | readonly string[];
48
+ readonly maxAge?: number;
49
+ readonly methods?: string | readonly string[];
50
+ readonly optionsSuccessStatus?: number;
51
+ readonly origin?: AllowedOrigins | OriginCallback;
52
+ readonly preflightContinue?: boolean;
53
+ }
54
+
55
+ /**
56
+ * What a browser may send. The CSRF header is named because a
57
+ * request that carries it stops being a simple one: a foreign
58
+ * site cannot make it without a preflight, and the preflight is
59
+ * where this list is read.
60
+ */
61
+ const ALLOWED_HEADERS = [
62
+ 'authorization',
63
+ 'content-type',
64
+ 'x-csrf-token',
65
+ ];
66
+
67
+ /** What a browser may call. */
68
+ const ALLOWED_METHODS = [
69
+ 'DELETE',
70
+ 'GET',
71
+ 'HEAD',
72
+ 'OPTIONS',
73
+ 'PATCH',
74
+ 'POST',
75
+ 'PUT',
76
+ ];
77
+
78
+ /** How long a browser may reuse a preflight answer. */
79
+ const MAX_AGE_SECONDS = 600;
80
+
81
+ /**
82
+ * The status a preflight is answered with. The specification
83
+ * asks for no content, and the middleware always sends it.
84
+ */
85
+ const PREFLIGHT_STATUS = 204;
86
+
87
+ const NO_HEADERS: readonly string[] = [];
88
+
89
+ function matchesRule(
90
+ origin: string,
91
+ rule: string | RegExp,
92
+ ): boolean {
93
+ if (rule instanceof RegExp) {
94
+ return rule.test(origin);
95
+ }
96
+
97
+ return rule === origin;
98
+ }
99
+
100
+ function isAllowedOrigin(
101
+ origin: string,
102
+ allowed: AllowedOrigins | undefined,
103
+ ): boolean {
104
+ if (
105
+ origin === '' ||
106
+ allowed === false ||
107
+ allowed === undefined
108
+ ) {
109
+ return false;
110
+ }
111
+
112
+ if (allowed === true) {
113
+ return true;
114
+ }
115
+
116
+ if (typeof allowed === 'string') {
117
+ return allowed === '*' || allowed === origin;
118
+ }
119
+
120
+ if (allowed instanceof RegExp) {
121
+ return allowed.test(origin);
122
+ }
123
+
124
+ return allowed.some((candidate) =>
125
+ matchesRule(origin, candidate),
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Reads the origin a browser may call from: the origin itself
131
+ * when it is allowed, and nothing when it is not.
132
+ *
133
+ * Hono writes no header for nothing, and a browser that finds
134
+ * no matching header keeps a foreign page from reading the
135
+ * answer. Nothing here refuses a request either — CORS is a
136
+ * rule a browser follows, and a service that enforced it would
137
+ * have to guess at callers that are not browsers at all.
138
+ */
139
+ function allowedOrigin(
140
+ allowed: AllowedOrigins | undefined,
141
+ ): (origin: string) => string | undefined {
142
+ return (origin: string): string | undefined => {
143
+ if (isAllowedOrigin(origin, allowed)) {
144
+ return origin;
145
+ }
146
+
147
+ return undefined;
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Reads a list of header names. Nest accepts the same list as
153
+ * an array or as one comma-separated string, and the string
154
+ * form is what the platform adapters are given, so both are
155
+ * split.
156
+ */
157
+ /**
158
+ * Reads a list that Nest accepts either as an array or as one
159
+ * comma-separated string. The string form is what the platform
160
+ * adapters are given, so both end up as the same list.
161
+ */
162
+ function asList(
163
+ value: string | readonly string[],
164
+ ): readonly string[] {
165
+ if (typeof value === 'string') {
166
+ return [value];
167
+ }
168
+ return value;
169
+ }
170
+
171
+ function headerList(
172
+ value: string | readonly string[] | undefined,
173
+ fallback: readonly string[],
174
+ ): string[] {
175
+ if (value === undefined) {
176
+ return [...fallback];
177
+ }
178
+
179
+ return asList(value)
180
+ .flatMap((entry) => entry.split(','))
181
+ .map((entry) => entry.trim())
182
+ .filter((entry) => entry !== '');
183
+ }
184
+
185
+ /**
186
+ * The origin rules a middleware reads on its own. A callback is
187
+ * not one of them: it answers per request, which the bridge
188
+ * does before the middleware runs.
189
+ */
190
+ function staticOrigin(
191
+ origin: AllowedOrigins | OriginCallback | undefined,
192
+ ): AllowedOrigins | undefined {
193
+ if (typeof origin === 'function') {
194
+ return undefined;
195
+ }
196
+
197
+ return origin;
198
+ }
199
+
200
+ /**
201
+ * Reads the origin a callback decides for one request.
202
+ *
203
+ * The callback answers through a second argument rather than by
204
+ * returning, so its answer is awaited here. An error it reports
205
+ * is thrown, which leaves the request on the path the exception
206
+ * layer already owns.
207
+ */
208
+ function askOrigin(
209
+ origin: OriginCallback,
210
+ requestOrigin: string | undefined,
211
+ answer: OriginAnswer,
212
+ ): void {
213
+ origin(requestOrigin, answer);
214
+ }
215
+
216
+ const askResolvedOrigin = promisify(askOrigin);
217
+
218
+ /**
219
+ * The origin this request is allowed: the configured value
220
+ * itself, unless a callback decides it per request, in which
221
+ * case the callback is asked and its answer awaited.
222
+ */
223
+ function resolveOrigin(
224
+ origin: AllowedOrigins | OriginCallback | undefined,
225
+ requestOrigin: string | undefined,
226
+ ): Promise<AllowedOrigins | undefined> {
227
+ if (typeof origin !== 'function') {
228
+ return Promise.resolve(origin);
229
+ }
230
+
231
+ return askResolvedOrigin(origin, requestOrigin);
232
+ }
233
+
234
+ /**
235
+ * Translates the options Nest was given into the middleware the
236
+ * adapter runs. An origin a callback resolved for this request
237
+ * is handed in, because the middleware reads the option once.
238
+ *
239
+ * An answer status other than the one the middleware sends is
240
+ * refused rather than ignored: the adapter answers a preflight
241
+ * itself, and quietly doing something other than what was asked
242
+ * is worse than failing at startup.
243
+ */
244
+ function corsMiddleware(
245
+ options: CorsOptions,
246
+ resolved?: AllowedOrigins,
247
+ ): MiddlewareHandler<NodeEnv> {
248
+ const named = resolved ?? staticOrigin(options.origin);
249
+ if (
250
+ options.preflightContinue !== true &&
251
+ options.optionsSuccessStatus !== undefined &&
252
+ options.optionsSuccessStatus !== PREFLIGHT_STATUS
253
+ ) {
254
+ throw new TypeError(
255
+ 'The Hono adapter answers CORS preflight requests with ' +
256
+ `${PREFLIGHT_STATUS}, so optionsSuccessStatus cannot ` +
257
+ 'name another status.',
258
+ );
259
+ }
260
+
261
+ return cors({
262
+ allowHeaders: headerList(
263
+ options.allowedHeaders,
264
+ ALLOWED_HEADERS,
265
+ ),
266
+ allowMethods: headerList(options.methods, ALLOWED_METHODS),
267
+ credentials: options.credentials ?? false,
268
+ exposeHeaders: headerList(
269
+ options.exposedHeaders,
270
+ NO_HEADERS,
271
+ ),
272
+ maxAge: options.maxAge ?? MAX_AGE_SECONDS,
273
+ origin: allowedOrigin(named),
274
+ });
275
+ }
276
+
277
+ /** A step that ends the chain, for reading a preflight answer. */
278
+ const NOOP_NEXT = (): Promise<void> => Promise.resolve();
279
+
280
+ /** Says whether this request is a preflight the router answers. */
281
+ function isContinuedPreflight(
282
+ options: CorsOptions,
283
+ context: NestContext,
284
+ ): boolean {
285
+ return (
286
+ options.preflightContinue === true &&
287
+ context.req.method === 'OPTIONS'
288
+ );
289
+ }
290
+
291
+ /**
292
+ * Writes the headers a preflight would have carried, so the
293
+ * request can travel on to the route that answers it.
294
+ */
295
+ async function forwardPreflight(
296
+ context: NestContext,
297
+ middleware: MiddlewareHandler<NodeEnv>,
298
+ ): Promise<void> {
299
+ const answer = await middleware(context, NOOP_NEXT);
300
+ if (answer === undefined) {
301
+ return;
302
+ }
303
+
304
+ for (const [name, value] of answer.headers) {
305
+ context.header(name, value);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Runs CORS once a deployment has turned it on, and once the
311
+ * origin for this request is known.
312
+ *
313
+ * The options are read when the application is configured,
314
+ * which happens after the chain it belongs to already exists,
315
+ * so the chain holds a step that looks them up instead. A
316
+ * service that never enables CORS simply continues, and no
317
+ * origin is allowed because no header is ever written.
318
+ *
319
+ * A preflight asked to continue is written with its headers and
320
+ * then travels on, which is what an application that answers
321
+ * OPTIONS itself expects.
322
+ */
323
+ function corsBridge(
324
+ handler: () => CorsOptions | undefined,
325
+ ): MiddlewareHandler<NodeEnv> {
326
+ return async (context, next) => {
327
+ const options = handler();
328
+ if (options === undefined) {
329
+ return next();
330
+ }
331
+
332
+ const allowed = await resolveOrigin(
333
+ options.origin,
334
+ context.req.header('origin'),
335
+ );
336
+ const middleware = corsMiddleware(options, allowed);
337
+ if (isContinuedPreflight(options, context)) {
338
+ await forwardPreflight(context, middleware);
339
+ return next();
340
+ }
341
+
342
+ return middleware(context, next);
343
+ };
344
+ }
345
+
346
+ export { corsBridge, corsMiddleware };
347
+ export type { CorsOptions };
@@ -0,0 +1,226 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import type { Next } from 'hono';
3
+
4
+ import { readBody } from './body.ts';
5
+ import { toNestRequest } from './bridge.ts';
6
+ import type {
7
+ NestHandler,
8
+ NestRequest,
9
+ NextHandler,
10
+ RequestOptions,
11
+ } from './bridge.ts';
12
+ import type { NestContext } from './context.ts';
13
+ import { finalizeOnResponse } from './response-helpers.ts';
14
+ import { mountSse } from './sse.ts';
15
+
16
+ /**
17
+ * Called when a handler runs outside a Hono pipeline, where
18
+ * there is no next handler to continue to.
19
+ */
20
+ const NOOP_NEXT: NextHandler = () => Promise.resolve();
21
+
22
+ /** Which of the two things a route handler races settled first. */
23
+ const HANDLED = 'handled';
24
+ const STREAMING = 'streaming';
25
+
26
+ type Outcome = typeof HANDLED | typeof STREAMING;
27
+
28
+ /** Says a handler failed after the stream it started was sent. */
29
+ const logger = new Logger('SseRoute');
30
+
31
+ /** The message of a failure, whatever shape it was thrown in. */
32
+ function describe(error: unknown): string {
33
+ if (error instanceof Error) {
34
+ return error.message;
35
+ }
36
+ return String(error);
37
+ }
38
+
39
+ /** The handler Hono runs for a route Nest registered. */
40
+ type HonoRouteHandler = (
41
+ context: NestContext,
42
+ next: Next,
43
+ ) => Promise<Response>;
44
+
45
+ /**
46
+ * The handler Nest installs as its global exception layer. It
47
+ * takes the exception first, so it is not a route handler. The
48
+ * arguments are spelled as a tuple because their order is fixed
49
+ * by Nest rather than by this adapter.
50
+ */
51
+ type NestExceptionHandler = (
52
+ ...args: [unknown, NestRequest, NestContext, NextHandler]
53
+ ) => unknown;
54
+
55
+ /** Runs the exception layer for one failed request. */
56
+ type ExceptionRunner = (
57
+ error: unknown,
58
+ context: NestContext,
59
+ ) => Promise<Response>;
60
+
61
+ /**
62
+ * Everything the bridge needs to know about the adapter that
63
+ * built it. Each value is read per request rather than
64
+ * captured, because a deployment configures the parsers and the
65
+ * proxy behind the adapter after the routes exist.
66
+ */
67
+ interface BridgeOptions {
68
+ readonly bodyLimit: () => number | undefined;
69
+ readonly bodyParsingEnabled: () => boolean;
70
+ readonly pendingStatus: (
71
+ context: NestContext,
72
+ ) => number | undefined;
73
+ readonly rawBody: () => boolean;
74
+ readonly trustProxy: () => boolean;
75
+ }
76
+
77
+ function requestOptions(
78
+ options: BridgeOptions,
79
+ ): RequestOptions {
80
+ return { trustProxy: options.trustProxy() };
81
+ }
82
+
83
+ /**
84
+ * Builds the request Nest reads and the parser fills.
85
+ *
86
+ * Nest writes to the request bag, so the parsed payload is
87
+ * copied into it here instead of being passed along separately.
88
+ * A payload that cannot be parsed throws the exception Nest
89
+ * raises for a failed parse, which is what lets the exception
90
+ * layer answer it like any other failure.
91
+ */
92
+ async function prepareRequest(
93
+ context: NestContext,
94
+ options: BridgeOptions,
95
+ ): Promise<NestRequest> {
96
+ const request = toNestRequest(
97
+ context,
98
+ requestOptions(options),
99
+ );
100
+ finalizeOnResponse(context);
101
+ if (!options.bodyParsingEnabled()) {
102
+ return request;
103
+ }
104
+ const parsed = await readBody(context, {
105
+ bodyLimit: options.bodyLimit(),
106
+ rawBody: options.rawBody(),
107
+ });
108
+ request.body = parsed.body;
109
+ request.files = parsed.files;
110
+ request.rawBody = parsed.rawBody;
111
+ return request;
112
+ }
113
+
114
+ /**
115
+ * Waits for a promise and says which of the two settled, so the
116
+ * race below cannot confuse a handler that answered with
117
+ * nothing and a stream that started.
118
+ */
119
+ async function settledAs(
120
+ promise: Promise<unknown>,
121
+ outcome: Outcome,
122
+ ): Promise<Outcome> {
123
+ await promise;
124
+ return outcome;
125
+ }
126
+
127
+ function firstOutcome(
128
+ handled: Promise<unknown>,
129
+ streaming: Promise<unknown>,
130
+ ): Promise<Outcome> {
131
+ return Promise.race([
132
+ settledAs(handled, HANDLED),
133
+ settledAs(streaming, STREAMING),
134
+ ]);
135
+ }
136
+
137
+ /**
138
+ * Reports a failure that arrived after the stream it opened was
139
+ * answered: the client already has its `200`, so there is
140
+ * nothing left to send it to.
141
+ */
142
+ async function watchFailure(
143
+ handled: Promise<unknown>,
144
+ ): Promise<void> {
145
+ try {
146
+ await handled;
147
+ } catch (error) {
148
+ logger.error(
149
+ `A handler failed after its event stream started: ${describe(error)}`,
150
+ );
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Bridges one Nest handler to Hono: it builds the request Nest
156
+ * reads, runs the handler and hands back the response the
157
+ * adapter collected.
158
+ *
159
+ * An `@Sse()` route answers only when its observable completes,
160
+ * which is exactly what a long-lived stream never does, so the
161
+ * handler is raced against the stream it starts. The moment the
162
+ * stream commits its headers the response goes out, and the
163
+ * handler is left writing into it — with a late failure logged
164
+ * instead of escaping.
165
+ */
166
+ function createRouteHandler(
167
+ handler: NestHandler,
168
+ options: BridgeOptions,
169
+ ): HonoRouteHandler {
170
+ return async (context, next) => {
171
+ const request = await prepareRequest(context, options);
172
+ const started = mountSse(context, () =>
173
+ options.pendingStatus(context),
174
+ );
175
+ const handled = Promise.resolve(
176
+ handler(request, context, next),
177
+ );
178
+ const outcome = await firstOutcome(handled, started);
179
+ if (outcome === STREAMING) {
180
+ void watchFailure(handled);
181
+ }
182
+ return context.res;
183
+ };
184
+ }
185
+
186
+ async function runNestHandler(
187
+ handler: NestHandler,
188
+ context: NestContext,
189
+ options: BridgeOptions,
190
+ ): Promise<Response> {
191
+ const request = await prepareRequest(context, options);
192
+ await handler(request, context, NOOP_NEXT);
193
+ return context.res;
194
+ }
195
+
196
+ /**
197
+ * Builds the runner for the exception layer. The payload is not
198
+ * read again here: the failure may well be that reading it
199
+ * failed, and a second attempt would replace the answer with a
200
+ * failure of its own.
201
+ */
202
+ function createExceptionRunner(
203
+ handler: NestExceptionHandler,
204
+ options: BridgeOptions,
205
+ ): ExceptionRunner {
206
+ return async (error, context) => {
207
+ const request = toNestRequest(
208
+ context,
209
+ requestOptions(options),
210
+ );
211
+ finalizeOnResponse(context);
212
+ await handler(error, request, context, NOOP_NEXT);
213
+ return context.res;
214
+ };
215
+ }
216
+
217
+ export {
218
+ createExceptionRunner,
219
+ createRouteHandler,
220
+ runNestHandler,
221
+ };
222
+ export type {
223
+ BridgeOptions,
224
+ ExceptionRunner,
225
+ NestExceptionHandler,
226
+ };