#5383·hono

A failed c.req.formData() makes all other body methods throw

Author: xia-chaoCreated Sep 13, 2026Updated Sep 13, 2026

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:

typescript
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:

typescript
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 — the first call, bodyCache is still empty, key = "formData"

src/request.ts:243return (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 — key = "text" but anyCachedKey = "formData", and the cached promise is rejected

src/request.ts:226return (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:

debug console — FORM_DATA_REJECTED, then JSON_REJECTED, then STATUS 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():

typescript
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 kept

The same request, going through c.req.formData() instead:

typescript
await req.formData() // throws, as expected
await req.text()     // throws as well — this is the bug

What do you see instead?

TypeError: Can't decode form data from body because of incorrect MIME type/boundary

for 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.