Skip to content

Content-Range parser rejects valid unknown-size byte ranges #5119

@trivikr

Description

@trivikr

Bug Description

Undici’s Content-Range parser rejects valid RFC 9110 byte range headers where the complete representation length is unknown, such as:

Content-Range: bytes 0-499/*

Reproducible By

import { equal } from 'node:assert/strict'
import { once } from 'node:events'
import { createServer } from 'node:http'

import { Client, RetryHandler } from 'undici'

let requestCount = 0
const chunks = []

const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
  requestCount++

  if (requestCount === 1) {
    equal(req.headers.range, 'bytes=0-3')
    res.setHeader('etag', '"same"')
    res.write('abc')
    setTimeout(() => res.destroy(), 50)
    return
  }

  equal(req.headers.range, 'bytes=3-')
  res.writeHead(206, {
    etag: '"same"',
    'content-range': 'bytes 3-5/*'
  })
  res.end('def')
})

server.listen(0)
await once(server, 'listening')

const client = new Client(`http://localhost:${server.address().port}`)

try {
  const { promise, resolve, reject } = Promise.withResolvers();
  const handler = new RetryHandler({
    method: 'GET',
    path: '/',
    headers: {},
    retryOptions: {
      minTimeout: 1,
      retry: (err, _ctx, done) => {
        if (err.message === 'Content-Range mismatch') {
          done(err)
          return
        }

        done(null)
      }
    }
  }, {
    dispatch: client.dispatch.bind(client),
    handler: {
      onResponseStart () {
        return true
      },
      onResponseData (_controller, chunk) {
        chunks.push(chunk)
        return true
      },
      onResponseEnd () {
        resolve()
      },
      onResponseError (_controller, err) {
        reject(err)
      }
    }
  })

  client.dispatch({
    method: 'GET',
    path: '/',
    headers: {
      range: 'bytes=0-3'
    }
  }, handler)

  await promise
  equal(Buffer.concat(chunks).toString(), 'abcdef')
} finally {
  await client.close()
  server.close()
  await once(server, 'close')
}

Expected Behavior

No error since the Content-Range is valid

Logs & Screenshots

It throws error

RequestRetryError: Content-Range mismatch
    at RetryHandler.onResponseStart (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/handler/retry-handler.js:216:15)
    at Request.onResponseStart (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/core/request.js:330:39)
    at Parser.onHeadersComplete (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/dispatcher/client-h1.js:609:27)
    at wasm_on_headers_complete (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/dispatcher/client-h1.js:153:30)
    at wasm://wasm/00034eea:wasm-function[10]:0x571
    at wasm://wasm/00034eea:wasm-function[20]:0x845f
    at Parser.execute (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/dispatcher/client-h1.js:337:22)
    at Parser.readMore (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/dispatcher/client-h1.js:301:12)
    at Socket.onHttpSocketReadable (/Users/trivikram/workspace/test-repro/node_modules/undici/lib/dispatcher/client-h1.js:885:18)
    at Socket.emit (node:events:509:28) {
  code: 'UND_ERR_REQ_RETRY',
  statusCode: 206,
  data: { count: 2 },
  headers: {
    etag: '"same"',
    'content-range': 'bytes 3-5/*',
    date: 'Sun, 26 Apr 2026 02:42:45 GMT',
    connection: 'keep-alive',
    'keep-alive': 'timeout=5',
    'transfer-encoding': 'chunked'
  }
}

Environment

macOS 26.4.1
Node v24.15.0
undici v8.1.0

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
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