Workers SDK Issue Reports

← Back to Dashboard

#8967 Wrangler assets doesn't support HTTP 1.1 byte serving, but Vite-plugin does creating prod & dev mismatch

Recommendation:KEEP OPEN
Difficulty:medium
Reasoning:

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.

Suggested Action:

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

  1. No merged PRs reference this issue - Searched for PRs mentioning #8967, "byte serving", "Accept-Ranges", and "range request" - none found
  2. No changelog entries found - Searched wrangler and workers-shared changelogs for issue number and related keywords - no matches
  3. Code analysis confirms the bug:
    • The asset-worker in packages/workers-shared/asset-worker/src/utils/headers.ts does NOT add Accept-Ranges: bytes header to responses
    • The isCacheable() function (line 49) checks for Range header presence but never enables range support
    • Compare to packages/kv-asset-handler/src/index.ts (line 305) which correctly sets Accept-Ranges: bytes header
  4. 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:

  1. No Accept-Ranges header - The response never advertises that byte ranges are supported
  2. No Content-Length header - Required for proper Range request handling
  3. No Range request handling - Even if a Range header is sent, the full response body is returned
  4. 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:

  1. Parse the Range header from the request
  2. Slice the readable stream to the requested byte range
  3. Return 206 Partial Content status
  4. Add Content-Range header (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: bytes header 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

  1. packages/workers-shared/asset-worker/src/utils/headers.ts

    • Add Accept-Ranges: bytes header
    • Add Content-Length header support
    • Add Content-Range header support for partial responses
  2. 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
  3. packages/workers-shared/asset-worker/src/worker.ts

    • May need to expose content length from unstable_getByETag
  4. packages/workers-shared/utils/responses.ts (if exists)

    • May need a new PartialContentResponse class (206 status)

Testing Recommendations

  1. Unit Tests:

    • Test Accept-Ranges: bytes header 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
  2. Integration Tests:

    • Video file streaming with seek functionality
    • Large file download with resume capability
    • PDF viewer with lazy page loading
  3. E2E Tests:

    • Test in wrangler dev mode matches production behavior
    • Test with real browsers (Chrome, Firefox) video player
    • Test with curl/wget range request support
  4. 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.

Add Note