Fetcher submission to an undiscovered route hangs in `submitting` forever (no error, no timeout) when the /__manifest request stalls
Author: wra-solCreated Aug 17, 2026Updated Aug 20, 2026
Labelspkg:react-router
### I'm using React Router as a...
Framework
### Reproduction
Minimal framework-mode app (all files below): a resource route (`action` only, never linked, so it is not in the initial client manifest) and a page that `fetcher.submit()`s to it. An express middleware in front of the request handler simulates the `/__manifest` route-discovery request stalling (receiving no response and no error).
Steps:
1. `npm install && npm run build`
2. `STALL_MANIFEST=1 node server.mjs`, open http://localhost:3999
3. Click **Submit to /api/thing**
Observed (verified on 8.3.0): `fetcher.state` stays `"submitting"` **forever**. No error state, no rejection, nothing in the console, no timeout — the page just shows a pending submission indefinitely. Clicking again fires a fresh `/__manifest` fetch and a second fetcher pins the same way.
Control: with `STALL_MANIFEST=0` the same click settles to `idle` with the action data immediately.
Repro files (6 small files)
**package.json**
```json
{
"name": "rr-fetcher-discovery-hang-repro",
"private": true,
"type": "module",
"scripts": { "build": "react-router build", "start": "node server.mjs" },
"dependencies": {
"@react-router/express": "8.3.0",
"@react-router/node": "8.3.0",
"express": "^5.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "8.3.0"
},
"devDependencies": { "@react-router/dev": "8.3.0", "vite": "^7.0.0" }
}
```
**react-router.config.ts**
```ts
import type { Config } from "@react-router/dev/config";
export default { ssr: true } satisfies Config;
```
**vite.config.ts**
```ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [reactRouter()] });
```
**app/routes.ts**
```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("api/thing", "routes/api.thing.tsx"),
] satisfies RouteConfig;
```
**app/root.tsx**
```tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
export default function Root() {
return (
);
}
```
**app/routes/api.thing.tsx**
```tsx
export async function action() {
return Response.json({ ok: true });
}
```
**app/routes/home.tsx**
```tsx
import { useFetcher } from "react-router";
export default function Home() {
const fetcher = useFetcher<{ ok: boolean }>();
return (
fetcher.submit({}, { action: "/api/thing", method: "POST" })}>
Submit to /api/thing
fetcher.state: {fetcher.state}
fetcher.data: {JSON.stringify(fetcher.data ?? null)}
); } ``` **server.mjs** ```js import { createRequestHandler } from "@react-router/express"; import express from "express"; const STALL = process.env.STALL_MANIFEST === "1"; const app = express(); app.use((req, _res, next) => { if (STALL && req.url.startsWith("/__manifest")) return; // never respond next(); }); app.use(express.static("build/client")); app.all("*splat", createRequestHandler({ build: await import("./build/server/index.js") })); app.listen(3999, () => console.log("repro on http://localhost:3999")); ``` ### System Info ``` react-router: 8.3.0 (also present on current `main` — fog-of-war.ts has no timeout path) @react-router/dev: 8.3.0 node: 25.x, macOS; browser: Chromium ``` ### Expected Behavior Some way for the app (or the user) to learn the submission is not going to happen — e.g. the discovery fetch aborts after a timeout and the fetcher settles into an error state that `useFetcher().data`/an ErrorBoundary can observe, the same way a failed action POST surfaces. ### Actual Behavior When `fetcher.submit()` targets a route not yet in the client manifest, `checkFogOfWar`/`fetchAndApplyManifestPatches` awaits `fetch('/__manifest?...')` with only the navigation-interruption `AbortSignal` — there is no timeout. If that request stalls (never resolves, never errors), the fetcher is pinned in `"submitting"` permanently and silently: no error state, no console output, no way for app code to detect or recover. Each retry click spawns another fetcher that re-runs discovery and pins the same way. Browsers apply no default fetch timeout, so nothing ever rescues it. How we hit this in a real app, for context: the page held several long-lived SSE (`EventSource`) connections to the same origin. Over HTTP/1.1 (localhost dev), those consumed all six per-origin connections, so the browser queued the `/__manifest` request indefinitely — same URL via `curl` answered in 11 ms. The result was "clicking the button does nothing, forever, with an empty console", which took a full debugging session to attribute because the failure has no observable signal anywhere. The express stall in the repro is just a deterministic stand-in for that queuing. We fixed our trigger (share one SSE connection), but the failure mode seems worth hardening: a stalled discovery request is indistinguishable from the app being healthy.Source: remix-run/react-router