Skip to content

Commit

Permalink
[api-minor] Expose response headers, when available, in `PDFDocumentL…
Browse files Browse the repository at this point in the history
…oadingTask`

This is a feature that's been requested a few times over the years, see e.g. issue 14630, 16341, and 18792.

Note that by placing the `getResponseHeaders` method in `PDFDocumentLoadingTask` it's possible to access the response headers even if loading failed, e.g. with `MissingPDFException`, which wouldn't have been possible if the method was placed elsewhere (e.g. in `PDFDocumentProxy`).
Given the following JSDoc comment, this also seems generally reasonable: https://github.com/mozilla/pdf.js/blob/689ffda9df29f24e03f6e5145a096584d7f166cc/src/display/api.js#L570-L575

Considering that this functionality isn't relevant in e.g. the Firefox PDF Viewer, pre-processor statements are being used to limit the code-size impact of the implementation to GENERIC-builds as much as possible.
  • Loading branch information
Snuffleupagus committed Nov 24, 2024
1 parent f911635 commit 1ae0c25
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 7 deletions.
37 changes: 37 additions & 0 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,26 @@ class PDFDocumentLoadingTask {
this._worker = null;
}
}

/**
* @returns {Promise<Headers | null>}
*/
async getResponseHeaders() {
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
throw new Error("Not implemented: getResponseHeaders");
}
try {
await this.promise;
} catch {}

if (this.destroyed) {
throw new Error("LoadingTask destroyed.");
}
if (!this._transport) {
throw new Error("WorkerTransport unset.");
}
return this._transport._responseHeaders;
}
}

/**
Expand Down Expand Up @@ -2450,6 +2470,23 @@ class WorkerTransport {

this.setupMessageHandler();

if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// NOTE: This getter should *not* be invoked until after
// the `IPDFStreamReader.headersReady`-promise has settled.
Object.defineProperty(this, "_responseHeaders", {
get() {
if (!networkStream) {
return null;
}
assert(this._fullReader, "No `IPDFStreamReader`-instance available.");

const headers = this._fullReader.responseHeaders;
// Always create a copy of the *internal* response headers.
return headers ? new Headers(headers) : null;
},
});
}

if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
// For testing purposes.
Object.defineProperty(this, "getNetworkStreamName", {
Expand Down
10 changes: 8 additions & 2 deletions src/display/fetch_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ class PDFFetchStream {

/** @implements {IPDFStreamReader} */
class PDFFetchStreamReader {
#responseHeaders = null;

constructor(stream) {
this._stream = stream;
this._reader = null;
Expand Down Expand Up @@ -126,14 +128,14 @@ class PDFFetchStreamReader {
.then(response => {
stream._responseOrigin = getResponseOrigin(response.url);

const responseHeaders = (this.#responseHeaders = response.headers);

if (!validateResponseStatus(response.status)) {
throw createResponseStatusError(response.status, url);
}
this._reader = response.body.getReader();
this._headersCapability.resolve();

const responseHeaders = response.headers;

const { allowRangeRequests, suggestedLength } =
validateRangeRequestCapabilities({
responseHeaders,
Expand Down Expand Up @@ -163,6 +165,10 @@ class PDFFetchStreamReader {
return this._headersCapability.promise;
}

get responseHeaders() {
return this.#responseHeaders;
}

get filename() {
return this._filename;
}
Expand Down
10 changes: 8 additions & 2 deletions src/display/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ class PDFNetworkStream {

/** @implements {IPDFStreamReader} */
class PDFNetworkStreamFullRequestReader {
#responseHeaders = null;

constructor(manager, source) {
this._manager = manager;

Expand Down Expand Up @@ -281,7 +283,7 @@ class PDFNetworkStreamFullRequestReader {
);

const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders();
const responseHeaders = new Headers(
const responseHeaders = (this.#responseHeaders = new Headers(
rawResponseHeaders
? rawResponseHeaders
.trim()
Expand All @@ -291,7 +293,7 @@ class PDFNetworkStreamFullRequestReader {
return [key, val.join(": ")];
})
: []
);
));

const { allowRangeRequests, suggestedLength } =
validateRangeRequestCapabilities({
Expand Down Expand Up @@ -376,6 +378,10 @@ class PDFNetworkStreamFullRequestReader {
return this._headersCapability.promise;
}

get responseHeaders() {
return this.#responseHeaders;
}

async read() {
await this._headersCapability.promise;

Expand Down
4 changes: 4 additions & 0 deletions src/display/node_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class PDFNodeStreamFsFullReader {
return this._headersCapability.promise;
}

get responseHeaders() {
return null;
}

get filename() {
return this._filename;
}
Expand Down
4 changes: 4 additions & 0 deletions src/display/transport_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ class PDFDataTransportStreamReader {
return this._headersReady;
}

get responseHeaders() {
return null;
}

get filename() {
return this._filename;
}
Expand Down
19 changes: 16 additions & 3 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ describe("api", function () {
// Ensure that the Fetch API was used to load the PDF document.
expect(pdfDocument.getNetworkStreamName()).toEqual("PDFFetchStream");

const responseHeaders = await loadingTask.getResponseHeaders();
expect([...responseHeaders.keys()].length).toBeGreaterThan(0);

await loadingTask.destroy();
});

Expand Down Expand Up @@ -235,16 +238,19 @@ describe("api", function () {
progressReportedCapability.resolve(data);
};

const data = await Promise.all([
const [pdfDocument, progress] = await Promise.all([
loadingTask.promise,
progressReportedCapability.promise,
]);
expect(data[0] instanceof PDFDocumentProxy).toEqual(true);
expect(data[1].loaded / data[1].total).toEqual(1);
expect(pdfDocument instanceof PDFDocumentProxy).toEqual(true);
expect(progress.loaded / progress.total).toEqual(1);

// Check that the TypedArray was transferred.
expect(typedArrayPdf.length).toEqual(0);

const responseHeaders = await loadingTask.getResponseHeaders();
expect(responseHeaders).toBeNull();

await loadingTask.destroy();
});

Expand Down Expand Up @@ -311,6 +317,13 @@ describe("api", function () {
expect(reason instanceof MissingPDFException).toEqual(true);
}

const responseHeaders = await loadingTask.getResponseHeaders();
if (isNodeJS) {
expect(responseHeaders).toBeNull();
} else {
expect([...responseHeaders.keys()].length).toBeGreaterThan(0);
}

await loadingTask.destroy();
});

Expand Down

0 comments on commit 1ae0c25

Please sign in to comment.