Hono formData() after text() silently corrupts binary file uploads
Since hono#5366, calling formData() after text() silently alters binary uploads. Warpway flagged it in a replay of the PR; we confirmed it and filed #5539.
Published 3 min readReplayed through Warpway on By the Warpway team
Replay vs. what happened
Warpway replay
Flags it, given only what existed before merge
First report (#5539, filed by us)
25 days after merge
Flagged: Binary file uploads silently altered when text() is read first
Replayed on 2026-10-07. Warpway was not installed on honojs/hono when this pull request was open.
Hono caches each body representation a handler reads (text(), json(), arrayBuffer(), formData() and so on) and converts between them when the body is read again in a different way. In September 2026, honojs/hono#5366 fixed a gap in that conversion. Rebuilding the body through a bare Response dropped the request's media type, so calling formData() after text() threw. The fix carries the original Content-Type over.
We included this pull request in our backtest as a control: no later fix or revert cites it. When we replayed it through Warpway on 2026-10-07, using only what existed before merge, Warpway flagged it anyway, and the finding turned out to be a real bug that nobody had reported. We reproduced it on the latest release and filed it upstream as honojs/hono#5539.
Key facts
- Pull request
- honojs/hono#5366
- Merged
- 2026-09-12
- Shipped in
- 4.13.8
- Affected
- 4.13.8 to 4.13.13 (latest)
- Status
- Open: honojs/hono#5539
- Language
- TypeScript
What Warpway flags in the replay
- CorrectnessIssue found
- SecurityIssue found
- Testing / ReliabilityIssue found
- Architecture / MaintainabilityIssue found
- Backward CompatibilityIssue found
- PerformanceCleared
- Product SemanticsIssue found
- HighArchitecture / Maintainabilitysrc/request.ts:235
Multipart files are corrupted when formData() follows text()
For a multipart request containing a file byte such as 0xFF, calling req.text() first decodes the entire body as UTF-8 and replaces that invalid byte with U+FFFD. The new path then rebuilds a Response from that string and the original multipart Content-Type, so req.formData() succeeds but the resulting File contains the UTF-8 bytes EF BF BD rather than the uploaded 0xFF. Calling formData() first reads the original request bytes. This violates the change's stated goal that cached-body reuse behave like parsing the same request directly, and silently changes uploaded data.
Warpway saw the pull request as it stood before merge: final code, title, description and commits, without review comments or later history. Findings are quoted verbatim from the recorded review. Several lenses can report the same issue; Warpway merges them, and the first paragraph is shown.
The change
const contentType = anyCachedKey === 'formData' ? undefined : raw.headers.get('content-type');
return new Response(body, {
headers: contentType ? { 'Content-Type': contentType } : undefined,
})[key]();How the bug works
When a handler calls c.req.text(), the runtime decodes the whole request body as UTF-8 and Hono caches the resulting string. A multipart upload is not valid UTF-8 when it contains binary file parts, so every invalid byte becomes the replacement character U+FFFD during decoding. That information is gone.
Before #5366, a later formData() call rebuilt a Response from that string without a media type, and the runtime refused to parse it. After #5366, the original multipart/form-data header is carried over, so parsing succeeds, but on the damaged text. The file comes back with EF BF BD (U+FFFD encoded as UTF-8) wherever the upload had a byte such as 0xFF. Nothing throws, so nothing tells you.
Reproduce it
We independently repeated the reproduction with Hono 4.13.13 and Node.js 24 on October 8, 2026. The server returned HTTP 200, with uploaded bytes [255, 0, 65] changed to [239, 191, 189, 0, 65]. The model review identified the scenario through repository investigation; this runtime check separately confirmed the byte transformation.
- Original recorded review JSON
- Reviewed PR and pinned commits
- Recorded models and review configuration
- Reproduction output, version and timestamp
- Download the reproduction script
To repeat the check, use an empty directory with Node.js 24, install hono@4.13.13, save the downloaded script as reproduce.mjs, and run node reproduce.mjs.
import { Hono } from 'hono';
const app = new Hono();
app.post('/', async (c) => {
await c.req.text();
const file = (await c.req.formData()).get('file');
return c.json([...new Uint8Array(await file.arrayBuffer())]);
});
const fd = new FormData();
fd.append('file', new Blob([new Uint8Array([0xff, 0x00, 0x41])]), 'x.bin');
console.log(await (await app.request('/', { method: 'POST', body: fd })).text());| Hono version | Result |
|---|---|
| 4.13.7, before #5366 | formData() throws, and the request fails with a 500 |
| 4.13.8 to 4.13.13 | [239,191,189,0,65]: the request succeeds, but 0xff arrived as EF BF BD |
Are you affected?
You are affected on Hono 4.13.8 or later if something reads a multipart request as text before your handler parses the form. Typical cases are middleware that logs or hashes the raw body, and webhook handlers that verify a signature over the body text. Uploads containing only text survive, which is why tests with ASCII form fields pass.
Workaround
Read the raw bytes first and decode them yourself. Hono then rebuilds the form from the original bytes, and file contents are preserved. We verified this on 4.13.13:
app.post('/upload', async (c) => {
const bytes = new Uint8Array(await c.req.arrayBuffer());
const text = new TextDecoder().decode(bytes); // for logging or signatures
const form = await c.req.formData(); // file bytes intact
// ...
});What happened next
Opened
#5366 opened
Merged
Release
Hono 4.13.8 ships the change
Warpway replay
Warpway replay: given the pull request as it stood before merge, one high-severity finding, raised by six lenses
Frequently asked questions
Which Hono versions are affected?
Hono 4.13.8 through 4.13.13, the latest release at the time of writing, and the main branch. Before 4.13.8, the same calls threw a TypeError instead of returning altered bytes.
When does the corruption happen?
Only when a multipart request containing binary data is read with c.req.text() (or json()) first and c.req.formData() afterwards, for example by middleware that reads the raw body. Reading formData() first, or reading only formData(), is not affected.
How do I work around it?
Read the bytes with c.req.arrayBuffer() first, decode them with TextDecoder when you need the text, then call c.req.formData(). Hono then rebuilds the form from the original bytes and file contents are preserved. We verified this on 4.13.13.
Is this a security vulnerability?
We treat it as a data-integrity bug: uploads are silently altered. It is public in the Hono issue tracker as #5539.
How we ran this
- 01
Replay the pull request as it stood before merge
Its final code, title, description and commits, and the repository at the fork point. No review comments, no bug reports, nothing after merge.
- 02
Review with Warpway’s default lenses
The production role models, lenses and default policy, run through Codex subscription access. One run per pull request, with no repository configuration.
- 03
Compare with what actually happened
Later fixes, reverts and bug reports in the public history, linked from every page so you can check our reading.
This pull request is one of 12 we replayed on 2026-10-07 from hono, axios and requests, including ones that were never fixed or reverted and the cases Warpway got wrong. Warpway was not installed on these repositories at the time; every review here is a replay. Older pull requests may appear in the model's training data, which is one reason we include recent ones and a bug nobody had reported. See every result from the pilot.
These replays used Codex subscription access rather than the production OpenAI API. They establish the recorded findings, not production API cost, general accuracy, customer time savings or an approval rate. The projects are open-source examples, not Warpway customers.
More case studies
- psf/requests #6667Python
requests 2.32 SSLContext regression: the wrong mTLS client certificate
- Warpway flags in a replay
- Client certificates leaking across requests through one shared SSLContext
- What happened
- Reported by users from 7 days after merge; reverted a year later in 2.32.5
- honojs/hono #5133TypeScript
Hono cloneRawRequest: stale Content-Length after multipart re-encoding
- Warpway flags in a replay
- Forwarded multipart clones keep a Content-Length that no longer matches the body
- What happened
- Found independently 36 days after merge; fixed in 4.13.4
- honojs/hono #4707TypeScript
Hono 4.12.0: c.json(undefined) throws "Value is not JSON serializable"
- Warpway flags in a replay
- c.json(undefined) throws on the new fast path instead of returning a response
- What happened
- Reported 4 days after merge; fast path reverted in 4.12.2