denoland/deno · error

unexpected EOF while reading HTTP/1 body

Error message

unexpected EOF while reading HTTP/1 body

What it means

Deno's HTTP/1 implementation (libs/http_h1, backing fetch and HTTP handling): while draining a response body, if the connection reaches EOF while body parsing still reports Partial — Content-Length bytes unread, or a chunked body without its terminating chunk — the read fails with ErrorKind::UnexpectedEof via this helper instead of returning a short body silently.

Source

Thrown at libs/http_h1/conn.rs:1556

    consumed: usize,
  },
  Complete {
    consumed: usize,
  },
  Partial {
    consumed: usize,
  },
}

fn protocol_error(error: ProtocolError) -> Error {
  match error {
    ProtocolError::Parse(error) => Error::Parse(error),
    ProtocolError::HeadTooLarge => Error::HeadTooLarge,
  }
}

fn unexpected_eof() -> Error {
  Error::Io(io::Error::new(
    io::ErrorKind::UnexpectedEof,
    "unexpected EOF while reading HTTP/1 body",
  ))
}

fn body_status_from_buf(
  protocol: &mut Protocol,
  buf: &[u8],
) -> Result<ConnBodyStatus, Error> {
  let status = protocol.body_chunk(buf).map_err(protocol_error)?;
  Ok(match status {
    BodyStatus::Chunk { bytes, consumed } => ConnBodyStatus::Chunk {
      offset: consumed - bytes.len(),
      len: bytes.len(),
      consumed,
    },
    BodyStatus::Complete { consumed } => ConnBodyStatus::Complete { consumed },
    BodyStatus::Partial { consumed } => ConnBodyStatus::Partial { consumed },

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Retry idempotent requests (GET) with backoff — this class of failure is transient under keep-alive races.
  2. If you control the server, verify Content-Length computation and that the full body flushes before the handler returns; disable output-compression middleware that miscounts.
  3. Check intermediate proxies/LBs for response timeouts smaller than large transfers take.
  4. For streams that cannot be retried, read the body incrementally and persist progress so a truncated transfer resumes instead of restarting.

Example fix

// before
const res = await fetch(url);
const body = await res.arrayBuffer(); // may throw: unexpected EOF while reading HTTP/1 body

// after
async function fetchRetry(url, tries = 3) {
  for (let i = 0; ; i++) {
    try { const r = await fetch(url, { signal: AbortSignal.timeout(30_000) }); return await r.arrayBuffer(); }
    catch (e) {
      if (i === tries - 1 || !(e instanceof TypeError || /unexpected EOF/.test(String(e.cause ?? e)))) throw e;
      await new Promise(r => setTimeout(r, 2 ** i * 500));
    }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });
const declared = Number(res.headers.get("content-length") ?? -1);
if (declared === 0) throw new Error(`server declared empty body for ${url}`); // suspicious early-close setup

Try / catch

try { body = await res.arrayBuffer(); } catch (e) { if (/unexpected EOF|network/.test(String(e.cause ?? e))) { await sleep(backoff(i)); return fetchRetry(url, i + 1); } throw e; }

Prevention

When it happens

Trigger: A server or proxy closes the keep-alive connection before the promised HTTP/1 body is complete: Content-Length larger than the actual bytes sent, chunked stream cut before the 0-chunk terminator, mid-response timeouts, or connection races where a reused socket is closed server-side right as a response streams.

Common situations: Misbehaving backends that send wrong Content-Length; CDN/proxy idle timeouts cutting long responses; keep-alive reuse races against server-side reaping; debugging tools closing sockets; mobile networks dropping mid-download.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/f398fa79f629748b. Report an issue: GitHub.