Skip to content

feat: add multipart range requests - #2478

Closed
Ayush-840 wants to merge 1 commit into
webpack:mainfrom
Ayush-840:feat/option-renames
Closed

Ayush-840 wants to merge 1 commit into
webpack:mainfrom
Ayush-840:feat/option-renames

Conversation

@Ayush-840

@Ayush-840 Ayush-840 commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

Fixes #

A Range header with several separate ranges used to be ignored, and the middleware sent the whole file with a 200. For very large files (the reporter mentions game data files over 1 GB) that defeats the purpose of range requests.

This PR answers such requests with a 206 and a multipart/byteranges body, as described in RFC 9110 section 14.6:

  • one part per range, each with its own Content-Type and Content-Range;
  • Content-Length is calculated up front, so the response is streamed without chunked encoding;
  • overlapping or adjacent ranges are still merged first, so existing single-range behavior is unchanged;
  • if there are more than 200 ranges after merging, the Range header is ignored and the whole file is sent with a 200, to avoid abuse;
  • HEAD returns the same headers with no body.

What kind of change does this PR introduce?

feat

Did you add tests for your changes?

Yes, in test/middleware.test.js, run across all supported frameworks: a multipart 206 for two separate ranges, HEAD with multiple ranges, one valid and one unsatisfiable range, all ranges unsatisfiable (416), and more ranges than the cap (200). The existing tests for merged ranges still pass.

Does this PR introduce a breaking change?

No API changes. Behavior changes only for requests with several separate ranges: they now get a 206 multipart response instead of a 200 with the full file, which is what the HTTP spec describes. Single-range requests are unchanged.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

I added a "Range requests" section to the README, including how modifyResponseData behaves with a multipart body. A changeset (minor) is included.

Use of AI

I used Claude (Anthropic) to help analyze the issue, plan the approach and draft the first version of the implementation and tests. I reviewed the changes, ran lint, and ran the tests myself, and I can explain how the code works.

Summary by CodeRabbit

  • New Features
    • Multiple byte-range requests now receive a 206 Partial Content response with a separate part for each satisfiable range, including for HEAD requests.
    • Requests with more than 200 ranges, or ranges covering the entire file, receive a regular 200 response. Requests where all ranges are unsatisfiable receive 416.

@changeset-bot

changeset-bot Bot commented Oct 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ecbd40e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@linux-foundation-easycla

linux-foundation-easycla Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: Ayush-840 / name: Ayush (ecbd40e)

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

The middleware now returns multipart 206 responses for multiple byte-range requests when there are at most 200 ranges and their combined coverage is less than the file size. A new helper builds the multipart response body using streams or a cached file buffer. Tests cover GET and HEAD requests, mixed satisfiable ranges, all-unsatisfiable ranges, and requests with more than 200 ranges. The utility also has a TypeScript declaration and a changeset describing the response behavior.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to ecbd4

Multi-range requests now return multipart 206 responses. If a file read fails during this new path, the request skips the normal error response. The test for the 200-range limit does not actually test that limit. Fix both before merging. Smaller follow-ups are the content type used in each part and the wording of the release note.

Pre-merge checks | Passed 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding multipart range request support.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 4 files. (1 skipped: 1 …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ast-grep (0.45.3)
test/middleware.test.js

ast-grep timed out on this file



Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1c10354e-7f13-48b5-817f-a1cd62d7dd7f
📥 Commits

Reviewing files that changed from the base of the PR and between 8a7af5e and ecbd40e.

📒 Files selected for processing (5)
  • .changeset/multipart-range-requests.md
  • src/middleware.js
  • src/utils.js
  • test/middleware.test.js
  • types/utils.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

"webpack-dev-middleware": minor
---

Added support for `multipart/byteranges` responses. A `Range` header with several separate ranges now returns a `206` with one part per range instead of the whole file. Requests with more than 200 ranges, or ranges that cover the whole file, still get a normal `200`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the full-coverage status claim.

For a 10-byte file, Range: bytes=0-4,5-9 merges into one range and returns 206, not 200. The new full-coverage check only runs when multiple ranges remain. Remove or qualify the claim so the release note describes the implemented behavior. (raw.githubusercontent.com)

Comment thread src/middleware.js
}

if (shouldUseMultipart) {
const contentType = context.mimeTypes.contentType(filename);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use the selected response type for each part.

If headers sets Content-Type, or mime.default supplies it for an unknown extension, the filename-derived value can differ from the type a 200 response would use. Each multipart part then has the wrong type or no type. Read the established response Content-Type before replacing the outer header with multipart/byteranges. RFC 9110 recommends carrying that representation type in each part. (rfc-editor.org)

Comment thread src/utils.js
Comment on lines +593 to +595
cachedFullBuffer = /** @type {Buffer} */ (
outputFileSystem.readFileSync(filename)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle synchronous read failures before creating the response.

If a file disappears after the middleware stats it, readFileSync(filename) can throw. The multipart call in src/middleware.js is outside the ordinary read’s try block. The request then bypasses errorHandler instead of receiving the file-read error response. Catch this read failure at the call site or handle it within createMultipartBody.

Comment thread test/middleware.test.js
Comment on lines +1350 to +1355
for (let i = 0; i < 201; i++) {
ranges.push(`bytes=${i * 2}-${i * 2 + 1}`);
}
const response = await req
.get("/bundle.js")
.set("Range", ranges.join(", "));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Construct 201 valid, separate ranges for the cap test.

ranges.join(", ") produces bytes=0-1, bytes=2-3, .... range-parser skips the entries with the repeated bytes= prefix, so this request does not reach the cap and takes the single-range 206 path. Removing those prefixes alone is insufficient: the adjacent intervals merge into one range. Use one bytes= prefix and leave gaps between the 201 intervals. (raw.githubusercontent.com)

@alexander-akait

Copy link
Copy Markdown
Member

Fully AI generated PR with bugs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants