#8967 Wrangler assets doesn't support HTTP 1.1 byte serving, but Vite-plugin does creating prod & dev mismatch
Valid bug confirmed in code. asset-worker doesn't add Accept-Ranges header or handle Range requests. kv-asset-handler correctly implements this. Creates dev/prod mismatch with Vite plugin.
Implement Accept-Ranges: bytes header and Range request handling in asset-worker/src/utils/headers.ts
Analysis Report
Issue Review: cloudflare/workers-sdk#8967
Summary
Wrangler assets (Workers + Assets) doesn't support HTTP 1.1 byte serving (Range requests) while the Vite plugin does, creating a dev/prod mismatch.
Findings
- Created: 2025-04-16
- Updated: 2025-04-16
- Version: wrangler 4.10.0 -> 4.60.0 (current)
- Component: Workers + Assets (asset-worker)
- Labels: bug, Workers + Assets, vite-plugin
- Comments: 0
Key Evidence
- No merged PRs reference this issue - Searched for PRs mentioning #8967, "byte serving", "Accept-Ranges", and "range request" - none found
- No changelog entries found - Searched wrangler and workers-shared changelogs for issue number and related keywords - no matches
- Code analysis confirms the bug:
- The asset-worker in
packages/workers-shared/asset-worker/src/utils/headers.tsdoes NOT addAccept-Ranges: bytesheader to responses - The
isCacheable()function (line 49) checks forRangeheader presence but never enables range support - Compare to
packages/kv-asset-handler/src/index.ts(line 305) which correctly setsAccept-Ranges: bytesheader
- The asset-worker in
- Vite plugin works differently - In dev mode, Vite's built-in static file server handles Range requests natively, explaining the dev/prod mismatch
Recommendation
Status: KEEP OPEN
Reasoning: This is a valid bug report. The asset-worker does not advertise or support HTTP Range requests (byte serving), while the Vite plugin's development server does. This creates a meaningful dev/prod parity issue, particularly for applications that rely on byte range requests (e.g., video streaming, large file downloads, PDF viewers with lazy loading).
Action: Implement Accept-Ranges: bytes header support in the asset-worker, consistent with how kv-asset-handler already implements it.
Root Cause Analysis
The bug is in packages/workers-shared/asset-worker/src/utils/headers.ts.
Current Code (headers.ts, lines 16-51):
export function getAssetHeaders(
{ eTag, resolver }: AssetIntentWithResolver,
contentType: string | undefined,
cacheStatus: string,
request: Request,
configuration: Required<AssetConfig>
) {
const headers = new Headers({
ETag: `"${eTag}"`,
});
if (contentType !== undefined) {
headers.append("Content-Type", contentType);
}
if (isCacheable(request)) {
headers.append("Cache-Control", CACHE_CONTROL_BROWSER);
}
// ... rest of function
return headers;
}
function isCacheable(request: Request) {
return !request.headers.has("Authorization") && !request.headers.has("Range");
}
Issues:
- No
Accept-Rangesheader - The response never advertises that byte ranges are supported - No Content-Length header - Required for proper Range request handling
- No Range request handling - Even if a
Rangeheader is sent, the full response body is returned - No 206 Partial Content status - Range requests should return 206, not 200
Comparison with Working Implementation (kv-asset-handler/src/index.ts, lines 300-315):
response = new Response(body);
if (shouldEdgeCache) {
response.headers.set("Accept-Ranges", "bytes");
response.headers.set("Content-Length", String(body.byteLength));
// ...
}
Proposed Solution
Minimal Fix - Add Accept-Ranges header
File: packages/workers-shared/asset-worker/src/utils/headers.ts
export function getAssetHeaders(
{ eTag, resolver }: AssetIntentWithResolver,
contentType: string | undefined,
cacheStatus: string,
request: Request,
configuration: Required<AssetConfig>,
contentLength?: number // NEW: add content length parameter
) {
const headers = new Headers({
ETag: `"${eTag}"`,
});
if (contentType !== undefined) {
headers.append("Content-Type", contentType);
}
// NEW: Add Accept-Ranges and Content-Length headers
headers.append("Accept-Ranges", "bytes");
if (contentLength !== undefined) {
headers.append("Content-Length", String(contentLength));
}
if (isCacheable(request)) {
headers.append("Cache-Control", CACHE_CONTROL_BROWSER);
}
// ... rest unchanged
return headers;
}
Full Fix - Support actual Range requests (206 Partial Content)
For complete byte serving support, the handler.ts would need to:
- Parse the
Rangeheader from the request - Slice the readable stream to the requested byte range
- Return 206 Partial Content status
- Add
Content-Rangeheader (e.g.,bytes 0-499/1234)
File: packages/workers-shared/asset-worker/src/handler.ts
The resolveAssetIntentToResponse function needs to be updated:
const resolveAssetIntentToResponse = async (
assetIntent: AssetIntentWithResolver,
request: Request,
env: Env,
configuration: Required<AssetConfig>,
getByETag: typeof EntrypointType.prototype.unstable_getByETag,
analytics: Analytics
) => {
// ... existing code to get asset ...
const rangeHeader = request.headers.get("Range");
let body = asset.readableStream;
let status = assetIntent.status;
if (rangeHeader && method === "GET") {
const parsed = parseRangeHeader(rangeHeader, contentLength);
if (parsed) {
const { start, end } = parsed;
body = sliceStream(asset.readableStream, start, end);
status = 206;
headers.append("Content-Range", `bytes ${start}-${end}/${contentLength}`);
headers.set("Content-Length", String(end - start + 1));
}
}
// ... rest of response building ...
};
function parseRangeHeader(rangeHeader: string, totalLength: number): { start: number; end: number } | null {
const match = rangeHeader.match(/^bytes=(\d*)-(\d*)$/);
if (!match) return null;
let start = match[1] ? parseInt(match[1], 10) : 0;
let end = match[2] ? parseInt(match[2], 10) : totalLength - 1;
if (start >= totalLength || end >= totalLength || start > end) {
return null; // Could return 416 Range Not Satisfiable
}
return { start, end };
}
Implementation Difficulty
Rating: MEDIUM
Justification:
- Easy part: Adding
Accept-Ranges: bytesheader is trivial (1-2 lines) - Medium part: Getting content length requires changes to the asset retrieval pipeline
- Hard part: Actually slicing readable streams for partial content requires careful handling of:
- Stream transformation (the asset is a ReadableStream, not an ArrayBuffer)
- Multiple range requests (e.g.,
Range: bytes=0-499, 1000-1499) - Edge cases (invalid ranges, ranges past EOF)
- Proper 416 Range Not Satisfiable responses
- Performance considerations for large files
The existing kv-asset-handler uses ArrayBuffer which makes slicing easy. The asset-worker uses ReadableStream, which requires different handling.
Files That Would Need to Be Modified
packages/workers-shared/asset-worker/src/utils/headers.ts- Add
Accept-Ranges: bytesheader - Add
Content-Lengthheader support - Add
Content-Rangeheader support for partial responses
- Add
packages/workers-shared/asset-worker/src/handler.ts- Add Range header parsing
- Implement stream slicing for partial content
- Return 206 Partial Content status when appropriate
packages/workers-shared/asset-worker/src/worker.ts- May need to expose content length from
unstable_getByETag
- May need to expose content length from
packages/workers-shared/utils/responses.ts(if exists)- May need a new
PartialContentResponseclass (206 status)
- May need a new
Testing Recommendations
Unit Tests:
- Test
Accept-Ranges: bytesheader is present in all asset responses - Test Range header parsing for various formats:
Range: bytes=0-499(first 500 bytes)Range: bytes=500-999(second 500 bytes)Range: bytes=-500(last 500 bytes)Range: bytes=9500-(from byte 9500 to end)
- Test 206 response status for valid range requests
- Test 416 response for invalid ranges
- Test
Integration Tests:
- Video file streaming with seek functionality
- Large file download with resume capability
- PDF viewer with lazy page loading
E2E Tests:
- Test in
wrangler devmode matches production behavior - Test with real browsers (Chrome, Firefox) video player
- Test with curl/wget range request support
- Test in
Regression Tests:
- Ensure non-range requests still work correctly
- Ensure caching behavior isn't affected
- Ensure ETag/If-None-Match still works with Range requests
Notes & Feedback (0)
No notes yet.