Skip to content

feat: add multipart range requests - #2479

Open
Ayush-840 wants to merge 1 commit into
webpack:mainfrom
Ayush-840:feat/multipart-range-requests
Open

Ayush-840 wants to merge 1 commit into
webpack:mainfrom
Ayush-840:feat/multipart-range-requests

Conversation

@Ayush-840

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

Copy link
Copy Markdown

Summary

Refs #2454 (RFC: regroup the options), specifically the “Names worth fixing while we are here” section.

This PR renames two client options that are confusing when used together:

  • reload → reloadOnFailedUpdate
  • urlPrefix → urlParamPrefix

The first one is especially confusing because reload and liveReload sound like they do almost the same thing, but they actually have different triggers. liveReload reloads the page after a successful build, while reload reloads the page when a hot update cannot be applied. Renaming it to reloadOnFailedUpdate makes the behavior much clearer.

Similarly, urlPrefix is a bit vague because it doesn't make it clear that the option controls the prefix used for the URL query parameters generated by the client. urlParamPrefix describes that more accurately.

Both changes are backward-compatible aliases, following the same approach already used for cache/mime and previously for hot.progress.

How the aliases work

  • Both the old and new names are accepted.
  • If both are provided, the new name takes precedence and a warning explains which value is being used.
  • Using the old name shows a deprecation warning:
    ... deprecated and will be removed in the next major release. Use '<new>' instead.
  • Each legacy alias has a // TODO in the next major release marker.
  • The old keys are intentionally kept on the options object because instance.context.options is publicly reachable.

For RFC open question 1, I went with warning immediately, following the existing behavior of the cache/mime option grouping. If maintainers prefer warnings only when the next major release happens, I'm happy to change this.

For the other open question around build.fileSystem, I interpreted it as shorthand for the existing outputFileSystem. However, this PR doesn't make any changes to the build group or the hot.client fold. Those are more design-heavy changes, so I've left them for the maintainers to decide based on the RFC's open questions.

What kind of change does this PR introduce?

feat — renames two options while keeping the old names working as backward-compatible aliases.

The changes cover:

  • options schema: src/options.json
  • regenerated validator: src/options.check.js
  • HotOptions / HotClientOptions JSDoc typedefs
  • client option serialization
  • option name-sync tests
  • README documentation
  • .cspell.json

Did you add tests for your changes?

Yes.

I added/updated tests covering:

  • both new option names working independently
  • the new name taking precedence when both old and new names are provided
  • deprecation warnings for the old names
  • warnings correctly mentioning the replacement name
  • no warning when only the new name is used
  • schema/client name synchronization
  • browser query parameters receiving the resolved value regardless of which spelling is used

The full npm test suite is also passing, including linting, schema checks, types, and e2e tests.

Does this PR introduce a breaking change?

No.

Existing configurations using reload or urlPrefix will continue to work. They will only receive a deprecation warning.

The old names are planned to be removed in the next major release.

Documentation

The README has already been updated to use:

  • reloadOnFailedUpdate as the primary name, with reload marked as deprecated
  • urlParamPrefix as the primary name, with urlPrefix marked as deprecated

Nothing else should be needed when this PR is merged. The old names can be removed in the next-major migration notes alongside cache/mime.

Use of AI

Yes. I used Claude to help research the RFC and existing repository conventions, think through the implementation approach, draft parts of this PR description, and put together the verification checklist.

I reviewed the complete diff myself, ran the full test suite locally, and I'm responsible for the changes. I have also read the webpack AI policy.

Summary by CodeRabbit

  • New Features
    • Multiple byte-range requests can now receive 206 Partial Content responses in multipart/byteranges format. Satisfiable ranges are served even when the request also includes unsatisfiable ranges; requests with no satisfiable ranges return 416.
    • Requests with more than 200 ranges, or ranges whose combined size is at least the full file size, receive the regular full-file response instead.

@changeset-bot

changeset-bot Bot commented Oct 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 49fc520

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

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

The middleware now returns multipart 206 responses for multiple satisfiable ranges when there are no more than 200 ranges and their combined length is less than the file size. It uses a new helper to create the multipart body as a readable stream. Tests cover GET and HEAD requests, mixed satisfiable ranges, all-unsatisfiable ranges, and requests above the range limit.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 49fc5

