diff --git a/src/display/api.js b/src/display/api.js index cf11866adb240c..5749d514da3f75 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -450,15 +450,20 @@ function getDocument(src = {}) { PDFJSDev.test("GENERIC") && isNodeJS ) { - const isFetchSupported = - typeof fetch !== "undefined" && - typeof Response !== "undefined" && - "body" in Response.prototype; - - NetworkStream = - isFetchSupported && isValidFetchUrl(url) - ? PDFFetchStream - : PDFNodeStream; + if (isValidFetchUrl(url)) { + if ( + typeof fetch === "undefined" || + typeof Response === "undefined" || + !("body" in Response.prototype) + ) { + throw new Error( + "getDocument - the Fetch API was disabled in Node.js, see `--no-experimental-fetch`." + ); + } + NetworkStream = PDFFetchStream; + } else { + NetworkStream = PDFNodeStream; + } } else { NetworkStream = isValidFetchUrl(url) ? PDFFetchStream diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 069f025da52ff8..9223c5aa9cd92b 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -15,11 +15,6 @@ /* globals process */ import { AbortException, assert, MissingPDFException } from "../shared/util.js"; -import { - createHeaders, - extractFilenameFromHeader, - validateRangeRequestCapabilities, -} from "./network_utils.js"; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { throw new Error( @@ -37,24 +32,14 @@ function parseUrlOrPath(sourceUrl) { return new URL(url.pathToFileURL(sourceUrl)); } -function createRequest(url, headers, callback) { - if (url.protocol === "http:") { - const http = process.getBuiltinModule("http"); - return http.request(url, { headers }, callback); - } - const https = process.getBuiltinModule("https"); - return https.request(url, { headers }, callback); -} - class PDFNodeStream { constructor(source) { this.source = source; this.url = parseUrlOrPath(source.url); - this.isHttp = - this.url.protocol === "http:" || this.url.protocol === "https:"; - // Check if url refers to filesystem. - this.isFsUrl = this.url.protocol === "file:"; - this.headers = createHeaders(this.isHttp, source.httpHeaders); + assert( + this.url.protocol === "file:", + "PDFNodeStream only supports file:// URLs." + ); this._fullRequestReader = null; this._rangeRequestReaders = []; @@ -69,9 +54,7 @@ class PDFNodeStream { !this._fullRequestReader, "PDFNodeStream.getFullReader can only be called once." ); - this._fullRequestReader = this.isFsUrl - ? new PDFNodeStreamFsFullReader(this) - : new PDFNodeStreamFullReader(this); + this._fullRequestReader = new PDFNodeStreamFsFullReader(this); return this._fullRequestReader; } @@ -79,9 +62,7 @@ class PDFNodeStream { if (end <= this._progressiveDataLength) { return null; } - const rangeReader = this.isFsUrl - ? new PDFNodeStreamFsRangeReader(this, start, end) - : new PDFNodeStreamRangeReader(this, start, end); + const rangeReader = new PDFNodeStreamFsRangeReader(this, start, end); this._rangeRequestReaders.push(rangeReader); return rangeReader; } @@ -288,79 +269,6 @@ class BaseRangeReader { } } -class PDFNodeStreamFullReader extends BaseFullReader { - constructor(stream) { - super(stream); - - // Node.js requires the `headers` to be a regular Object. - const headers = Object.fromEntries(stream.headers); - - const handleResponse = response => { - if (response.statusCode === 404) { - const error = new MissingPDFException(`Missing PDF "${this._url}".`); - this._storedError = error; - this._headersCapability.reject(error); - return; - } - this._headersCapability.resolve(); - this._setReadableStream(response); - - const responseHeaders = new Headers(this._readableStream.headers); - - const { allowRangeRequests, suggestedLength } = - validateRangeRequestCapabilities({ - responseHeaders, - isHttp: stream.isHttp, - rangeChunkSize: this._rangeChunkSize, - disableRange: this._disableRange, - }); - - this._isRangeSupported = allowRangeRequests; - // Setting right content length. - this._contentLength = suggestedLength || this._contentLength; - - this._filename = extractFilenameFromHeader(responseHeaders); - }; - - this._request = createRequest(this._url, headers, handleResponse); - - this._request.on("error", reason => { - this._storedError = reason; - this._headersCapability.reject(reason); - }); - // Note: `request.end(data)` is used to write `data` to request body - // and notify end of request. But one should always call `request.end()` - // even if there is no data to write -- (to notify the end of request). - this._request.end(); - } -} - -class PDFNodeStreamRangeReader extends BaseRangeReader { - constructor(stream, start, end) { - super(stream); - - // Node.js requires the `headers` to be a regular Object. - const headers = Object.fromEntries(stream.headers); - headers.Range = `bytes=${start}-${end - 1}`; - - const handleResponse = response => { - if (response.statusCode === 404) { - const error = new MissingPDFException(`Missing PDF "${this._url}".`); - this._storedError = error; - return; - } - this._setReadableStream(response); - }; - - this._request = createRequest(this._url, headers, handleResponse); - - this._request.on("error", reason => { - this._storedError = reason; - }); - this._request.end(); - } -} - class PDFNodeStreamFsFullReader extends BaseFullReader { constructor(stream) { super(stream); diff --git a/test/unit/node_stream_spec.js b/test/unit/node_stream_spec.js index 085164bb70526a..09ef917281c7d5 100644 --- a/test/unit/node_stream_spec.js +++ b/test/unit/node_stream_spec.js @@ -14,7 +14,6 @@ */ import { AbortException, isNodeJS } from "../../src/shared/util.js"; -import { createTemporaryNodeServer } from "./test_utils.js"; import { PDFNodeStream } from "../../src/display/node_stream.js"; // Ensure that these tests only run in Node.js environments. @@ -25,33 +24,12 @@ if (!isNodeJS) { } describe("node_stream", function () { - let tempServer = null; - const url = process.getBuiltinModule("url"); const cwdURL = url.pathToFileURL(process.cwd()) + "/"; const pdf = new URL("./test/pdfs/tracemonkey.pdf", cwdURL).href; const pdfLength = 1016315; - beforeAll(function () { - tempServer = createTemporaryNodeServer(); - }); - - afterAll(function () { - // Close the server from accepting new connections after all test finishes. - const { server } = tempServer; - server.close(); - - tempServer = null; - }); - - it("read both http(s) and filesystem pdf files", async function () { - const stream1 = new PDFNodeStream({ - url: `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`, - rangeChunkSize: 65536, - disableStream: true, - disableRange: true, - }); - + it("read filesystem pdf files", async function () { const stream2 = new PDFNodeStream({ url: pdf, rangeChunkSize: 65536, @@ -59,32 +37,15 @@ describe("node_stream", function () { disableRange: true, }); - const fullReader1 = stream1.getFullReader(); const fullReader2 = stream2.getFullReader(); - let isStreamingSupported1, isRangeSupported1; - const promise1 = fullReader1.headersReady.then(() => { - isStreamingSupported1 = fullReader1.isStreamingSupported; - isRangeSupported1 = fullReader1.isRangeSupported; - }); - let isStreamingSupported2, isRangeSupported2; const promise2 = fullReader2.headersReady.then(() => { isStreamingSupported2 = fullReader2.isStreamingSupported; isRangeSupported2 = fullReader2.isRangeSupported; }); - let len1 = 0, - len2 = 0; - const read1 = function () { - return fullReader1.read().then(function (result) { - if (result.done) { - return undefined; - } - len1 += result.value.byteLength; - return read1(); - }); - }; + let len2 = 0; const read2 = function () { return fullReader2.read().then(function (result) { if (result.done) { @@ -95,25 +56,15 @@ describe("node_stream", function () { }); }; - await Promise.all([read1(), read2(), promise1, promise2]); + await Promise.all([read2(), promise2]); - expect(isStreamingSupported1).toEqual(false); - expect(isRangeSupported1).toEqual(false); expect(isStreamingSupported2).toEqual(false); expect(isRangeSupported2).toEqual(false); - expect(len1).toEqual(pdfLength); - expect(len1).toEqual(len2); + expect(len2).toEqual(pdfLength); }); - it("read custom ranges for both http(s) and filesystem urls", async function () { + it("read custom ranges for filesystem urls", async function () { const rangeSize = 32768; - const stream1 = new PDFNodeStream({ - url: `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`, - length: pdfLength, - rangeChunkSize: rangeSize, - disableStream: true, - disableRange: false, - }); const stream2 = new PDFNodeStream({ url: pdf, length: pdfLength, @@ -122,23 +73,13 @@ describe("node_stream", function () { disableRange: false, }); - const fullReader1 = stream1.getFullReader(); const fullReader2 = stream2.getFullReader(); - let isStreamingSupported1, isRangeSupported1, fullReaderCancelled1; let isStreamingSupported2, isRangeSupported2, fullReaderCancelled2; - - const promise1 = fullReader1.headersReady.then(function () { - isStreamingSupported1 = fullReader1.isStreamingSupported; - isRangeSupported1 = fullReader1.isRangeSupported; - // we shall be able to close the full reader without issues - fullReader1.cancel(new AbortException("Don't need fullReader1.")); - fullReaderCancelled1 = true; - }); - const promise2 = fullReader2.headersReady.then(function () { isStreamingSupported2 = fullReader2.isStreamingSupported; isRangeSupported2 = fullReader2.isRangeSupported; + // we shall be able to close the full reader without issues fullReader2.cancel(new AbortException("Don't need fullReader2.")); fullReaderCancelled2 = true; }); @@ -146,15 +87,6 @@ describe("node_stream", function () { // Skipping fullReader results, requesting something from the PDF end. const tailSize = pdfLength % rangeSize || rangeSize; - const range11Reader = stream1.getRangeReader( - pdfLength - tailSize - rangeSize, - pdfLength - tailSize - ); - const range12Reader = stream1.getRangeReader( - pdfLength - tailSize, - pdfLength - ); - const range21Reader = stream2.getRangeReader( pdfLength - tailSize - rangeSize, pdfLength - tailSize @@ -164,11 +96,8 @@ describe("node_stream", function () { pdfLength ); - const result11 = { value: 0 }, - result12 = { value: 0 }; const result21 = { value: 0 }, result22 = { value: 0 }; - const read = function (reader, lenResult) { return reader.read().then(function (result) { if (result.done) { @@ -180,21 +109,13 @@ describe("node_stream", function () { }; await Promise.all([ - read(range11Reader, result11), - read(range12Reader, result12), read(range21Reader, result21), read(range22Reader, result22), - promise1, promise2, ]); - expect(result11.value).toEqual(rangeSize); - expect(result12.value).toEqual(tailSize); expect(result21.value).toEqual(rangeSize); expect(result22.value).toEqual(tailSize); - expect(isStreamingSupported1).toEqual(false); - expect(isRangeSupported1).toEqual(true); - expect(fullReaderCancelled1).toEqual(true); expect(isStreamingSupported2).toEqual(false); expect(isRangeSupported2).toEqual(true); expect(fullReaderCancelled2).toEqual(true);