Merge pull request #20595 from Snuffleupagus/NetworkManager-simplify

Simplify the `NetworkManager` class, and inline it in the  `PDFNetworkStream` class
This commit is contained in:
Tim van der Meij 2026-01-29 21:56:58 +01:00 committed by GitHub
commit 7cdd03ad9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -31,77 +31,80 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
const OK_RESPONSE = 200; const OK_RESPONSE = 200;
const PARTIAL_CONTENT_RESPONSE = 206; const PARTIAL_CONTENT_RESPONSE = 206;
function getArrayBuffer(xhr) { function getArrayBuffer(val) {
const data = xhr.response; return typeof val !== "string" ? val : stringToBytes(val).buffer;
if (typeof data !== "string") {
return data;
}
return stringToBytes(data).buffer;
} }
class NetworkManager { /** @implements {IPDFStream} */
class PDFNetworkStream {
#pendingRequests = new WeakMap();
_fullRequestReader = null;
_rangeRequestReaders = [];
_responseOrigin = null; _responseOrigin = null;
constructor({ url, httpHeaders, withCredentials }) { constructor(source) {
this.url = url; this._source = source;
this.isHttp = /^https?:/i.test(url); this.url = source.url;
this.headers = createHeaders(this.isHttp, httpHeaders); this.isHttp = /^https?:/i.test(this.url);
this.withCredentials = withCredentials || false; this.headers = createHeaders(this.isHttp, source.httpHeaders);
this.currXhrId = 0;
this.pendingRequests = Object.create(null);
} }
request(args) { /**
* @ignore
*/
_request(args) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const xhrId = this.currXhrId++; const pendingRequest = {
const pendingRequest = (this.pendingRequests[xhrId] = { xhr }); validateStatus: null,
onHeadersReceived: args.onHeadersReceived,
onDone: args.onDone,
onError: args.onError,
onProgress: args.onProgress,
};
this.#pendingRequests.set(xhr, pendingRequest);
xhr.open("GET", this.url); xhr.open("GET", this.url);
xhr.withCredentials = this.withCredentials; xhr.withCredentials = this._source.withCredentials;
for (const [key, val] of this.headers) { for (const [key, val] of this.headers) {
xhr.setRequestHeader(key, val); xhr.setRequestHeader(key, val);
} }
if (this.isHttp && "begin" in args && "end" in args) { if (this.isHttp && "begin" in args && "end" in args) {
xhr.setRequestHeader("Range", `bytes=${args.begin}-${args.end - 1}`); xhr.setRequestHeader("Range", `bytes=${args.begin}-${args.end - 1}`);
pendingRequest.expectedStatus = PARTIAL_CONTENT_RESPONSE;
// From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2:
// "A server MAY ignore the Range header". This means it's possible to
// get a 200 rather than a 206 response from a range request.
pendingRequest.validateStatus = status =>
status === PARTIAL_CONTENT_RESPONSE || status === OK_RESPONSE;
} else { } else {
pendingRequest.expectedStatus = OK_RESPONSE; pendingRequest.validateStatus = status => status === OK_RESPONSE;
} }
xhr.responseType = "arraybuffer"; xhr.responseType = "arraybuffer";
assert(args.onError, "Expected `onError` callback to be provided."); assert(args.onError, "Expected `onError` callback to be provided.");
xhr.onerror = () => { xhr.onerror = () => args.onError(xhr.status);
args.onError(xhr.status); xhr.onreadystatechange = this.#onStateChange.bind(this, xhr);
}; xhr.onprogress = this.#onProgress.bind(this, xhr);
xhr.onreadystatechange = this.onStateChange.bind(this, xhrId);
xhr.onprogress = this.onProgress.bind(this, xhrId);
pendingRequest.onHeadersReceived = args.onHeadersReceived;
pendingRequest.onDone = args.onDone;
pendingRequest.onError = args.onError;
pendingRequest.onProgress = args.onProgress;
xhr.send(null); xhr.send(null);
return xhrId; return xhr;
} }
onProgress(xhrId, evt) { #onProgress(xhr, evt) {
const pendingRequest = this.pendingRequests[xhrId]; const pendingRequest = this.#pendingRequests.get(xhr);
if (!pendingRequest) { pendingRequest?.onProgress?.(evt);
return; // Maybe abortRequest was called...
}
pendingRequest.onProgress?.(evt);
} }
onStateChange(xhrId, evt) { #onStateChange(xhr, evt) {
const pendingRequest = this.pendingRequests[xhrId]; const pendingRequest = this.#pendingRequests.get(xhr);
if (!pendingRequest) { if (!pendingRequest) {
return; // Maybe abortRequest was called... return; // Maybe abortRequest was called...
} }
const xhr = pendingRequest.xhr;
if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) { if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) {
pendingRequest.onHeadersReceived(); pendingRequest.onHeadersReceived();
delete pendingRequest.onHeadersReceived; delete pendingRequest.onHeadersReceived;
@ -111,13 +114,12 @@ class NetworkManager {
return; return;
} }
if (!(xhrId in this.pendingRequests)) { if (!this.#pendingRequests.has(xhr)) {
// The XHR request might have been aborted in onHeadersReceived() // The XHR request might have been aborted in onHeadersReceived()
// callback, in which case we should abort request. // callback, in which case we should abort request.
return; return;
} }
this.#pendingRequests.delete(xhr);
delete this.pendingRequests[xhrId];
// Success status == 0 can be on ftp, file and other protocols. // Success status == 0 can be on ftp, file and other protocols.
if (xhr.status === 0 && this.isHttp) { if (xhr.status === 0 && this.isHttp) {
@ -126,74 +128,36 @@ class NetworkManager {
} }
const xhrStatus = xhr.status || OK_RESPONSE; const xhrStatus = xhr.status || OK_RESPONSE;
// From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: if (!pendingRequest.validateStatus(xhrStatus)) {
// "A server MAY ignore the Range header". This means it's possible to
// get a 200 rather than a 206 response from a range request.
const ok_response_on_range_request =
xhrStatus === OK_RESPONSE &&
pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE;
if (
!ok_response_on_range_request &&
xhrStatus !== pendingRequest.expectedStatus
) {
pendingRequest.onError(xhr.status); pendingRequest.onError(xhr.status);
return; return;
} }
const chunk = getArrayBuffer(xhr); const chunk = getArrayBuffer(xhr.response);
if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { if (xhrStatus === PARTIAL_CONTENT_RESPONSE) {
const rangeHeader = xhr.getResponseHeader("Content-Range"); const rangeHeader = xhr.getResponseHeader("Content-Range");
const matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); if (/bytes (\d+)-(\d+)\/(\d+)/.test(rangeHeader)) {
if (matches) { pendingRequest.onDone(chunk);
pendingRequest.onDone({
begin: parseInt(matches[1], 10),
chunk,
});
} else { } else {
warn(`Missing or invalid "Content-Range" header.`); warn(`Missing or invalid "Content-Range" header.`);
pendingRequest.onError(0); pendingRequest.onError(0);
} }
} else if (chunk) { } else if (chunk) {
pendingRequest.onDone({ pendingRequest.onDone(chunk);
begin: 0,
chunk,
});
} else { } else {
pendingRequest.onError(xhr.status); pendingRequest.onError(xhr.status);
} }
} }
getRequestXhr(xhrId) { /**
return this.pendingRequests[xhrId].xhr; * Abort the request, if it's pending.
} * @ignore
*/
isPendingRequest(xhrId) { _abortRequest(xhr) {
return xhrId in this.pendingRequests; if (this.#pendingRequests.has(xhr)) {
} this.#pendingRequests.delete(xhr);
abortRequest(xhrId) {
const xhr = this.pendingRequests[xhrId].xhr;
delete this.pendingRequests[xhrId];
xhr.abort(); xhr.abort();
} }
}
/** @implements {IPDFStream} */
class PDFNetworkStream {
constructor(source) {
this._source = source;
this._manager = new NetworkManager(source);
this._rangeChunkSize = source.rangeChunkSize;
this._fullRequestReader = null;
this._rangeRequestReaders = [];
}
_onRangeRequestReaderClosed(reader) {
const i = this._rangeRequestReaders.indexOf(reader);
if (i >= 0) {
this._rangeRequestReaders.splice(i, 1);
}
} }
getFullReader() { getFullReader() {
@ -201,20 +165,18 @@ class PDFNetworkStream {
!this._fullRequestReader, !this._fullRequestReader,
"PDFNetworkStream.getFullReader can only be called once." "PDFNetworkStream.getFullReader can only be called once."
); );
this._fullRequestReader = new PDFNetworkStreamFullRequestReader( this._fullRequestReader = new PDFNetworkStreamFullRequestReader(this);
this._manager,
this._source
);
return this._fullRequestReader; return this._fullRequestReader;
} }
getRangeReader(begin, end) { getRangeReader(begin, end) {
const reader = new PDFNetworkStreamRangeRequestReader( const reader = new PDFNetworkStreamRangeRequestReader(this, begin, end);
this._manager, reader.onClosed = () => {
begin, const i = this._rangeRequestReaders.indexOf(reader);
end if (i >= 0) {
); this._rangeRequestReaders.splice(i, 1);
reader.onClosed = this._onRangeRequestReaderClosed.bind(this); }
};
this._rangeRequestReaders.push(reader); this._rangeRequestReaders.push(reader);
return reader; return reader;
} }
@ -230,20 +192,20 @@ class PDFNetworkStream {
/** @implements {IPDFStreamReader} */ /** @implements {IPDFStreamReader} */
class PDFNetworkStreamFullRequestReader { class PDFNetworkStreamFullRequestReader {
constructor(manager, source) { constructor(stream) {
this._manager = manager; this._stream = stream;
const { disableRange, length, rangeChunkSize } = stream._source;
this._url = source.url; this._fullRequestXhr = stream._request({
this._fullRequestId = manager.request({
onHeadersReceived: this._onHeadersReceived.bind(this), onHeadersReceived: this._onHeadersReceived.bind(this),
onDone: this._onDone.bind(this), onDone: this._onDone.bind(this),
onError: this._onError.bind(this), onError: this._onError.bind(this),
onProgress: this._onProgress.bind(this), onProgress: this._onProgress.bind(this),
}); });
this._headersCapability = Promise.withResolvers(); this._headersCapability = Promise.withResolvers();
this._disableRange = source.disableRange || false; this._disableRange = disableRange || false;
this._contentLength = source.length; // Optional this._contentLength = length; // Optional
this._rangeChunkSize = source.rangeChunkSize; this._rangeChunkSize = rangeChunkSize;
if (!this._rangeChunkSize && !this._disableRange) { if (!this._rangeChunkSize && !this._disableRange) {
this._disableRange = true; this._disableRange = true;
} }
@ -261,12 +223,10 @@ class PDFNetworkStreamFullRequestReader {
} }
_onHeadersReceived() { _onHeadersReceived() {
const fullRequestXhrId = this._fullRequestId; const stream = this._stream;
const fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); const fullRequestXhr = this._fullRequestXhr;
this._manager._responseOrigin = getResponseOrigin( stream._responseOrigin = getResponseOrigin(fullRequestXhr.responseURL);
fullRequestXhr.responseURL
);
const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders(); const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders();
const responseHeaders = new Headers( const responseHeaders = new Headers(
@ -285,7 +245,7 @@ class PDFNetworkStreamFullRequestReader {
const { allowRangeRequests, suggestedLength } = const { allowRangeRequests, suggestedLength } =
validateRangeRequestCapabilities({ validateRangeRequestCapabilities({
responseHeaders, responseHeaders,
isHttp: this._manager.isHttp, isHttp: stream.isHttp,
rangeChunkSize: this._rangeChunkSize, rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange, disableRange: this._disableRange,
}); });
@ -303,20 +263,18 @@ class PDFNetworkStreamFullRequestReader {
// requests, there will be an issue for sites where you can only // requests, there will be an issue for sites where you can only
// request the pdf once. However, if this is the case, then the // request the pdf once. However, if this is the case, then the
// server should not be returning that it can support range requests. // server should not be returning that it can support range requests.
this._manager.abortRequest(fullRequestXhrId); stream._abortRequest(fullRequestXhr);
} }
this._headersCapability.resolve(); this._headersCapability.resolve();
} }
_onDone(data) { _onDone(chunk) {
if (data) {
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const requestCapability = this._requests.shift();
requestCapability.resolve({ value: data.chunk, done: false }); requestCapability.resolve({ value: chunk, done: false });
} else { } else {
this._cachedChunks.push(data.chunk); this._cachedChunks.push(chunk);
}
} }
this._done = true; this._done = true;
if (this._cachedChunks.length > 0) { if (this._cachedChunks.length > 0) {
@ -329,7 +287,7 @@ class PDFNetworkStreamFullRequestReader {
} }
_onError(status) { _onError(status) {
this._storedError = createResponseError(status, this._url); this._storedError = createResponseError(status, this._stream.url);
this._headersCapability.reject(this._storedError); this._headersCapability.reject(this._storedError);
for (const requestCapability of this._requests) { for (const requestCapability of this._requests) {
requestCapability.reject(this._storedError); requestCapability.reject(this._storedError);
@ -390,20 +348,20 @@ class PDFNetworkStreamFullRequestReader {
requestCapability.resolve({ value: undefined, done: true }); requestCapability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
if (this._manager.isPendingRequest(this._fullRequestId)) {
this._manager.abortRequest(this._fullRequestId); this._stream._abortRequest(this._fullRequestXhr);
} this._fullRequestXhr = null;
this._fullRequestReader = null;
} }
} }
/** @implements {IPDFStreamRangeReader} */ /** @implements {IPDFStreamRangeReader} */
class PDFNetworkStreamRangeRequestReader { class PDFNetworkStreamRangeRequestReader {
constructor(manager, begin, end) { onClosed = null;
this._manager = manager;
this._url = manager.url; constructor(stream, begin, end) {
this._requestId = manager.request({ this._stream = stream;
this._requestXhr = stream._request({
begin, begin,
end, end,
onHeadersReceived: this._onHeadersReceived.bind(this), onHeadersReceived: this._onHeadersReceived.bind(this),
@ -417,28 +375,20 @@ class PDFNetworkStreamRangeRequestReader {
this._storedError = undefined; this._storedError = undefined;
this.onProgress = null; this.onProgress = null;
this.onClosed = null;
} }
_onHeadersReceived() { _onHeadersReceived() {
const responseOrigin = getResponseOrigin( const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL);
this._manager.getRequestXhr(this._requestId)?.responseURL
);
if (responseOrigin !== this._manager._responseOrigin) { if (responseOrigin !== this._stream._responseOrigin) {
this._storedError = new Error( this._storedError = new Error(
`Expected range response-origin "${responseOrigin}" to match "${this._manager._responseOrigin}".` `Expected range response-origin "${responseOrigin}" to match "${this._stream._responseOrigin}".`
); );
this._onError(0); this._onError(0);
} }
} }
_close() { _onDone(chunk) {
this.onClosed?.(this);
}
_onDone(data) {
const chunk = data.chunk;
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const requestCapability = this._requests.shift();
requestCapability.resolve({ value: chunk, done: false }); requestCapability.resolve({ value: chunk, done: false });
@ -450,11 +400,11 @@ class PDFNetworkStreamRangeRequestReader {
requestCapability.resolve({ value: undefined, done: true }); requestCapability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this._close(); this.onClosed?.(this);
} }
_onError(status) { _onError(status) {
this._storedError ??= createResponseError(status, this._url); this._storedError ??= createResponseError(status, this._stream.url);
for (const requestCapability of this._requests) { for (const requestCapability of this._requests) {
requestCapability.reject(this._storedError); requestCapability.reject(this._storedError);
} }
@ -495,10 +445,9 @@ class PDFNetworkStreamRangeRequestReader {
requestCapability.resolve({ value: undefined, done: true }); requestCapability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
if (this._manager.isPendingRequest(this._requestId)) {
this._manager.abortRequest(this._requestId); this._stream._abortRequest(this._requestXhr);
} this.onClosed?.(this);
this._close();
} }
} }