Multipart range responses are a new public behavior. On filesystems without stream support, a file that disappears after it is checked can make the request fail without a proper error response. Response-data callbacks can receive a different stream type than the documented one. The test for the 200-range limit does not actually exercise that limit. These issues should be fixed before merging.

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 changes: adding multipart byte-range request support. It aligns with the changed files and commit message.
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. (2 skipped: 2 …
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
🧪 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: 4f7e441b-2183-4068-9aa9-0c9bd97ef753
📥 Commits

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

📒 Files selected for processing (6)
  • .changeset/multipart-range-requests.md
  • .cspell.json
  • 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.

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 Content-Type for each part.

If headers overrides Content-Type, or mime.default supplies it for an unknown extension, this lookup does not return the Content-Type selected earlier in processRequest. The multipart parts can therefore identify the file with a different type or omit its type. Read the selected response header before setting the outer multipart Content-Type, and pass that value to createMultipartBody. RFC 9110 says each part should carry the Content-Type that the corresponding 200 response would have carried. (rfc-editor.org)

Comment thread src/middleware.js

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

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 multipart read failures.

If a filesystem has no createReadStream and the file disappears after stat, createMultipartBody throws from readFileSync. This call is outside the try that handles regular file-read failures. processRequest then rejects without sending the normal error response. Catch this call and pass the error to errorHandler, as the regular read path does.

Comment thread src/middleware.js
req,
res,
bufferOrStream,
/** @type {Buffer | ReadStream} */ (bufferOrStream),

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 | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Inspect the callback declaration and nearby usage without executing repository code.
rg -n -C 5 'ModifyResponseData|modifyResponseData|type ResponseData' types src README.md

Repository: webpack/webpack-dev-middleware

Length of output: 41759


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- diff ---'
git diff --no-ext-diff --unified=30 8a7af5e362f56610ace8d7dd0a4d277b4952c2a7 49fc520b8a20f25187096d5b0fdf45a8fe1119c4
printf '%s\n' '--- middleware imports and range path ---'
sed -n '1,120p' src/middleware.js
sed -n '930,1175p' src/middleware.js
printf '%s\n' '--- public callback declarations ---'
sed -n '1,100p' src/index.js
sed -n '160,205p' types/index.d.ts
printf '%s\n' '--- README callback documentation ---'
sed -n '318,345p' README.md

Repository: webpack/webpack-dev-middleware

Length of output: 44636


Expose multipart response data as Readable.

createMultipartBody returns Readable.from(body()), and the multipart branch passes that value to modifyResponseData. The callback contract still declares Buffer | ReadStream. A callback that uses a ReadStream-specific property can fail for multipart responses.

Update ResponseData, ModifyResponseData, and the README to use generic Readable. Keep Buffer; fs.ReadStream remains supported because it is a Readable.

🐛 Suggested fix
- * @property {Buffer | ReadStream} data data
+ * @property {Buffer | Readable} data data
...
- * @param {Buffer | ReadStream} data data
+ * @param {Buffer | Readable} data data

Apply the equivalent changes in types/index.d.ts, import or define Readable from node:stream, and update the README from ReadStream to Readable.

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

Generate 201 separate ranges for the cap test.

ranges.join(", ") repeats bytes= after the first entry, so range-parser discards those entries. Changing only that prefix is insufficient: the generated ranges are adjacent, and combine: true merges them. Use one bytes= prefix and leave a gap between ranges. The current test cannot reach the 201-range fallback and can fail its 200 assertion. (github.com)

Proposed test input
 for (let i = 0; i < 201; i++) {
-  ranges.push(`bytes=${i * 2}-${i * 2 + 1}`);
+  ranges.push(`${i * 3}-${i * 3 + 1}`);
 }
 const response = await req
   .get("/bundle.js")
-  .set("Range", ranges.join(", "));
+  .set("Range", `bytes=${ranges.join(",")}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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(", "));
for (let i = 0; i < 201; i++) {
ranges.push(`${i * 3}-${i * 3 + 1}`);
}
const response = await req
.get("/bundle.js")
.set("Range", `bytes=${ranges.join(",")}`);

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.

1 participant