Skip to content

HTTP/2: request hangs when the server sends a final response to an Expect: 100-continue request #5465

Description

@jeswr

Bug Description

Over HTTP/2, when a server answers a request carrying Expect: 100-continue with a final response (e.g. 401) instead of 100 (Continue), the request never resolves. undici correctly withholds the request body, but the response is never delivered and the request hangs.

RFC 9110 §10.1.1 permits a server to send a final status determinable from the header fields — an "immediate success, redirect, or error response" — in place of 100 (Continue); the client must then deliver that response and not send the body. The common cases are an early 401/413/417 (and equally a 2xx). undici delivers nothing.

This is HTTP/2-only; HTTP/1.x handles it.

Root cause: in lib/dispatcher/client-h2.js, the request-body write is bound to the stream's 'continue' event. On an early final response, 'continue' never fires, so the client never sends END_STREAM; the stream stays half-open, its 'close' event never fires, and onRequestStreamClose (which finalizes the response) never runs.

Reproducible By

Minimal repro: https://github.com/jeswr/undici-h2-expect-continue-bug (npm install && npm startFAIL: request hung ... after 5s). Condensed:

const http2 = require('node:http2')
const { Client } = require('undici')

// h2 server: answer 401 from the headers alone, no 100 (Continue)
const server = http2.createSecureServer({ key, cert })
server.on('stream', (stream) => {
  stream.respond({ ':status': 401 })
  stream.end('denied')
})
server.listen(0, async () => {
  const client = new Client(`https://localhost:${server.address().port}`, {
    allowH2: true, connect: { rejectUnauthorized: false }
  })
  // hangs — the 401 is never delivered
  const res = await client.request({
    path: '/', method: 'POST', body: 'x'.repeat(1024), expectContinue: true
  })
  console.log(res.statusCode)
})

Expected Behavior

undici delivers the early final response (e.g. 401) and completes the request without sending the body, as it does over HTTP/1.x and as RFC 9110 §10.1.1 requires.

Environment

  • undici 8.5.0
  • Node.js v25.1.0
  • HTTP/2 (allowH2: true); request with expectContinue: true and a body

I have a candidate fix (close the request stream's writable half on an early final response) with a test; happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions