UrlSource: Deal gracefully with servers that cap unbounded range requests (fixes #231)

This commit is contained in:
Vanilagy
2025-11-26 11:26:09 +01:00
parent cc3d48451e
commit f33dd4f2cd
+21 -35
View File
@@ -234,12 +234,7 @@ export class BlobSource extends Source {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) { if (done) {
this._orchestrator.forgetWorker(worker); this._orchestrator.forgetWorker(worker);
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
if (worker.currentPos < worker.targetPos) { // I think this `if` should always hit?
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
}
break;
} }
if (worker.aborted) { if (worker.aborted) {
@@ -441,7 +436,7 @@ export class UrlSource extends Source {
let fileSize: number; let fileSize: number;
if (response.status === 206) { if (response.status === 206) {
fileSize = this._getPartialLengthFromRangeResponse(response); fileSize = this._getTotalLengthFromRangeResponse(response);
worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT)); worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT));
} else { } else {
// Server probably returned a 200. // Server probably returned a 200.
@@ -515,14 +510,6 @@ export class UrlSource extends Source {
); );
} }
const length = this._getPartialLengthFromRangeResponse(response);
const required = worker.targetPos - worker.currentPos;
if (length < required) {
throw new Error(
`HTTP response unexpectedly too short: Needed at least ${required} bytes, got only ${length}.`,
);
}
if (!response.body) { if (!response.body) {
throw new Error( throw new Error(
'Missing HTTP response body stream. The used fetch function must provide the response body as a' 'Missing HTTP response body stream. The used fetch function must provide the response body as a'
@@ -568,16 +555,17 @@ export class UrlSource extends Source {
const { done, value } = readResult; const { done, value } = readResult;
if (done) { if (done) {
this._orchestrator.forgetWorker(worker); if (worker.currentPos >= worker.targetPos) {
// All data was delivered, we're good
if (worker.currentPos < worker.targetPos) { this._orchestrator.forgetWorker(worker);
throw new Error( worker.running = false;
'Response stream reader stopped unexpectedly before all requested data was read.', return;
);
} }
worker.running = false; // The response stopped early, before the target. This can happen if server decides to cap range
return; // requests arbitrarily, even if the request had an uncapped end. In this case, let's fetch the rest
// of the data using a new request.
break;
} }
this.onread?.(worker.currentPos, worker.currentPos + value.length); this.onread?.(worker.currentPos, worker.currentPos + value.length);
@@ -597,25 +585,23 @@ export class UrlSource extends Source {
} }
/** @internal */ /** @internal */
private _getPartialLengthFromRangeResponse(response: Response) { private _getTotalLengthFromRangeResponse(response: Response) {
const contentRange = response.headers.get('Content-Range'); const contentRange = response.headers.get('Content-Range');
if (contentRange) { if (contentRange) {
const match = /\/(\d+)/.exec(contentRange); const match = /\/(\d+)/.exec(contentRange);
if (match) { if (match) {
return Number(match[1]); return Number(match[1]);
} else {
throw new Error(`Invalid Content-Range header: ${contentRange}`);
} }
}
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
return Number(contentLength);
} else { } else {
const contentLength = response.headers.get('Content-Length'); throw new Error(
if (contentLength) { 'Partial HTTP response (status 206) must surface either Content-Range or'
return Number(contentLength); + ' Content-Length header.',
} else { );
throw new Error(
'Partial HTTP response (status 206) must surface either Content-Range or'
+ ' Content-Length header.',
);
}
} }
} }