[🐞] Native form POST to a routeAction$ returns 500 in a production build when a layout has a routeLoader$ (rc.0)
Maintainers usually reply within 1 day
@akasakariko is already working on this.
Since Oct 18, 2026.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript, vite
Research direction
Start in packages/qwik-router/src/runtime/src/route-loaders.ts at getLoaderRequestEvent, especially the Request construction around line 975, and trace how a native action POST reaches the root layout loader. Check the existing route-loader tests for coverage of POST requests with a consumed body. Done means the production build renders the page with status 200 after a native form POST, while the action's side effect remains saved.
Written by the indexing model from the issue text.
Description
Which component is affected?
Qwik Router
Describe the bug
On 2.0.0-rc.0, a native form POST to a routeAction$ (no JS, or the user submits before the Qwik loader has run) returns 500 Internal Server Error in a production build.
The action itself runs and its side effect is saved. The 500 comes afterwards, while the page is rendered. So the user sees an error page for a write that succeeded, and is likely to submit again.
Expected: the page re-renders with status 200, as the starter's todo demo promises ("This little app works even when JavaScript is disabled").
It happens with the unmodified playground starter.
Request to /demo/todolist/?qaction=<id> |
vite preview (build) |
vite --mode ssr (dev) |
|---|---|---|
POST, Accept: text/html (native form submit) |
500, item is saved | 200 |
POST, Accept: application/json (what the client runtime sends) |
200 | 200 |
Also confirmed in Chrome with JavaScript disabled: submit shows the 500 page, and after a reload the item is in the list.
Reproduction
Stock starter, no changes: npm create qwik@rc playground ./repro (route /demo/todolist/)
Steps to reproduce
npm create qwik@rc playground ./repro
cd repro
npm install
npx qwik build preview
npx vite preview --port 6190
Then, in a second terminal:
# 1. read the action id from the form's action attribute
curl -s http://localhost:6190/demo/todolist/ | grep -o 'qaction=[A-Za-z0-9]*' | head -1
# 2. native form POST -> 500
curl -i -X POST "http://localhost:6190/demo/todolist/?qaction=<id>" \
-H "Accept: text/html" \
-H "Origin: http://localhost:6190" \
--data "text=hello"
# 3. the item was saved anyway
curl -s http://localhost:6190/demo/todolist/ | grep -o hello
Or in a browser: open /demo/todolist/, disable JavaScript in DevTools, reload, add an item.
System Info
System:
OS: macOS 27.0
CPU: (10) arm64 Apple M1 Pro
Shell: 5.9 - /bin/zsh
Binaries:
Node: 25.2.1
npm: 11.6.2
pnpm: 10.14.0
Browsers:
Chrome: 154.0.8037.93
Safari: 27.0
npmPackages:
@qwik.dev/core: ^2.0.0-rc.0 => 2.0.0-rc.0
@qwik.dev/router: ^2.0.0-rc.0 => 2.0.0-rc.0
typescript: 5.9.3 => 5.9.3
vite: 8.2.1 => 8.2.1
Additional Information
Cause. getLoaderRequestEvent builds a new Request from the root request when a loader's path or search differs from the page's:
const request = new Request(url, rootRequestEv.request);
On a native action POST the action has already read the body, so rootRequestEv.request.bodyUsed is true and the constructor throws.
I wrapped the global Request constructor to log the failing call in the starter's preview build:
new Request threw: TypeError: Response body object should not be disturbed or locked
new url: http://localhost:6192/?qaction=tCQ608gnjnQ
source request: POST http://localhost:6192/demo/todolist/?qaction=tCQ608gnjnQ bodyUsed=true
at extractBody (node:internal/deps/undici/undici)
at new Request (node:internal/deps/undici/undici)
at ki (server/build/q-ClbfaIuI.js:471:3853)
at Ri (server/build/q-ClbfaIuI.js:471:5870)
at server/entry.preview.js:1:20984
The new URL has pathname /, so the failing call is for the root layout's loader (useServerTimeLoader in src/routes/layout.tsx), whose path differs from the page's /demo/todolist/. Any page under a layout that has a routeLoader$ looks affected.
Not Node-specific. We first hit this in a Cloudflare Workers build of our own app, where workerd reports TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.
Regression. 2.0.0-beta.37 does not contain this line, and the same request returned 200 there in our app.
Possible fix. Don't pass the consumed body when creating the loader's request (copy the headers and signal only, or skip the body when bodyUsed is true). A loader should not need the action's body.
- Dominant language
- TypeScript
- Stars
- 22.1k
- Forks
- 1.4k
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 123
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from QwikDev/qwik
-
[🐞] Production SSR render errors are never logged or passed to `next()` (node middleware returns on `headersSent` first)Possibly taken @bunnysayzz claimed this 16 days ago. OpenIMPORTANT
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
QwikDev/qwik#9191 · 6 comments ·
Maintainers usually reply within 1 day
-
docs router V2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
DX enhancement V2
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
QwikDev/qwik#8480 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
QwikDev/qwik#9310 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 1-2 days Newbie friendliness 63/100
QwikDev/qwik#9308 · 1 comment ·
Maintainers usually reply within 1 day
Similar issues
-
area:desktop area:ui bug platform:macos
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
anthropics/claude-code#106183 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
feature:App-Tour feature:Chat ready Web
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
digitalfabrik/integreat-app#4523 · 2 reactions ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Required Lexical field accepts two empty paragraphsPossibly taken @lazerg claimed this today. OpenBug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
payloadcms/payload#19222 ·
Maintainers usually reply within 1 day