From 840cc5e0d42dfc97ea0317ca3cf37dff586acd39 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 2 Sep 2024 10:33:50 +0200 Subject: [PATCH 1/2] Use response-`Headers` in the different `IPDFStream` implementations Given that the `Headers` functionality is now available in all browsers/environments that we support, [see MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers#browser_compatibility), we can utilize "proper" `Headers` in the helper functions that are used to parse the response. --- src/display/fetch_stream.js | 6 +- src/display/network.js | 15 +- src/display/network_utils.js | 12 +- src/display/node_stream.js | 9 +- test/unit/network_utils_spec.js | 312 +++++++++++++------------------- 5 files changed, 153 insertions(+), 201 deletions(-) diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index 42ab4200fad74..1d025ce8bee68 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -127,11 +127,11 @@ class PDFFetchStreamReader { this._reader = response.body.getReader(); this._headersCapability.resolve(); - const getResponseHeader = name => response.headers.get(name); + const responseHeaders = response.headers; const { allowRangeRequests, suggestedLength } = validateRangeRequestCapabilities({ - getResponseHeader, + responseHeaders, isHttp: stream.isHttp, rangeChunkSize: this._rangeChunkSize, disableRange: this._disableRange, @@ -141,7 +141,7 @@ class PDFFetchStreamReader { // Setting right content length. this._contentLength = suggestedLength || this._contentLength; - this._filename = extractFilenameFromHeader(getResponseHeader); + this._filename = extractFilenameFromHeader(responseHeaders); // We need to stop reading when range is supported and streaming is // disabled. diff --git a/src/display/network.js b/src/display/network.js index 7bf98fe8dc4e3..fe0c901bd261f 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -273,11 +273,20 @@ class PDFNetworkStreamFullRequestReader { const fullRequestXhrId = this._fullRequestId; const fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); - const getResponseHeader = name => fullRequestXhr.getResponseHeader(name); + const responseHeaders = new Headers( + fullRequestXhr + .getAllResponseHeaders() + .trim() + .split(/[\r\n]+/) + .map(x => { + const [key, ...val] = x.split(": "); + return [key, val.join(": ")]; + }) + ); const { allowRangeRequests, suggestedLength } = validateRangeRequestCapabilities({ - getResponseHeader, + responseHeaders, isHttp: this._manager.isHttp, rangeChunkSize: this._rangeChunkSize, disableRange: this._disableRange, @@ -289,7 +298,7 @@ class PDFNetworkStreamFullRequestReader { // Setting right content length. this._contentLength = suggestedLength || this._contentLength; - this._filename = extractFilenameFromHeader(getResponseHeader); + this._filename = extractFilenameFromHeader(responseHeaders); if (this._isRangeSupported) { // NOTE: by cancelling the full request, and then issuing range diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 5c69af6a38df8..80f89588566c4 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -37,7 +37,7 @@ function createHeaders(isHttp, httpHeaders) { } function validateRangeRequestCapabilities({ - getResponseHeader, + responseHeaders, isHttp, rangeChunkSize, disableRange, @@ -53,7 +53,7 @@ function validateRangeRequestCapabilities({ suggestedLength: undefined, }; - const length = parseInt(getResponseHeader("Content-Length"), 10); + const length = parseInt(responseHeaders.get("Content-Length"), 10); if (!Number.isInteger(length)) { return returnValues; } @@ -69,11 +69,11 @@ function validateRangeRequestCapabilities({ if (disableRange || !isHttp) { return returnValues; } - if (getResponseHeader("Accept-Ranges") !== "bytes") { + if (responseHeaders.get("Accept-Ranges") !== "bytes") { return returnValues; } - const contentEncoding = getResponseHeader("Content-Encoding") || "identity"; + const contentEncoding = responseHeaders.get("Content-Encoding") || "identity"; if (contentEncoding !== "identity") { return returnValues; } @@ -82,8 +82,8 @@ function validateRangeRequestCapabilities({ return returnValues; } -function extractFilenameFromHeader(getResponseHeader) { - const contentDisposition = getResponseHeader("Content-Disposition"); +function extractFilenameFromHeader(responseHeaders) { + const contentDisposition = responseHeaders.get("Content-Disposition"); if (contentDisposition) { let filename = getFilenameFromContentDispositionHeader(contentDisposition); if (filename.includes("%")) { diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 51fd7ecc10fe5..6808488d62f84 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -305,14 +305,11 @@ class PDFNodeStreamFullReader extends BaseFullReader { this._headersCapability.resolve(); this._setReadableStream(response); - // Make sure that headers name are in lower case, as mentioned - // here: https://nodejs.org/api/http.html#http_message_headers. - const getResponseHeader = name => - this._readableStream.headers[name.toLowerCase()]; + const responseHeaders = new Headers(this._readableStream.headers); const { allowRangeRequests, suggestedLength } = validateRangeRequestCapabilities({ - getResponseHeader, + responseHeaders, isHttp: stream.isHttp, rangeChunkSize: this._rangeChunkSize, disableRange: this._disableRange, @@ -322,7 +319,7 @@ class PDFNodeStreamFullReader extends BaseFullReader { // Setting right content length. this._contentLength = suggestedLength || this._contentLength; - this._filename = extractFilenameFromHeader(getResponseHeader); + this._filename = extractFilenameFromHeader(responseHeaders); }; this._request = createRequest(this._url, headers, handleResponse); diff --git a/test/unit/network_utils_spec.js b/test/unit/network_utils_spec.js index 5a73dc8ae82b3..e697e24deeb74 100644 --- a/test/unit/network_utils_spec.js +++ b/test/unit/network_utils_spec.js @@ -84,12 +84,9 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: true, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Content-Length") { - return 8; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Content-Length": 8, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -101,12 +98,9 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: false, - getResponseHeader: headerName => { - if (headerName === "Content-Length") { - return 8; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Content-Length": 8, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -120,14 +114,10 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Accept-Ranges") { - return "none"; - } else if (headerName === "Content-Length") { - return 8; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Accept-Ranges": "none", + "Content-Length": 8, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -141,16 +131,11 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Accept-Ranges") { - return "bytes"; - } else if (headerName === "Content-Encoding") { - return "gzip"; - } else if (headerName === "Content-Length") { - return 8; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Accept-Ranges": "bytes", + "Content-Encoding": "gzip", + "Content-Length": 8, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -164,16 +149,10 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Accept-Ranges") { - return "bytes"; - } else if (headerName === "Content-Encoding") { - return null; - } else if (headerName === "Content-Length") { - return "eight"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Accept-Ranges": "bytes", + "Content-Length": "eight", + }), rangeChunkSize: 64, }) ).toEqual({ @@ -187,16 +166,10 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Accept-Ranges") { - return "bytes"; - } else if (headerName === "Content-Encoding") { - return null; - } else if (headerName === "Content-Length") { - return 8; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Accept-Ranges": "bytes", + "Content-Length": 8, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -210,16 +183,10 @@ describe("network_utils", function () { validateRangeRequestCapabilities({ disableRange: false, isHttp: true, - getResponseHeader: headerName => { - if (headerName === "Accept-Ranges") { - return "bytes"; - } else if (headerName === "Content-Encoding") { - return null; - } else if (headerName === "Content-Length") { - return 8192; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }, + responseHeaders: new Headers({ + "Accept-Ranges": "bytes", + "Content-Length": 8192, + }), rangeChunkSize: 64, }) ).toEqual({ @@ -232,194 +199,173 @@ describe("network_utils", function () { describe("extractFilenameFromHeader", function () { it("returns null when content disposition header is blank", function () { expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return null; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) - ).toBeNull(); - - expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return undefined; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + // Empty headers. + }) + ) ).toBeNull(); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return ""; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "", + }) + ) ).toBeNull(); }); it("gets the filename from the response header", function () { expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "inline"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "inline", + }) + ) ).toBeNull(); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "attachment", + }) + ) ).toBeNull(); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="filename.pdf"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": 'attachment; filename="filename.pdf"', + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="filename.pdf and spaces.pdf"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + 'attachment; filename="filename.pdf and spaces.pdf"', + }) + ) ).toEqual("filename.pdf and spaces.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="tl;dr.pdf"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": 'attachment; filename="tl;dr.pdf"', + }) + ) ).toEqual("tl;dr.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename=filename.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "attachment; filename=filename.pdf", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename=filename.pdf someotherparam"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + "attachment; filename=filename.pdf someotherparam", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="%e4%b8%ad%e6%96%87.pdf"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + 'attachment; filename="%e4%b8%ad%e6%96%87.pdf"', + }) + ) ).toEqual("中文.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="100%.pdf"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": 'attachment; filename="100%.pdf"', + }) + ) ).toEqual("100%.pdf"); }); it("gets the filename from the response header (RFC 6266)", function () { expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename*=filename.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "attachment; filename*=filename.pdf", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename*=''filename.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "attachment; filename*=''filename.pdf", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename*=utf-8''filename.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": "attachment; filename*=utf-8''filename.pdf", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename=no.pdf; filename*=utf-8''filename.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + "attachment; filename=no.pdf; filename*=utf-8''filename.pdf", + }) + ) ).toEqual("filename.pdf"); expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename*=utf-8''filename.pdf; filename=no.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + "attachment; filename*=utf-8''filename.pdf; filename=no.pdf", + }) + ) ).toEqual("filename.pdf"); }); it("gets the filename from the response header (RFC 2231)", function () { // Tests continuations (RFC 2231 section 3, via RFC 5987 section 3.1). expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return "attachment; filename*0=filename; filename*1=.pdf"; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + "attachment; filename*0=filename; filename*1=.pdf", + }) + ) ).toEqual("filename.pdf"); }); it("only extracts filename with pdf extension", function () { expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'attachment; filename="filename.png"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": 'attachment; filename="filename.png"', + }) + ) ).toBeNull(); }); it("extension validation is case insensitive", function () { expect( - extractFilenameFromHeader(headerName => { - if (headerName === "Content-Disposition") { - return 'form-data; name="fieldName"; filename="file.PdF"'; - } - throw new Error(`Unexpected headerName: ${headerName}`); - }) + extractFilenameFromHeader( + new Headers({ + "Content-Disposition": + 'form-data; name="fieldName"; filename="file.PdF"', + }) + ) ).toEqual("file.PdF"); }); }); From 2a01931e4a6148fa8a9c591a1a0ba93cc1910cfd Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 7 Sep 2024 12:36:34 +0200 Subject: [PATCH 2/2] Use the `_headersCapability` name in `PDFNetworkStreamFullRequestReader` This improves consistency in the code-base since the implementations with the Fetch API respectively Node.js uses that name. --- src/display/network.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/display/network.js b/src/display/network.js index fe0c901bd261f..8ce4e1356cff6 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -249,7 +249,7 @@ class PDFNetworkStreamFullRequestReader { }; this._url = source.url; this._fullRequestId = manager.requestFull(args); - this._headersReceivedCapability = Promise.withResolvers(); + this._headersCapability = Promise.withResolvers(); this._disableRange = source.disableRange || false; this._contentLength = source.length; // Optional this._rangeChunkSize = source.rangeChunkSize; @@ -308,7 +308,7 @@ class PDFNetworkStreamFullRequestReader { this._manager.abortRequest(fullRequestXhrId); } - this._headersReceivedCapability.resolve(); + this._headersCapability.resolve(); } _onDone(data) { @@ -332,7 +332,7 @@ class PDFNetworkStreamFullRequestReader { _onError(status) { this._storedError = createResponseStatusError(status, this._url); - this._headersReceivedCapability.reject(this._storedError); + this._headersCapability.reject(this._storedError); for (const requestCapability of this._requests) { requestCapability.reject(this._storedError); } @@ -364,7 +364,7 @@ class PDFNetworkStreamFullRequestReader { } get headersReady() { - return this._headersReceivedCapability.promise; + return this._headersCapability.promise; } async read() { @@ -385,7 +385,7 @@ class PDFNetworkStreamFullRequestReader { cancel(reason) { this._done = true; - this._headersReceivedCapability.reject(reason); + this._headersCapability.reject(reason); for (const requestCapability of this._requests) { requestCapability.resolve({ value: undefined, done: true }); }