A failed c.req.formData() makes all other body methods throw
What version of Hono are you using?
4.13.7 (main, edd138ee)
What runtime/platform is your app running on? (with version if possible)
Reproduced on all three, same result: Node.js 26 (@hono/node-server), Bun 1.4.3, Deno 2.9.6.
What steps can reproduce the bug?
When c.req.formData() fails, the failure does not stay local to that call — every other body representation of the same request throws afterwards.
The shortest reproduction is a request whose body is not a form at all:
import { Hono } from 'hono'
const app = new Hono()
app.post('/', async (c) => {
try {
await c.req.formData() // JSON body, so this throws
} catch {}
const body = await c.req.json() // throws too
return c.json({ body })
})
await app.request('/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"event":"push"}',
})The shape that made me notice it is trying one parser and falling back to another — which is the entire point of the try/catch:
let body
try {
body = await c.req.formData()
} catch {
body = await c.req.json() // never works
}Tracing it with the debugger
1. The first call, c.req.formData() on a Content-Type: application/json request. bodyCache is still empty, and line 243 stores raw.formData()'s promise into it unconditionally — including when that promise is about to reject.

src/request.ts:243 — return (bodyCache[key] = raw[key]()). Variables: key = "formData", bodyCache = {}, cachedBody = undefined. Watch: "formData requested | cached: []". Call stack: #cachedBody:243 ← formData:335 ← <anonymous> debug-src.ts:19.
2. The second call comes from the catch above: c.req.json() asks for text. The direct cache lookup misses, so it drops into the fallback loop — and the only entry there is the already-rejected formData promise, which line 226 hands straight back:

src/request.ts:226 — return (bodyCache[anyCachedKey as keyof Body] as Promise<BodyInit>).then(...). Variables: key = "text", anyCachedKey = "formData", bodyCache = {formData: Promise}, cachedBody = undefined.
bodyCache.formData
= Promise { result: TypeError [ERR_FORMDATA_PARSE_ERROR]:
Can't decode form data from body because of incorrect MIME type/boundary,
status: "rejected" }Watch: "text requested | cached: [formData]". Call stack: #cachedBody:226 ← json:259 ← <anonymous> debug-src.ts:27 — the json frame is the catch fallback.
3. The fallback therefore rejects with the first parser's error, and the request ends as a 500:

Debugger attached.
FORM_DATA_REJECTED: TypeError
JSON_REJECTED: TypeError | Can't decode form data from body because of incorrect MIME type/boundary
STATUS: 500
RESULT: {"ok":false,"error":"Can't decode form data from body because of incorrect MIME type/boundary"}
Debugger detached.What is the expected behavior?
A failed c.req.formData() should fail only that call. The other representations should still be readable, exactly as they already are when the body is read with c.req.parseBody():
const req = new HonoRequest(
new Request('http://localhost', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"event":"push"}',
})
)
await req.parseBody()
await req.text() // works today — the bytes were keptThe same request, going through c.req.formData() instead:
await req.formData() // throws, as expected
await req.text() // throws as well — this is the bugWhat do you see instead?
TypeError: Can't decode form data from body because of incorrect MIME type/boundaryfor text(), json(), arrayBuffer(), bytes() and blob() — all five, after a single failed formData().
Additional information
c.req.parseBody() does not have this problem because it reads the bytes first and parses them afterwards, so the raw body survives a failed parse (src/utils/body.ts, parseFormData()). c.req.formData() goes straight to raw.formData(), which consumes the stream and caches only the result — a rejected promise when the media type does not match.
On Node this is worse than it looks: undici marks the body as used even when it rejects before parsing, so raw.text() is no longer possible either (Body is unusable: Body has already been read). Bun happens not to mark it used, so the two runtimes disagree on how recoverable the request is. The cache is what both share.
Related: #4806 / #4807 fixed the same class of problem for parseBody(), where a cached entry made later text() / json() calls throw.
Source: honojs/hono