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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions