From 1f54424698ff074a9d3b340853fe32758340e448 Mon Sep 17 00:00:00 2001
From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com>
Date: Fri, 29 Mar 2024 17:21:31 +0300
Subject: [PATCH] test: more (#1800)
---
src/index.js | 4 +-
src/middleware.js | 321 ++++++++++++++++++++------------
src/utils/compatibleAPI.js | 44 ++++-
src/utils/getFilenameFromUrl.js | 39 +---
src/utils/memorize.js | 43 +++++
test/middleware.test.js | 193 +++++++++++++++----
types/index.d.ts | 8 +-
types/utils/compatibleAPI.d.ts | 16 ++
types/utils/memorize.d.ts | 26 +++
9 files changed, 495 insertions(+), 199 deletions(-)
create mode 100644 src/utils/memorize.js
create mode 100644 types/utils/memorize.d.ts
diff --git a/src/index.js b/src/index.js
index d0c634c40..de2419f10 100644
--- a/src/index.js
+++ b/src/index.js
@@ -59,7 +59,7 @@ const noop = () => {};
/**
* @typedef {Object} ResponseData
- * @property {string | Buffer | ReadStream} data
+ * @property {Buffer | ReadStream} data
* @property {number} byteLength
*/
@@ -69,7 +69,7 @@ const noop = () => {};
* @callback ModifyResponseData
* @param {RequestInternal} req
* @param {ResponseInternal} res
- * @param {string | Buffer | ReadStream} data
+ * @param {Buffer | ReadStream} data
* @param {number} byteLength
* @return {ResponseData}
*/
diff --git a/src/middleware.js b/src/middleware.js
index e1bf81125..dcf1217ef 100644
--- a/src/middleware.js
+++ b/src/middleware.js
@@ -5,9 +5,15 @@ const mime = require("mime-types");
const onFinishedStream = require("on-finished");
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
-const { setStatusCode, send, pipe } = require("./utils/compatibleAPI");
+const {
+ setStatusCode,
+ send,
+ pipe,
+ createReadStreamOrReadFileSync,
+} = require("./utils/compatibleAPI");
const ready = require("./utils/ready");
const parseTokenList = require("./utils/parseTokenList");
+const memorize = require("./utils/memorize");
/** @typedef {import("./index.js").NextFunction} NextFunction */
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
@@ -84,6 +90,21 @@ const statuses = {
500: "Internal Server Error",
};
+const parseRangeHeaders = memorize(
+ /**
+ * @param {string} value
+ * @returns {import("range-parser").Result | import("range-parser").Ranges}
+ */
+ (value) => {
+ const [len, rangeHeader] = value.split("|");
+
+ // eslint-disable-next-line global-require
+ return require("range-parser")(Number(len), rangeHeader, {
+ combine: true,
+ });
+ },
+);
+
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
@@ -141,7 +162,8 @@ function wrapper(context) {
// eslint-disable-next-line global-require
const escapeHtml = require("./utils/escapeHtml");
const content = statuses[status] || String(status);
- let document = `
+ let document = Buffer.from(
+ `
@@ -150,7 +172,9 @@ function wrapper(context) {
${escapeHtml(content)}
-`;
+`,
+ "utf-8",
+ );
// Clear existing headers
const headers = res.getHeaderNames();
@@ -182,7 +206,7 @@ function wrapper(context) {
if (options && options.modifyResponseData) {
({ data: document, byteLength } =
- /** @type {{data: string, byteLength: number }} */
+ /** @type {{ data: Buffer, byteLength: number }} */
(options.modifyResponseData(req, res, document, byteLength)));
}
@@ -267,9 +291,16 @@ function wrapper(context) {
return false;
}
- // if-none-match
+ // fields
const noneMatch = req.headers["if-none-match"];
+ const modifiedSince = req.headers["if-modified-since"];
+ // unconditional request
+ if (!noneMatch && !modifiedSince) {
+ return false;
+ }
+
+ // if-none-match
if (noneMatch && noneMatch !== "*") {
if (!resHeaders.etag) {
return false;
@@ -305,21 +336,15 @@ function wrapper(context) {
}
// if-modified-since
- const modifiedSince = req.headers["if-modified-since"];
-
if (modifiedSince) {
const lastModified = resHeaders["last-modified"];
- const parsedHttpDate = parseHttpDate(modifiedSince);
// A recipient MUST ignore the If-Modified-Since header field if the
// received field-value is not a valid HTTP-date, or if the request
// method is neither GET nor HEAD.
- if (isNaN(parsedHttpDate)) {
- return true;
- }
-
const modifiedStale =
- !lastModified || !(parseHttpDate(lastModified) <= parsedHttpDate);
+ !lastModified ||
+ !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
if (modifiedStale) {
return false;
@@ -361,6 +386,43 @@ function wrapper(context) {
return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
}
+ /**
+ * @returns {string | undefined}
+ */
+ function getRangeHeader() {
+ const rage = req.headers.range;
+
+ if (rage && BYTES_RANGE_REGEXP.test(rage)) {
+ return rage;
+ }
+
+ // eslint-disable-next-line no-undefined
+ return undefined;
+ }
+
+ /**
+ * @param {import("range-parser").Range} range
+ * @returns {[number, number]}
+ */
+ function getOffsetAndLenFromRange(range) {
+ const offset = range.start;
+ const len = range.end - range.start + 1;
+
+ return [offset, len];
+ }
+
+ /**
+ * @param {number} offset
+ * @param {number} len
+ * @returns {[number, number]}
+ */
+ function calcStartAndEnd(offset, len) {
+ const start = offset;
+ const end = Math.max(offset, offset + len - 1);
+
+ return [start, end];
+ }
+
async function processRequest() {
// Pipe and SendFile
/** @type {import("./utils/getFilenameFromUrl").Extra} */
@@ -389,6 +451,11 @@ function wrapper(context) {
return;
}
+ const { size } = /** @type {import("fs").Stats} */ (extra.stats);
+
+ let len = size;
+ let offset = 0;
+
// Send logic
let { headers } = context.options;
@@ -433,125 +500,75 @@ function wrapper(context) {
res.setHeader("Accept-Ranges", "bytes");
}
- let len = /** @type {import("fs").Stats} */ (extra.stats).size;
- let offset = 0;
-
- const rangeHeader = /** @type {string} */ (req.headers.range);
-
- if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
- let parsedRanges =
- /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */
- (
- // eslint-disable-next-line global-require
- require("range-parser")(len, rangeHeader, {
- combine: true,
- })
- );
-
- // If-Range support
- if (!isRangeFresh()) {
- parsedRanges = [];
- }
-
- if (parsedRanges === -1) {
- context.logger.error("Unsatisfiable range for 'Range' header.");
-
- res.setHeader(
- "Content-Range",
- getValueContentRangeHeader("bytes", len),
- );
-
- sendError(416, {
- headers: {
- "Content-Range": res.getHeader("Content-Range"),
- },
- modifyResponseData: context.options.modifyResponseData,
- });
-
- return;
- } else if (parsedRanges === -2) {
- context.logger.error(
- "A malformed 'Range' header was provided. A regular response will be sent for this request.",
- );
- } else if (parsedRanges.length > 1) {
- context.logger.error(
- "A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.",
- );
- }
-
- if (parsedRanges !== -2 && parsedRanges.length === 1) {
- // Content-Range
- setStatusCode(res, 206);
- res.setHeader(
- "Content-Range",
- getValueContentRangeHeader(
- "bytes",
- len,
- /** @type {import("range-parser").Ranges} */ (parsedRanges)[0],
- ),
- );
+ if (context.options.lastModified && !res.getHeader("Last-Modified")) {
+ const modified =
+ /** @type {import("fs").Stats} */
+ (extra.stats).mtime.toUTCString();
- offset += parsedRanges[0].start;
- len = parsedRanges[0].end - parsedRanges[0].start + 1;
- }
+ res.setHeader("Last-Modified", modified);
}
- const start = offset;
- const end = Math.max(offset, offset + len - 1);
+ /** @type {number} */
+ let start;
+ /** @type {number} */
+ let end;
- // Stream logic
- const isFsSupportsStream =
- typeof context.outputFileSystem.createReadStream === "function";
-
- /** @type {string | Buffer | ReadStream} */
+ /** @type {undefined | Buffer | ReadStream} */
let bufferOrStream;
+ /** @type {number} */
let byteLength;
- try {
- if (isFsSupportsStream) {
- bufferOrStream =
- /** @type {import("fs").createReadStream} */
- (context.outputFileSystem.createReadStream)(filename, {
- start,
- end,
- });
+ const rangeHeader = getRangeHeader();
- // Handle files with zero bytes
- byteLength = end === 0 ? 0 : end - start + 1;
+ if (context.options.etag && !res.getHeader("ETag")) {
+ /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */
+ let value;
+
+ if (context.options.etag === "weak") {
+ value = /** @type {import("fs").Stats} */ (extra.stats);
} else {
- bufferOrStream = /** @type {import("fs").readFileSync} */ (
- context.outputFileSystem.readFileSync
- )(filename);
- ({ byteLength } = bufferOrStream);
- }
- } catch (_ignoreError) {
- await goNext();
+ if (rangeHeader) {
+ const parsedRanges =
+ /** @type {import("range-parser").Ranges | import("range-parser").Result} */
+ (parseRangeHeaders(`${size}|${rangeHeader}`));
+
+ if (
+ parsedRanges !== -2 &&
+ parsedRanges !== -1 &&
+ parsedRanges.length === 1
+ ) {
+ [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
+ }
+ }
- return;
- }
+ [start, end] = calcStartAndEnd(offset, len);
- if (context.options.lastModified && !res.getHeader("Last-Modified")) {
- const modified =
- /** @type {import("fs").Stats} */
- (extra.stats).mtime.toUTCString();
+ try {
+ const result = createReadStreamOrReadFileSync(
+ filename,
+ context.outputFileSystem,
+ start,
+ end,
+ );
- res.setHeader("Last-Modified", modified);
- }
+ value = result.bufferOrStream;
+ ({ bufferOrStream, byteLength } = result);
+ } catch (_err) {
+ // Ignore here
+ }
+ }
- if (context.options.etag && !res.getHeader("ETag")) {
- const value =
- context.options.etag === "weak"
- ? /** @type {import("fs").Stats} */ (extra.stats)
- : bufferOrStream;
+ if (value) {
+ // eslint-disable-next-line global-require
+ const result = await require("./utils/etag")(value);
- // eslint-disable-next-line global-require
- const val = await require("./utils/etag")(value);
+ // Because we already read stream, we can cache buffer to avoid extra read from fs
+ if (result.buffer) {
+ bufferOrStream = result.buffer;
+ }
- if (val.buffer) {
- bufferOrStream = val.buffer;
+ res.setHeader("ETag", result.hash);
}
-
- res.setHeader("ETag", val.hash);
}
// Conditional GET support
@@ -592,16 +609,88 @@ function wrapper(context) {
}
}
+ if (rangeHeader) {
+ let parsedRanges =
+ /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */
+ (parseRangeHeaders(`${size}|${rangeHeader}`));
+
+ // If-Range support
+ if (!isRangeFresh()) {
+ parsedRanges = [];
+ }
+
+ if (parsedRanges === -1) {
+ context.logger.error("Unsatisfiable range for 'Range' header.");
+
+ res.setHeader(
+ "Content-Range",
+ getValueContentRangeHeader("bytes", size),
+ );
+
+ sendError(416, {
+ headers: {
+ "Content-Range": res.getHeader("Content-Range"),
+ },
+ modifyResponseData: context.options.modifyResponseData,
+ });
+
+ return;
+ } else if (parsedRanges === -2) {
+ context.logger.error(
+ "A malformed 'Range' header was provided. A regular response will be sent for this request.",
+ );
+ } else if (parsedRanges.length > 1) {
+ context.logger.error(
+ "A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.",
+ );
+ }
+
+ if (parsedRanges !== -2 && parsedRanges.length === 1) {
+ // Content-Range
+ setStatusCode(res, 206);
+ res.setHeader(
+ "Content-Range",
+ getValueContentRangeHeader(
+ "bytes",
+ size,
+ /** @type {import("range-parser").Ranges} */ (parsedRanges)[0],
+ ),
+ );
+
+ [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
+ }
+ }
+
+ // When strong Etag generation is enabled we already read file, so we can skip extra fs call
+ if (!bufferOrStream) {
+ [start, end] = calcStartAndEnd(offset, len);
+
+ try {
+ ({ bufferOrStream, byteLength } = createReadStreamOrReadFileSync(
+ filename,
+ context.outputFileSystem,
+ start,
+ end,
+ ));
+ } catch (_ignoreError) {
+ await goNext();
+
+ return;
+ }
+ }
+
if (context.options.modifyResponseData) {
({ data: bufferOrStream, byteLength } =
context.options.modifyResponseData(
req,
res,
bufferOrStream,
+ // @ts-ignore
byteLength,
));
}
+ // @ts-ignore
res.setHeader("Content-Length", byteLength);
if (req.method === "HEAD") {
diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js
index 2d819e8b4..ab315b6f0 100644
--- a/src/utils/compatibleAPI.js
+++ b/src/utils/compatibleAPI.js
@@ -62,4 +62,46 @@ function send(res, bufferOrStream) {
res.end(bufferOrStream);
}
-module.exports = { setStatusCode, send, pipe };
+/**
+ * @param {string} filename
+ * @param {import("../index").OutputFileSystem} outputFileSystem
+ * @param {number} start
+ * @param {number} end
+ * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }}
+ */
+function createReadStreamOrReadFileSync(
+ filename,
+ outputFileSystem,
+ start,
+ end,
+) {
+ /** @type {string | Buffer | import("fs").ReadStream} */
+ let bufferOrStream;
+ /** @type {number} */
+ let byteLength;
+
+ // Stream logic
+ const isFsSupportsStream =
+ typeof outputFileSystem.createReadStream === "function";
+
+ if (isFsSupportsStream) {
+ bufferOrStream =
+ /** @type {import("fs").createReadStream} */
+ (outputFileSystem.createReadStream)(filename, {
+ start,
+ end,
+ });
+
+ // Handle files with zero bytes
+ byteLength = end === 0 ? 0 : end - start + 1;
+ } else {
+ bufferOrStream =
+ /** @type {import("fs").readFileSync} */
+ (outputFileSystem.readFileSync)(filename);
+ ({ byteLength } = bufferOrStream);
+ }
+
+ return { bufferOrStream, byteLength };
+}
+
+module.exports = { setStatusCode, send, pipe, createReadStreamOrReadFileSync };
diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js
index 0cb9c05a4..319b149db 100644
--- a/src/utils/getFilenameFromUrl.js
+++ b/src/utils/getFilenameFromUrl.js
@@ -3,48 +3,13 @@ const { parse } = require("url");
const querystring = require("querystring");
const getPaths = require("./getPaths");
+const memorize = require("./memorize");
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
-const cacheStore = new WeakMap();
-
-/**
- * @template T
- * @param {Function} fn
- * @param {{ cache?: Map } | undefined} cache
- * @param {(value: T) => T} callback
- * @returns {any}
- */
-const mem = (fn, { cache = new Map() } = {}, callback) => {
- /**
- * @param {any} arguments_
- * @return {any}
- */
- const memoized = (...arguments_) => {
- const [key] = arguments_;
- const cacheItem = cache.get(key);
-
- if (cacheItem) {
- return cacheItem.data;
- }
-
- let result = fn.apply(this, arguments_);
- result = callback(result);
-
- cache.set(key, {
- data: result,
- });
-
- return result;
- };
-
- cacheStore.set(memoized, cache);
-
- return memoized;
-};
// eslint-disable-next-line no-undefined
-const memoizedParse = mem(parse, undefined, (value) => {
+const memoizedParse = memorize(parse, undefined, (value) => {
if (value.pathname) {
// eslint-disable-next-line no-param-reassign
value.pathname = decode(value.pathname);
diff --git a/src/utils/memorize.js b/src/utils/memorize.js
new file mode 100644
index 000000000..a8921157e
--- /dev/null
+++ b/src/utils/memorize.js
@@ -0,0 +1,43 @@
+const cacheStore = new WeakMap();
+
+/**
+ * @template T
+ * @param {Function} fn
+ * @param {{ cache?: Map } | undefined} cache
+ * @param {((value: T) => T)=} callback
+ * @returns {any}
+ */
+function memorize(fn, { cache = new Map() } = {}, callback) {
+ /**
+ * @param {any} arguments_
+ * @return {any}
+ */
+ const memoized = (...arguments_) => {
+ const [key] = arguments_;
+ console.log("CACHE", key);
+ const cacheItem = cache.get(key);
+
+ if (cacheItem) {
+ return cacheItem.data;
+ }
+
+ // @ts-ignore
+ let result = fn.apply(this, arguments_);
+
+ if (callback) {
+ result = callback(result);
+ }
+
+ cache.set(key, {
+ data: result,
+ });
+
+ return result;
+ };
+
+ cacheStore.set(memoized, cache);
+
+ return memoized;
+}
+
+module.exports = memorize;
diff --git a/test/middleware.test.js b/test/middleware.test.js
index c47a18b01..cab109ba7 100644
--- a/test/middleware.test.js
+++ b/test/middleware.test.js
@@ -242,6 +242,13 @@ function applyTestMiddleware(name, middlewares) {
return middlewares;
}
+function parseHttpDate(date) {
+ const timestamp = date && Date.parse(date);
+
+ // istanbul ignore next: guard against date.js Date.parse patching
+ return typeof timestamp === "number" ? timestamp : NaN;
+}
+
describe.each([
["connect", connect],
["express", express],
@@ -4238,7 +4245,7 @@ describe.each([
expect(response.headers.etag.startsWith("W/")).toBe(true);
});
- it('should return the "304" code for the "GET" request to the bundle file with etag and "if-match" header', async () => {
+ it('should return the "304" code for the "GET" request to the bundle file with etag and "if-none-match" header', async () => {
const response1 = await req.get(`/bundle.js`);
expect(response1.statusCode).toEqual(200);
@@ -4247,18 +4254,38 @@ describe.each([
const response2 = await req
.get(`/bundle.js`)
- .set("if-match", response1.headers.etag);
+ .set("if-none-match", response1.headers.etag);
expect(response2.statusCode).toEqual(304);
expect(response2.headers.etag).toBeDefined();
expect(response2.headers.etag.startsWith("W/")).toBe(true);
- const response3 = await req.get(`/bundle.js`).set("if-match", "test");
+ const response3 = await req
+ .get(`/bundle.js`)
+ .set("if-none-match", `${response1.headers.etag}, test`);
- expect(response3.statusCode).toEqual(412);
+ expect(response3.statusCode).toEqual(304);
+ expect(response3.headers.etag).toBeDefined();
+ expect(response3.headers.etag.startsWith("W/")).toBe(true);
+
+ const response4 = await req
+ .get(`/bundle.js`)
+ .set("if-none-match", "*");
+
+ expect(response4.statusCode).toEqual(304);
+ expect(response4.headers.etag).toBeDefined();
+ expect(response4.headers.etag.startsWith("W/")).toBe(true);
+
+ const response5 = await req
+ .get(`/bundle.js`)
+ .set("if-none-match", "test");
+
+ expect(response5.statusCode).toEqual(200);
+ expect(response5.headers.etag).toBeDefined();
+ expect(response5.headers.etag.startsWith("W/")).toBe(true);
});
- it('should return the "304" code for the "GET" request to the bundle file with etag "if-none-match" header', async () => {
+ it('should return the "200" code for the "GET" request to the bundle file with etag and "if-match" header', async () => {
const response1 = await req.get(`/bundle.js`);
expect(response1.statusCode).toEqual(200);
@@ -4267,19 +4294,29 @@ describe.each([
const response2 = await req
.get(`/bundle.js`)
- .set("if-none-match", response1.headers.etag);
+ .set("if-match", response1.headers.etag);
- expect(response2.statusCode).toEqual(304);
+ expect(response2.statusCode).toEqual(200);
expect(response2.headers.etag).toBeDefined();
expect(response2.headers.etag.startsWith("W/")).toBe(true);
const response3 = await req
.get(`/bundle.js`)
- .set("if-none-match", "test");
+ .set("if-match", `${response1.headers.etag}, foo`);
expect(response3.statusCode).toEqual(200);
expect(response3.headers.etag).toBeDefined();
expect(response3.headers.etag.startsWith("W/")).toBe(true);
+
+ const response4 = await req.get(`/bundle.js`).set("if-match", "*");
+
+ expect(response4.statusCode).toEqual(200);
+ expect(response4.headers.etag).toBeDefined();
+ expect(response4.headers.etag.startsWith("W/")).toBe(true);
+
+ const response5 = await req.get(`/bundle.js`).set("if-match", "test");
+
+ expect(response5.statusCode).toEqual(412);
});
it('should return the "412" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => {
@@ -4310,10 +4347,25 @@ describe.each([
expect(response2.headers.etag).toBeDefined();
expect(response2.headers.etag.startsWith("W/")).toBe(true);
});
+
+ it('should return the "206" code for the "GET" request with the valid range header and "if-range" header', async () => {
+ const response = await req
+ .get("/bundle.js")
+ .set("if-range", '"test"')
+ .set("Range", "bytes=3000-3500");
+
+ expect(response.statusCode).toEqual(200);
+ expect(response.headers["content-type"]).toEqual(
+ "application/javascript; charset=utf-8",
+ );
+ expect(response.headers.etag).toBeDefined();
+ expect(response.headers.etag.startsWith("W/")).toBe(true);
+ });
});
describe("should work and generate strong etag", () => {
beforeEach(async () => {
+ const outputPath = path.resolve(__dirname, "./outputs/basic");
const compiler = getCompiler(webpackConfig);
[server, req, instance] = await frameworkFactory(
@@ -4324,13 +4376,21 @@ describe.each([
etag: "strong",
},
);
+
+ instance.context.outputFileSystem.mkdirSync(outputPath, {
+ recursive: true,
+ });
+ instance.context.outputFileSystem.writeFileSync(
+ path.resolve(outputPath, "file.txt"),
+ "",
+ );
});
afterEach(async () => {
await close(server, instance);
});
- it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => {
+ it('should return the "200" code for the "GET" request to the bundle file and set strong etag', async () => {
const response = await req.get(`/bundle.js`);
expect(response.statusCode).toEqual(200);
@@ -4339,6 +4399,37 @@ describe.each([
'"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"',
);
});
+
+ it('should return the "200" code for the "GET" request to the file.txt and set strong etag on empty file', async () => {
+ const response = await req.get(`/file.txt`);
+
+ expect(response.statusCode).toEqual(200);
+ expect(response.headers.etag).toBe(
+ /* cspell:disable-next-line */
+ '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"',
+ );
+ });
+
+ it('should return the "200" code for the "GET" request with the valid range header and wrong "If-Range" header', async () => {
+ const response = await req
+ .get("/bundle.js")
+ .set("Range", "bytes=3000-3500");
+
+ expect(response.statusCode).toEqual(206);
+ expect(response.headers["content-length"]).toEqual("501");
+ expect(response.headers["content-type"]).toEqual(
+ "application/javascript; charset=utf-8",
+ );
+ expect(response.text.length).toBe(501);
+ expect(response.headers.etag).toBeDefined();
+
+ const response1 = await req
+ .get("/bundle.js")
+ .set("If-Range", '"test')
+ .set("Range", "bytes=3000-3500");
+
+ expect(response1.statusCode).toEqual(200);
+ });
});
describe("should work and generate strong etag without createReadStream", () => {
@@ -4371,6 +4462,31 @@ describe.each([
);
});
});
+
+ describe("should work and without etag generation and `if-none-match` header", () => {
+ beforeEach(async () => {
+ const compiler = getCompiler(webpackConfig);
+
+ [server, req, instance] = await frameworkFactory(
+ name,
+ framework,
+ compiler,
+ );
+ });
+
+ afterEach(async () => {
+ await close(server, instance);
+ });
+
+ it('should return the "200" code for the "GET" request to the bundle file and `if-none-match` header without etag', async () => {
+ const response = await req
+ .get(`/bundle.js`)
+ .set("if-none-match", "etag");
+
+ expect(response.statusCode).toEqual(200);
+ expect(response.headers.etag).toBeUndefined();
+ });
+ });
});
describe("lastModified", () => {
@@ -4392,13 +4508,6 @@ describe.each([
await close(server, instance);
});
- function parseHttpDate(date) {
- const timestamp = date && Date.parse(date);
-
- // istanbul ignore next: guard against date.js Date.parse patching
- return typeof timestamp === "number" ? timestamp : NaN;
- }
-
it('should return the "200" code for the "GET" request to the bundle file and set "Last-Modified"', async () => {
const response = await req.get(`/bundle.js`);
@@ -4406,7 +4515,7 @@ describe.each([
expect(response.headers["last-modified"]).toBeDefined();
});
- it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-unmodified-since" header', async () => {
+ it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-modified-since" header', async () => {
const response1 = await req.get(`/bundle.js`);
expect(response1.statusCode).toEqual(200);
@@ -4414,19 +4523,25 @@ describe.each([
const response2 = await req
.get(`/bundle.js`)
- .set("if-unmodified-since", response1.headers["last-modified"]);
+ .set("if-modified-since", response1.headers["last-modified"]);
expect(response2.statusCode).toEqual(304);
expect(response2.headers["last-modified"]).toBeDefined();
const response3 = await req
.get(`/bundle.js`)
- .set("if-unmodified-since", "Fri, 29 Mar 2020 10:25:50 GMT");
+ .set(
+ "if-modified-since",
+ new Date(
+ parseHttpDate(response1.headers["last-modified"]) - 1000,
+ ).toUTCString(),
+ );
- expect(response3.statusCode).toEqual(412);
+ expect(response3.statusCode).toEqual(200);
+ expect(response3.headers["last-modified"]).toBeDefined();
});
- it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-modified-since" header', async () => {
+ it('should return the "200" code for the "GET" request to the bundle file with "Last-Modified" and "if-unmodified-since" header', async () => {
const response1 = await req.get(`/bundle.js`);
expect(response1.statusCode).toEqual(200);
@@ -4434,22 +4549,16 @@ describe.each([
const response2 = await req
.get(`/bundle.js`)
- .set("if-modified-since", response1.headers["last-modified"]);
+ .set("if-unmodified-since", response1.headers["last-modified"]);
- expect(response2.statusCode).toEqual(304);
+ expect(response2.statusCode).toEqual(200);
expect(response2.headers["last-modified"]).toBeDefined();
const response3 = await req
.get(`/bundle.js`)
- .set(
- "if-modified-since",
- new Date(
- parseHttpDate(response1.headers["last-modified"]) - 1000,
- ).toUTCString(),
- );
+ .set("if-unmodified-since", "Fri, 29 Mar 2020 10:25:50 GMT");
- expect(response3.statusCode).toEqual(200);
- expect(response3.headers["last-modified"]).toBeDefined();
+ expect(response3.statusCode).toEqual(412);
});
it('should return the "412" code for the "GET" request to the bundle file with etag and "if-unmodified-since" header', async () => {
@@ -4484,6 +4593,19 @@ describe.each([
expect(response2.statusCode).toEqual(200);
expect(response1.headers["last-modified"]).toBeDefined();
});
+
+ it('should return the "200" code for the "GET" request with the valid range header and old "if-range" header', async () => {
+ const response = await req
+ .get("/bundle.js")
+ .set("if-range", new Date(1000).toUTCString())
+ .set("Range", "bytes=3000-3500");
+
+ expect(response.statusCode).toEqual(200);
+ expect(response.headers["content-type"]).toEqual(
+ "application/javascript; charset=utf-8",
+ );
+ expect(response.headers["last-modified"]).toBeDefined();
+ });
});
describe('should work and prefer "if-match" and "if-none-match"', () => {
@@ -4505,13 +4627,6 @@ describe.each([
await close(server, instance);
});
- function parseHttpDate(date) {
- const timestamp = date && Date.parse(date);
-
- // istanbul ignore next: guard against date.js Date.parse patching
- return typeof timestamp === "number" ? timestamp : NaN;
- }
-
it('should return the "304" code for the "GET" request to the bundle file and prefer "if-match" over "if-unmodified-since"', async () => {
const response1 = await req.get(`/bundle.js`);
@@ -4529,7 +4644,7 @@ describe.each([
).toUTCString(),
);
- expect(response2.statusCode).toEqual(304);
+ expect(response2.statusCode).toEqual(200);
expect(response2.headers["last-modified"]).toBeDefined();
expect(response2.headers.etag).toBeDefined();
});
diff --git a/types/index.d.ts b/types/index.d.ts
index 1080632b0..ac7d038ca 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -38,7 +38,7 @@ export = wdm;
*/
/**
* @typedef {Object} ResponseData
- * @property {string | Buffer | ReadStream} data
+ * @property {Buffer | ReadStream} data
* @property {number} byteLength
*/
/**
@@ -47,7 +47,7 @@ export = wdm;
* @callback ModifyResponseData
* @param {RequestInternal} req
* @param {ResponseInternal} res
- * @param {string | Buffer | ReadStream} data
+ * @param {Buffer | ReadStream} data
* @param {number} byteLength
* @return {ResponseData}
*/
@@ -284,7 +284,7 @@ type Callback = (
stats?: import("webpack").Stats | import("webpack").MultiStats | undefined,
) => any;
type ResponseData = {
- data: string | Buffer | ReadStream;
+ data: Buffer | ReadStream;
byteLength: number;
};
type ModifyResponseData<
@@ -293,7 +293,7 @@ type ModifyResponseData<
> = (
req: RequestInternal,
res: ResponseInternal,
- data: string | Buffer | ReadStream,
+ data: Buffer | ReadStream,
byteLength: number,
) => ResponseData;
type Context<
diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts
index 369054f7e..0998c346d 100644
--- a/types/utils/compatibleAPI.d.ts
+++ b/types/utils/compatibleAPI.d.ts
@@ -45,3 +45,19 @@ export function pipe(
res: Response & ExpectedResponse,
bufferOrStream: import("fs").ReadStream,
): void;
+/**
+ * @param {string} filename
+ * @param {import("../index").OutputFileSystem} outputFileSystem
+ * @param {number} start
+ * @param {number} end
+ * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }}
+ */
+export function createReadStreamOrReadFileSync(
+ filename: string,
+ outputFileSystem: import("../index").OutputFileSystem,
+ start: number,
+ end: number,
+): {
+ bufferOrStream: Buffer | import("fs").ReadStream;
+ byteLength: number;
+};
diff --git a/types/utils/memorize.d.ts b/types/utils/memorize.d.ts
new file mode 100644
index 000000000..6313160a0
--- /dev/null
+++ b/types/utils/memorize.d.ts
@@ -0,0 +1,26 @@
+export = memorize;
+/**
+ * @template T
+ * @param {Function} fn
+ * @param {{ cache?: Map } | undefined} cache
+ * @param {((value: T) => T)=} callback
+ * @returns {any}
+ */
+declare function memorize(
+ fn: Function,
+ {
+ cache,
+ }?:
+ | {
+ cache?:
+ | Map<
+ string,
+ {
+ data: T;
+ }
+ >
+ | undefined;
+ }
+ | undefined,
+ callback?: ((value: T) => T) | undefined,
+): any;