HarperFast / HarperFast/harper
Response.redirect() throws TypeError: immutable — response pipeline mutates headers after handler returns
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
`Response.redirect(url, status)` constructs a `Response` whose `Headers` object has its guard set to `"immutable"` per the Fetch spec. Harper's own HTTP response pipeline mutates the handler's returned `Response`'s headers *after* the handler returns (e.g. to add CORS/instrumentation headers) — against an immutable-guarded `Headers` this throws `TypeError: immutable`, turning the request into a 500.
This fires on **any** resource `post()`/`get()` handler that returns `Response.redirect(...)` — not an edge case, the standard-library redirect constructor itself is unusable.
## Observed
Found while writing the first real (non-mocked, live-Harper) integration test for an OAuth authorize/deny flow. A minimal resource:
```js
import { Resource } from "@harperfast/harper";
export class Redirect extends Resource {
async post() {
return Response.redirect("https://example.com/callback?code=abc", 302);
}
}
```
`curl -X POST http://localhost:9926/Redirect` against an unmodified Harper 5.1.15 origin/build returns a 500 with `TypeError: immutable` in the logs, instead of a 302.
## Workaround (currently carried, would like to retire)
Construct the `Response` manually instead of via the spec's redirect helper — same wire bytes, but the default (mutable) `"response"` Headers guard, so Harper's later header mutation succeeds:
```js
function redirectTo(url, status = 302) {
return new Response(null, { status, headers: { Location: url } });
}
```
Identical behavior on the wire; it just avoids the immutable guard `Response.redirect()` sets. Every consumer of the Fetch-standard constructor has to know to avoid it, which isn't discoverable until it 500s in production.
## Expected
Harper's response pipeline should not mutate a `Response`'s `Headers` when its guard is `immutable` — either clone the `Headers` before mutating (spec-compliant `Response.redirect()` stays usable), or honor the guard and skip the mutation for that response. A `Response.redirect()` return from any resource handler should behave identically to a hand-built 3xx Response.
## Environment
- `@harperfast/harper` 5.1.15 (npm), reproduced via native Node (not Docker)
- macOS arm64, Node v25.9.0
## Related
Relates to the #1660 epic (HTTP response security hardening / headers). Also touches REST HTTP-semantics more broadly (#1682).
Contributor guide
Research direction
Start with the live Harper integration reproduction: run the POST request against a resource handler returning Response.redirect(), then trace the response pipeline's post-handler header mutation. Done means the redirect returns HTTP 302 rather than a 500, and the pipeline handles the immutable Headers guard without breaking the expected response headers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100