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
+12 -26
View File
@@ -234,14 +234,9 @@ 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);
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.'); throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
} }
break;
}
if (worker.aborted) { if (worker.aborted) {
break; break;
} }
@@ -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,18 +555,19 @@ export class UrlSource extends Source {
const { done, value } = readResult; const { done, value } = readResult;
if (done) { if (done) {
if (worker.currentPos >= worker.targetPos) {
// All data was delivered, we're good
this._orchestrator.forgetWorker(worker); this._orchestrator.forgetWorker(worker);
if (worker.currentPos < worker.targetPos) {
throw new Error(
'Response stream reader stopped unexpectedly before all requested data was read.',
);
}
worker.running = false; worker.running = false;
return; return;
} }
// The response stopped early, before the target. This can happen if server decides to cap range
// 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);
this._orchestrator.supplyWorkerData(worker, value); this._orchestrator.supplyWorkerData(worker, value);
} }
@@ -597,16 +585,15 @@ 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}`);
} }
} else { }
const contentLength = response.headers.get('Content-Length'); const contentLength = response.headers.get('Content-Length');
if (contentLength) { if (contentLength) {
return Number(contentLength); return Number(contentLength);
@@ -617,7 +604,6 @@ export class UrlSource extends Source {
); );
} }
} }
}
/** @internal */ /** @internal */
_dispose() { _dispose() {