Skip to content

Commit 59dd6c9

Browse files
authored
Fix flows for returning/throwing data() from middleware (#14128)
1 parent 9dd7730 commit 59dd6c9

File tree

5 files changed

+532
-18
lines changed

5 files changed

+532
-18
lines changed

.changeset/curly-chefs-smell.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+
[REMOVE] Few additional fixes for returning/throwing data from middleware - attach to #14093

integration/middleware-test.ts

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
Response as PlaywrightResponse,
44
} from "@playwright/test";
55
import { test, expect } from "@playwright/test";
6-
import { UNSAFE_ServerMode } from "react-router";
6+
import { UNSAFE_ErrorResponseImpl, UNSAFE_ServerMode } from "react-router";
77

88
import {
99
createAppFixture,
@@ -2938,6 +2938,234 @@ test.describe("Middleware", () => {
29382938
appFixture.close();
29392939
});
29402940

2941+
test("bubbles response up the chain when middleware throws data() before next", async ({
2942+
page,
2943+
}) => {
2944+
let fixture = await createFixture(
2945+
{
2946+
files: {
2947+
"react-router.config.ts": reactRouterConfig({
2948+
middleware: true,
2949+
}),
2950+
"vite.config.ts": js`
2951+
import { defineConfig } from "vite";
2952+
import { reactRouter } from "@react-router/dev/vite";
2953+
2954+
export default defineConfig({
2955+
build: { manifest: true, minify: false },
2956+
plugins: [reactRouter()],
2957+
});
2958+
`,
2959+
"app/routes/_index.tsx": js`
2960+
import { Link } from 'react-router'
2961+
export default function Component({ loaderData }) {
2962+
return <Link to="/a/b/c">/a/b/c</Link>;
2963+
}
2964+
`,
2965+
"app/routes/a.tsx": js`
2966+
import { Outlet } from 'react-router'
2967+
export const unstable_middleware = [
2968+
async (_, next) => {
2969+
let res = await next();
2970+
res.headers.set('x-a', 'true');
2971+
return res;
2972+
}
2973+
];
2974+
export default function Component() {
2975+
return <Outlet/>
2976+
}
2977+
export function ErrorBoundary({ error }) {
2978+
return (
2979+
<>
2980+
<h1 data-error>A Error Boundary</h1>
2981+
<pre>{error.data}</pre>
2982+
</>
2983+
);
2984+
}
2985+
`,
2986+
"app/routes/a.b.tsx": js`
2987+
import { Link, Outlet } from 'react-router'
2988+
export const unstable_middleware = [
2989+
async (_, next) => {
2990+
let res = await next();
2991+
res.headers.set('x-b', 'true');
2992+
return res;
2993+
}
2994+
];
2995+
export default function Component({ loaderData }) {
2996+
return <Outlet/>;
2997+
}
2998+
`,
2999+
"app/routes/a.b.c.tsx": js`
3000+
import { data } from "react-router";
3001+
export const unstable_middleware = [(_, next) => {
3002+
throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
3003+
}];
3004+
// Force middleware to run on client side navs
3005+
export function loader() {
3006+
return null;
3007+
}
3008+
export default function Component({ loaderData }) {
3009+
return <h1>C</h1>
3010+
}
3011+
`,
3012+
},
3013+
},
3014+
UNSAFE_ServerMode.Development,
3015+
);
3016+
3017+
let appFixture = await createAppFixture(
3018+
fixture,
3019+
UNSAFE_ServerMode.Development,
3020+
);
3021+
3022+
let res = await fixture.requestDocument("/a/b/c");
3023+
expect(res.status).toBe(418);
3024+
expect(res.headers.get("x-a")).toBe("true");
3025+
expect(res.headers.get("x-b")).toBe("true");
3026+
let html = await res.text();
3027+
expect(html).toContain("A Error Boundary");
3028+
expect(html).toContain("C ERROR");
3029+
3030+
let data = await fixture.requestSingleFetchData("/a/b/c.data");
3031+
expect(data.status).toBe(418);
3032+
expect(data.headers.get("x-a")).toBe("true");
3033+
expect(data.headers.get("x-b")).toBe("true");
3034+
expect((data.data as any)["routes/a.b.c"]).toEqual({
3035+
error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
3036+
});
3037+
3038+
let app = new PlaywrightFixture(appFixture, page);
3039+
await app.goto("/");
3040+
await app.clickLink("/a/b/c");
3041+
await page.waitForSelector("[data-error]");
3042+
expect(await page.locator("[data-error]").textContent()).toBe(
3043+
"A Error Boundary",
3044+
);
3045+
expect(await page.locator("pre").textContent()).toBe("C ERROR");
3046+
3047+
appFixture.close();
3048+
});
3049+
3050+
test("bubbles response up the chain when middleware throws data() after next", async ({
3051+
page,
3052+
}) => {
3053+
let fixture = await createFixture(
3054+
{
3055+
files: {
3056+
"react-router.config.ts": reactRouterConfig({
3057+
middleware: true,
3058+
}),
3059+
"vite.config.ts": js`
3060+
import { defineConfig } from "vite";
3061+
import { reactRouter } from "@react-router/dev/vite";
3062+
3063+
export default defineConfig({
3064+
build: { manifest: true, minify: false },
3065+
plugins: [reactRouter()],
3066+
});
3067+
`,
3068+
"app/routes/_index.tsx": js`
3069+
import { Link } from 'react-router'
3070+
export default function Component({ loaderData }) {
3071+
return <Link to="/a/b/c">/a/b/c</Link>;
3072+
}
3073+
`,
3074+
"app/routes/a.tsx": js`
3075+
import { Outlet } from 'react-router'
3076+
export const unstable_middleware = [
3077+
async (_, next) => {
3078+
let res = await next();
3079+
res.headers.set('x-a', 'true');
3080+
return res;
3081+
}
3082+
];
3083+
export function loader() {
3084+
return "A LOADER";
3085+
}
3086+
export default function Component() {
3087+
return <Outlet/>
3088+
}
3089+
export function ErrorBoundary({ error, loaderData }) {
3090+
return (
3091+
<>
3092+
<h1 data-error>A Error Boundary</h1>
3093+
<pre>{error.data}</pre>
3094+
<p>{loaderData}</p>
3095+
</>
3096+
);
3097+
}
3098+
`,
3099+
"app/routes/a.b.tsx": js`
3100+
import { Link, Outlet } from 'react-router'
3101+
export const unstable_middleware = [
3102+
async (_, next) => {
3103+
let res = await next();
3104+
res.headers.set('x-b', 'true');
3105+
return res;
3106+
}
3107+
];
3108+
export default function Component({ loaderData }) {
3109+
return <Outlet/>;
3110+
}
3111+
`,
3112+
"app/routes/a.b.c.tsx": js`
3113+
import { data } from "react-router";
3114+
export const unstable_middleware = [async (_, next) => {
3115+
let res = await next();
3116+
throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
3117+
}];
3118+
// Force middleware to run on client side navs
3119+
export function loader() {
3120+
return null;
3121+
}
3122+
export default function Component({ loaderData }) {
3123+
return <h1>C</h1>
3124+
}
3125+
`,
3126+
},
3127+
},
3128+
UNSAFE_ServerMode.Development,
3129+
);
3130+
3131+
let appFixture = await createAppFixture(
3132+
fixture,
3133+
UNSAFE_ServerMode.Development,
3134+
);
3135+
3136+
let res = await fixture.requestDocument("/a/b/c");
3137+
expect(res.status).toBe(418);
3138+
expect(res.headers.get("x-a")).toBe("true");
3139+
expect(res.headers.get("x-b")).toBe("true");
3140+
let html = await res.text();
3141+
expect(html).toContain("A Error Boundary");
3142+
expect(html).toContain("C ERROR");
3143+
expect(html).toContain("A LOADER");
3144+
3145+
let data = await fixture.requestSingleFetchData("/a/b/c.data");
3146+
expect(data.status).toBe(418);
3147+
expect(data.headers.get("x-a")).toBe("true");
3148+
expect(data.headers.get("x-b")).toBe("true");
3149+
expect((data.data as any)["routes/a"]).toEqual({
3150+
data: "A LOADER",
3151+
});
3152+
expect((data.data as any)["routes/a.b.c"]).toEqual({
3153+
error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
3154+
});
3155+
3156+
let app = new PlaywrightFixture(appFixture, page);
3157+
await app.goto("/");
3158+
await app.clickLink("/a/b/c");
3159+
await page.waitForSelector("[data-error]");
3160+
expect(await page.locator("[data-error]").textContent()).toBe(
3161+
"A Error Boundary",
3162+
);
3163+
expect(await page.locator("pre").textContent()).toBe("C ERROR");
3164+
expect(await page.locator("p").textContent()).toBe("A LOADER");
3165+
3166+
appFixture.close();
3167+
});
3168+
29413169
test("still calls middleware for all matches on granular data requests", async ({
29423170
page,
29433171
}) => {

0 commit comments

Comments
 (0)