diff --git a/.changeset/mighty-grapes-laugh.md b/.changeset/mighty-grapes-laugh.md new file mode 100644 index 00000000000..2ed960e791b --- /dev/null +++ b/.changeset/mighty-grapes-laugh.md @@ -0,0 +1,6 @@ +--- +"@remix-run/server-runtime": patch +--- + +- Ensure `loader` `request`'s proxy headers on document `action` submissions +- Fix an issue where loader `request`'s reflected `method: "POST"` on document submissions diff --git a/.changeset/sharp-paws-nail.md b/.changeset/sharp-paws-nail.md new file mode 100644 index 00000000000..66ec7a06229 --- /dev/null +++ b/.changeset/sharp-paws-nail.md @@ -0,0 +1,5 @@ +--- +"@remix-run/server-runtime": patch +--- + +Fix error boundary tracking for multiple errors bubbling to the same boundary diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index c31d61bfcde..37ccb0a680c 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -67,6 +67,9 @@ test.describe("CatchBoundary", () => { ${HAS_BOUNDARY_LOADER} + + ${HAS_BOUNDARY_LOADER}/child + ${NO_BOUNDARY_LOADER} @@ -111,11 +114,27 @@ test.describe("CatchBoundary", () => { `, [`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: js` + import { useCatch } from '@remix-run/react'; export function loader() { throw new Response("", { status: 401 }) } export function CatchBoundary() { - return
${OWN_BOUNDARY_TEXT}
+ let caught = useCatch(); + return ( + <> +
${OWN_BOUNDARY_TEXT}
+
{caught.status}
+ + ); + } + export default function Index() { + return
+ } + `, + + [`app/routes${HAS_BOUNDARY_LOADER}/child.jsx`]: js` + export function loader() { + throw new Response("", { status: 404 }) } export default function Index() { return
@@ -292,4 +311,23 @@ test.describe("CatchBoundary", () => { expect(await app.getHtml("#child-catch")).toMatch("400"); expect(await app.getHtml("#child-catch")).toMatch("Caught!"); }); + + test("prefers parent catch when child loader also bubbles, document request", async () => { + let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); + expect(res.status).toBe(401); + let text = await res.text(); + expect(text).toMatch(OWN_BOUNDARY_TEXT); + expect(text).toMatch('
401
'); + }); + + test("prefers parent catch when child loader also bubbles, client transition", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); + await page.waitForSelector("#boundary-loader"); + expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); + expect(await app.getHtml("#status")).toMatch("401"); + }); }); diff --git a/integration/request-test.ts b/integration/request-test.ts new file mode 100644 index 00000000000..db8c47ec5a7 --- /dev/null +++ b/integration/request-test.ts @@ -0,0 +1,162 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; + +let fixture: Fixture; +let appFixture: AppFixture; + +test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/routes/index.jsx": js` + import { json } from "@remix-run/node"; + import { Form, useLoaderData, useActionData } from "@remix-run/react"; + export async function loader({ request }) { + let text = (await request.text()); + return json({ + method: request.method, + url: request.url, + headers: Object.fromEntries(request.headers.entries()), + text, + }); + } + export async function action({ request }) { + let text = (await request.text()); + return json({ + method: request.method, + url: request.url, + headers: Object.fromEntries(request.headers.entries()), + text, + }); + } + export default function Index() { + let loaderData = useLoaderData(); + let actionData = useActionData(); + return ( +
+ +
+
+ ) + } + `, + }, + }); + + appFixture = await createAppFixture(fixture); +}); + +test.afterAll(() => appFixture.close()); + +test("loader request on SSR GET requests", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickElement("#set-cookie"); + + let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual(undefined); + expect(loaderData.text).toEqual(""); + + await app.clickElement("#submit-get-ssr"); + + loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=ssr$/); + expect(loaderData.headers.cookie).toEqual("cookie=nomnom"); + expect(loaderData.text).toEqual(""); +}); + +test("loader request on CSR GET requests", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickElement("#set-cookie"); + + let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual(undefined); + expect(loaderData.text).toEqual(""); + + await app.clickElement("#submit-get-csr"); + + loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=csr$/); + expect(loaderData.headers.cookie).toEqual("cookie=nomnom"); + expect(loaderData.text).toEqual(""); +}); + +test("action + loader requests SSR POST requests", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickElement("#set-cookie"); + + let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual(undefined); + expect(loaderData.text).toEqual(""); + + await app.clickElement("#submit-post-ssr"); + + let actionData = JSON.parse(await page.locator("#action-data").innerHTML()); + expect(actionData.method).toEqual("POST"); + expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(actionData.headers.cookie).toEqual("cookie=nomnom"); + expect(actionData.text).toEqual("type=ssr"); + + loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual("cookie=nomnom"); + expect(loaderData.text).toEqual(""); +}); + +test("action + loader requests on CSR POST requests", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickElement("#set-cookie"); + + let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual(undefined); + expect(loaderData.text).toEqual(""); + + await app.clickElement("#submit-post-csr"); + + let actionData = JSON.parse(await page.locator("#action-data").innerHTML()); + expect(actionData.method).toEqual("POST"); + expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(actionData.headers.cookie).toEqual("cookie=nomnom"); + expect(actionData.text).toEqual("type=csr"); + + loaderData = JSON.parse(await page.locator("#loader-data").innerHTML()); + expect(loaderData.method).toEqual("GET"); + expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/); + expect(loaderData.headers.cookie).toEqual("cookie=nomnom"); + expect(loaderData.text).toEqual(""); +}); diff --git a/packages/remix-react/package.json b/packages/remix-react/package.json index 815d0c448e8..5332bd0a8d5 100644 --- a/packages/remix-react/package.json +++ b/packages/remix-react/package.json @@ -16,8 +16,8 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "1.0.5", - "react-router-dom": "6.4.5" + "@remix-run/router": "1.1.0-pre.0", + "react-router-dom": "6.5.0-pre.0" }, "devDependencies": { "@remix-run/server-runtime": "1.8.2", diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 65601f6b0f2..dfc12276571 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -16,7 +16,7 @@ "typings": "dist/index.d.ts", "module": "dist/esm/index.js", "dependencies": { - "@remix-run/router": "1.0.5", + "@remix-run/router": "1.1.0-pre.0", "@types/cookie": "^0.4.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.4.1", diff --git a/yarn.lock b/yarn.lock index 0bc88e5bac6..bcd64daba9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2174,6 +2174,11 @@ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.0.5.tgz#d5c65626add4c3c185a89aa5bd38b1e42daec075" integrity sha512-my0Mycd+jruq/1lQuO5LBB6WTlL/e8DTCYWp44DfMTDcXz8DcTlgF0ISaLsGewt+ctHN+yA8xMq3q/N7uWJPug== +"@remix-run/router@1.1.0-pre.0": + version "1.1.0-pre.0" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.1.0-pre.0.tgz#c33db6bd13c6d3847f73f618008ebecdaec237c7" + integrity sha512-FFa1aGAYUZm+PdOO2a7r/4gxLqSIOq1Qav3g6wwRgWj28PNyO7CjdUNER4O4DSKVVyob21mhZHvTe25z+2xnZw== + "@remix-run/web-blob@^3.0.3", "@remix-run/web-blob@^3.0.4": version "3.0.4" resolved "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.0.4.tgz" @@ -10722,20 +10727,20 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-router-dom@6.4.5: - version "6.4.5" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.5.tgz#4fdb12efef4f3848c693a76afbeaed1f6ca02047" - integrity sha512-a7HsgikBR0wNfroBHcZUCd9+mLRqZS8R5U1Z1mzLWxFXEkUT3vR1XXmSIVoVpxVX8Bar0nQYYYc9Yipq8dWwAA== +react-router-dom@6.5.0-pre.0: + version "6.5.0-pre.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.5.0-pre.0.tgz#73dc3861f43b1bc83a854bd5d3d3ab4d7beb1710" + integrity sha512-Hq8lhsAqLH4LDewj6OeDCiEel5RP6El1DsxohxwgOctt+xq4Q6kGiiuVnLA+eHl2wXFCEF8ZSK/kOddGKfLWYA== dependencies: - "@remix-run/router" "1.0.5" - react-router "6.4.5" + "@remix-run/router" "1.1.0-pre.0" + react-router "6.5.0-pre.0" -react-router@6.4.5: - version "6.4.5" - resolved "https://registry.npmjs.org/react-router/-/react-router-6.4.5.tgz#73f382af2c8b9a86d74e761a7c5fc3ce7cb0024d" - integrity sha512-1RQJ8bM70YEumHIlNUYc6mFfUDoWa5EgPDenK/fq0bxD8DYpQUi/S6Zoft+9DBrh2xmtg92N5HMAJgGWDhKJ5Q== +react-router@6.5.0-pre.0: + version "6.5.0-pre.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.5.0-pre.0.tgz#513b2fe858736443082a33a7a9d2f728b2bd7fe4" + integrity sha512-JqRGYSGzySjcqVMkgCa+1i3zvXiH2IHUfEPac71sJn5ArH5tvR5GmraWUn+D5t7Yzq2dz11is4u2SzHiT0pGdg== dependencies: - "@remix-run/router" "1.0.5" + "@remix-run/router" "1.1.0-pre.0" react@^18.2.0: version "18.2.0"