Skip to content

Commit 99551ae

Browse files
authored
Stop leaking internal MiddlewareError implementation detail (#13180)
1 parent 4f885c4 commit 99551ae

File tree

4 files changed

+67
-47
lines changed

4 files changed

+67
-47
lines changed

.changeset/clever-sheep-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
- UNSTABLE(BREAKING): If a middleware throws an error, ensure we only bubble the error itself via `next()` and are no longer leaking the `MiddlewareError` implementation detail

packages/react-router/__tests__/router/context-middleware-test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,7 @@ describe("context/middleware", () => {
15831583

15841584
describe("throwing", () => {
15851585
it("throwing from a middleware short circuits immediately (going down - loader)", async () => {
1586+
let error: unknown;
15861587
let handler = createStaticHandler([
15871588
{
15881589
path: "/",
@@ -1593,6 +1594,12 @@ describe("context/middleware", () => {
15931594
unstable_middleware: [
15941595
async ({ context }, next) => {
15951596
context.set(parentContext, "PARENT 1");
1597+
try {
1598+
await next();
1599+
} catch (e) {
1600+
error = e;
1601+
throw e;
1602+
}
15961603
},
15971604
async ({ context }, next) => {
15981605
throw new Error("PARENT 2");
@@ -1632,6 +1639,9 @@ describe("context/middleware", () => {
16321639
expect(staticContext.errors).toEqual({
16331640
parent: "ERROR: PARENT 2",
16341641
});
1642+
1643+
// Ensure we don't leak the `middlewareError`structure to userland
1644+
expect(error).toEqual(new Error("PARENT 2"));
16351645
});
16361646

16371647
it("throwing from a middleware short circuits immediately (going up - loader)", async () => {

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import * as React from "react";
22
import { decode } from "turbo-stream";
3-
import type {
4-
Router as DataRouter,
5-
MiddlewareError,
6-
} from "../../router/router";
3+
import type { Router as DataRouter } from "../../router/router";
74
import { isResponse, runMiddlewarePipeline } from "../../router/router";
85
import type {
96
DataStrategyFunction,
@@ -136,6 +133,10 @@ export function StreamTransfer({
136133
}
137134
}
138135

136+
function handleMiddlewareError(error: unknown, routeId: string) {
137+
return { [routeId]: { type: "error", result: error } };
138+
}
139+
139140
export function getSingleFetchDataStrategy(
140141
manifest: AssetsManifest,
141142
routeModules: RouteModules,
@@ -152,7 +153,7 @@ export function getSingleFetchDataStrategy(
152153
args,
153154
false,
154155
() => singleFetchActionStrategy(request, matches, basename),
155-
(e) => ({ [e.routeId]: { type: "error", result: e.error } })
156+
handleMiddlewareError
156157
) as Promise<Record<string, DataStrategyResult>>;
157158
}
158159

@@ -201,7 +202,7 @@ export function getSingleFetchDataStrategy(
201202
args,
202203
false,
203204
() => nonSsrStrategy(manifest, request, matches, basename),
204-
(e) => ({ [e.routeId]: { type: "error", result: e.error } })
205+
handleMiddlewareError
205206
) as Promise<Record<string, DataStrategyResult>>;
206207
}
207208
}
@@ -212,7 +213,7 @@ export function getSingleFetchDataStrategy(
212213
args,
213214
false,
214215
() => singleFetchLoaderFetcherStrategy(request, matches, basename),
215-
(e) => ({ [e.routeId]: { type: "error", result: e.error } })
216+
handleMiddlewareError
216217
) as Promise<Record<string, DataStrategyResult>>;
217218
}
218219

@@ -230,7 +231,7 @@ export function getSingleFetchDataStrategy(
230231
matches,
231232
basename
232233
),
233-
(e) => ({ [e.routeId]: { type: "error", result: e.error } })
234+
handleMiddlewareError
234235
) as Promise<Record<string, DataStrategyResult>>;
235236
};
236237
}

packages/react-router/lib/router/router.ts

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3525,9 +3525,9 @@ export function createStaticHandler(
35253525

35263526
return res;
35273527
},
3528-
async (e) => {
3529-
if (isResponse(e.error)) {
3530-
return e.error;
3528+
async (error, routeId) => {
3529+
if (isResponse(error)) {
3530+
return error;
35313531
}
35323532

35333533
if (renderedStaticContext) {
@@ -3538,16 +3538,16 @@ export function createStaticHandler(
35383538
// to align server/client behavior. Client side middleware uses
35393539
// dataStrategy and a given route can only have one result, so the
35403540
// error overwrites any prior loader data.
3541-
if (e.routeId in renderedStaticContext.loaderData) {
3542-
renderedStaticContext.loaderData[e.routeId] = undefined;
3541+
if (routeId in renderedStaticContext.loaderData) {
3542+
renderedStaticContext.loaderData[routeId] = undefined;
35433543
}
35443544

35453545
return respond(
35463546
getStaticContextFromError(
35473547
dataRoutes,
35483548
renderedStaticContext,
3549-
e.error,
3550-
findNearestBoundary(matches!, e.routeId).route.id
3549+
error,
3550+
findNearestBoundary(matches!, routeId).route.id
35513551
)
35523552
);
35533553
} else {
@@ -3568,11 +3568,9 @@ export function createStaticHandler(
35683568
loaderData: {},
35693569
actionData: null,
35703570
errors: {
3571-
[boundary.route.id]: e.error,
3571+
[boundary.route.id]: error,
35723572
},
3573-
statusCode: isRouteErrorResponse(e.error)
3574-
? e.error.status
3575-
: 500,
3573+
statusCode: isRouteErrorResponse(error) ? error.status : 500,
35763574
actionHeaders: {},
35773575
loaderHeaders: {},
35783576
});
@@ -3731,11 +3729,11 @@ export function createStaticHandler(
37313729
? new Response(value)
37323730
: Response.json(value);
37333731
},
3734-
(e) => {
3735-
if (isResponse(e.error)) {
3736-
return respond(e.error);
3732+
(error) => {
3733+
if (isResponse(error)) {
3734+
return respond(error);
37373735
}
3738-
return new Response(String(e.error), {
3736+
return new Response(String(error), {
37393737
status: 500,
37403738
statusText: "Unexpected Server Error",
37413739
});
@@ -4935,13 +4933,21 @@ async function defaultDataStrategyWithMiddleware(
49354933
args,
49364934
false,
49374935
() => defaultDataStrategy(args),
4938-
(e) => ({ [e.routeId]: { type: "error", result: e.error } })
4936+
(error, routeId) => ({ [routeId]: { type: "error", result: error } })
49394937
) as Promise<Record<string, DataStrategyResult>>;
49404938
}
49414939

49424940
type MutableMiddlewareState = {
4943-
handlerResult: unknown;
4944-
propagateResult: boolean;
4941+
// Track the result of the user-provided handler at the bottom of the
4942+
// middleware chain so we can return it out from runMiddleware for them when
4943+
// we're not propagating results
4944+
handlerResult?: unknown;
4945+
// Capture the throwing routeId for middleware error so we can bubble from the
4946+
// correct spot
4947+
middlewareError?: {
4948+
routeId: string;
4949+
error: unknown;
4950+
};
49454951
};
49464952

49474953
export async function runMiddlewarePipeline<T extends boolean>(
@@ -4957,12 +4963,11 @@ export async function runMiddlewarePipeline<T extends boolean>(
49574963
handler: () => T extends true
49584964
? MaybePromise<Response>
49594965
: MaybePromise<Record<string, DataStrategyResult>>,
4960-
errorHandler: (error: MiddlewareError) => unknown
4966+
errorHandler: (error: unknown, routeId: string) => unknown
49614967
): Promise<unknown> {
49624968
let { matches, request, params, context } = args;
49634969
let middlewareState: MutableMiddlewareState = {
49644970
handlerResult: undefined,
4965-
propagateResult,
49664971
};
49674972
try {
49684973
let tuples = matches.flatMap((m) =>
@@ -4973,40 +4978,34 @@ export async function runMiddlewarePipeline<T extends boolean>(
49734978
let result = await callRouteMiddleware(
49744979
{ request, params, context },
49754980
tuples,
4981+
propagateResult,
49764982
middlewareState,
49774983
handler
49784984
);
4979-
return middlewareState.propagateResult
4980-
? result
4981-
: middlewareState.handlerResult;
4985+
return propagateResult ? result : middlewareState.handlerResult;
49824986
} catch (e) {
4983-
if (!(e instanceof MiddlewareError)) {
4987+
if (!middlewareState.middlewareError) {
49844988
// This shouldn't happen? This would have to come from a bug in our
49854989
// library code...
49864990
throw e;
49874991
}
4988-
let result = await errorHandler(e);
4992+
let result = await errorHandler(
4993+
middlewareState.middlewareError.error,
4994+
middlewareState.middlewareError.routeId
4995+
);
49894996
if (propagateResult || !middlewareState.handlerResult) {
49904997
return result;
49914998
}
49924999
return Object.assign(middlewareState.handlerResult, result);
49935000
}
49945001
}
49955002

4996-
export class MiddlewareError {
4997-
routeId: string;
4998-
error: unknown;
4999-
constructor(routeId: string, error: unknown) {
5000-
this.routeId = routeId;
5001-
this.error = error;
5002-
}
5003-
}
5004-
50055003
async function callRouteMiddleware(
50065004
args:
50075005
| LoaderFunctionArgs<unstable_RouterContextProvider>
50085006
| ActionFunctionArgs<unstable_RouterContextProvider>,
50095007
middlewares: [string, unstable_MiddlewareFunction][],
5008+
propagateResult: boolean,
50105009
middlewareState: MutableMiddlewareState,
50115010
handler: () => void,
50125011
idx = 0
@@ -5039,11 +5038,12 @@ async function callRouteMiddleware(
50395038
let result = await callRouteMiddleware(
50405039
args,
50415040
middlewares,
5041+
propagateResult,
50425042
middlewareState,
50435043
handler,
50445044
idx + 1
50455045
);
5046-
if (middlewareState.propagateResult) {
5046+
if (propagateResult) {
50475047
nextResult = result;
50485048
return nextResult;
50495049
}
@@ -5070,11 +5070,15 @@ async function callRouteMiddleware(
50705070
} else {
50715071
return next();
50725072
}
5073-
} catch (e) {
5074-
if (e instanceof MiddlewareError) {
5075-
throw e;
5073+
} catch (error) {
5074+
if (!middlewareState.middlewareError) {
5075+
middlewareState.middlewareError = { routeId, error };
5076+
} else if (middlewareState.middlewareError.error !== error) {
5077+
// Another middleware already threw, so only capture this new routeId if
5078+
// it's a different error and not just bubbling up the existing error
5079+
middlewareState.middlewareError = { routeId, error };
50765080
}
5077-
throw new MiddlewareError(routeId, e);
5081+
throw error;
50785082
}
50795083
}
50805084

0 commit comments

Comments
 (0)