diff --git a/.changeset/wicked-avocados-fly.md b/.changeset/wicked-avocados-fly.md
new file mode 100644
index 00000000000..41b9e5a1b58
--- /dev/null
+++ b/.changeset/wicked-avocados-fly.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+Throw a semantically correct 405 `ErrorResponse` instead of just an `Error` when submitting to a route without an `action`
diff --git a/integration/action-test.ts b/integration/action-test.ts
index aeb54ad30e7..b3861f3a153 100644
--- a/integration/action-test.ts
+++ b/integration/action-test.ts
@@ -95,6 +95,18 @@ test.describe("actions", () => {
return
${PAGE_TEXT}
}
`,
+
+ "app/routes/no-action.tsx": js`
+ import { Form } from "@remix-run/react";
+
+ export default function Component() {
+ return (
+
+ );
+ }
+ `,
},
});
@@ -145,6 +157,22 @@ test.describe("actions", () => {
await page.waitForSelector(`#text:has-text("${SUBMITTED_VALUE}")`);
});
+ test("throws a 405 when no action exists", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto(`/no-action`);
+ await page.click("button[type=submit]");
+ await page.waitForSelector(`h1:has-text("405 Method Not Allowed")`);
+ expect(logs.length).toBe(2);
+ expect(logs[0]).toMatch('Route "routes/no-action" does not have an action');
+ // logs[1] is the raw ErrorResponse instance from the boundary but playwright
+ // seems to just log the name of the constructor, which in the minified code
+ // is meaningless so we don't bother asserting
+
+ // The rest of the tests in this suite assert no logs, so clear this out to
+ // avoid failures in afterEach
+ logs = [];
+ });
+
test("properly encodes form data for request.text() usage", async ({
page,
}) => {
diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts
index 26ca80a7f5b..11639ffaaed 100644
--- a/integration/resource-routes-test.ts
+++ b/integration/resource-routes-test.ts
@@ -247,7 +247,7 @@ test.describe("loader in an app", async () => {
await app.goto("/");
await app.clickSubmitButton("/no-action");
let html = await app.getHtml();
- expect(html).toMatch("Application Error");
+ expect(html).toMatch("405 Method Not Allowed");
expect(logs[0]).toContain(
'Route "routes/no-action" does not have an action'
);
diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx
index 87a40d4659c..84363794e1a 100644
--- a/packages/remix-react/routes.tsx
+++ b/packages/remix-react/routes.tsx
@@ -1,4 +1,5 @@
import * as React from "react";
+import { UNSAFE_ErrorResponseImpl as ErrorResponse } from "@remix-run/router";
import type {
DataRouteObject,
ShouldRevalidateFunction,
@@ -140,7 +141,9 @@ export function createClientRoutes(
`Route "${route.id}" does not have an action, but you are trying ` +
`to submit to it. To fix this, please add an \`action\` function to the route`;
console.error(msg);
- return Promise.reject(new Error(msg));
+ return Promise.reject(
+ new ErrorResponse(405, "Method Not Allowed", new Error(msg), true)
+ );
}
return fetchServerHandler(request, route);