Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
e0b4274707 | |||
8cd192a8e6 | |||
6e7071ed1c | |||
b236e84f09 | |||
5b4f2a0983 | |||
087a4a7a9b |
4 changed files with 147 additions and 21 deletions
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018-2024 SVR.JS
|
Copyright (c) 2018-2025 SVR.JS
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "SVR.JS Cache mod",
|
"name": "SVR.JS Cache mod",
|
||||||
"version": "Nightly-GitMain"
|
"version": "1.1.1"
|
||||||
}
|
}
|
||||||
|
|
75
src/index.js
75
src/index.js
|
@ -15,6 +15,9 @@ module.exports = function (req, res, logFacilities, config, next) {
|
||||||
const cacheVaryHeadersConfigured = config.cacheVaryHeaders
|
const cacheVaryHeadersConfigured = config.cacheVaryHeaders
|
||||||
? config.cacheVaryHeaders
|
? config.cacheVaryHeaders
|
||||||
: [];
|
: [];
|
||||||
|
const cacheIgnoreHeadersConfigured = config.cacheIgnoreHeaders
|
||||||
|
? config.cacheIgnoreHeaders
|
||||||
|
: [];
|
||||||
const maximumCachedResponseSize = config.maximumCachedResponseSize
|
const maximumCachedResponseSize = config.maximumCachedResponseSize
|
||||||
? config.maximumCachedResponseSize
|
? config.maximumCachedResponseSize
|
||||||
: null;
|
: null;
|
||||||
|
@ -68,28 +71,44 @@ module.exports = function (req, res, logFacilities, config, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture the response
|
// Capture the response
|
||||||
|
const originalSetHeader = res.setHeader.bind(res);
|
||||||
|
const originalRemoveHeader = res.removeHeader.bind(res);
|
||||||
const originalWriteHead = res.writeHead.bind(res);
|
const originalWriteHead = res.writeHead.bind(res);
|
||||||
|
const originalWrite = res.write.bind(res);
|
||||||
const originalEnd = res.end.bind(res);
|
const originalEnd = res.end.bind(res);
|
||||||
let writtenHeaders = res.getHeaders();
|
let writtenHeaders = res.getHeaders();
|
||||||
let writtenStatusCode = 200;
|
let writtenStatusCode = 200;
|
||||||
let responseBody = "";
|
let responseBody = "";
|
||||||
let maximumCachedResponseSizeExceeded = false;
|
let maximumCachedResponseSizeExceeded = false;
|
||||||
|
let piping = false;
|
||||||
|
|
||||||
|
res.setHeader = function (name, value) {
|
||||||
|
writtenHeaders[name.toLowerCase()] = value;
|
||||||
|
return originalSetHeader(name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
res.removeHeader = function (name) {
|
||||||
|
delete writtenHeaders[name.toLowerCase()];
|
||||||
|
return originalRemoveHeader(name);
|
||||||
|
};
|
||||||
|
|
||||||
res.writeHead = function (statusCode, statusCodeDescription, headers) {
|
res.writeHead = function (statusCode, statusCodeDescription, headers) {
|
||||||
const properHeaders = headers ? headers : statusCodeDescription;
|
const properHeaders = headers ? headers : statusCodeDescription;
|
||||||
Object.keys(properHeaders).forEach((key) => {
|
if (typeof properHeaders === "object" && properHeaders !== null) {
|
||||||
writtenHeaders[key.toLowerCase()] = properHeaders[key];
|
Object.keys(properHeaders).forEach((key) => {
|
||||||
});
|
writtenHeaders[key.toLowerCase()] = properHeaders[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
writtenStatusCode = statusCode;
|
writtenStatusCode = statusCode;
|
||||||
res.setHeader("X-SVRJS-Cache", "MISS");
|
originalSetHeader("X-SVRJS-Cache", "MISS");
|
||||||
if (headers) {
|
if (headers || typeof statusCodeDescription !== "object") {
|
||||||
originalWriteHead(
|
return originalWriteHead(
|
||||||
writtenStatusCode,
|
writtenStatusCode,
|
||||||
statusCodeDescription,
|
statusCodeDescription,
|
||||||
writtenHeaders
|
writtenHeaders
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
originalWriteHead(writtenStatusCode, writtenHeaders);
|
return originalWriteHead(writtenStatusCode, writtenHeaders);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,6 +177,12 @@ module.exports = function (req, res, logFacilities, config, next) {
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
varyCache.set(cacheKey, processedVary);
|
varyCache.set(cacheKey, processedVary);
|
||||||
|
|
||||||
|
// Ignore headers
|
||||||
|
cacheIgnoreHeadersConfigured.forEach((header) => {
|
||||||
|
delete writtenHeaders[header.toLowerCase()];
|
||||||
|
});
|
||||||
|
|
||||||
cache.set(cacheKeyWithVary, {
|
cache.set(cacheKeyWithVary, {
|
||||||
body: responseBody,
|
body: responseBody,
|
||||||
headers: writtenHeaders,
|
headers: writtenHeaders,
|
||||||
|
@ -168,11 +193,38 @@ module.exports = function (req, res, logFacilities, config, next) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
originalEnd(chunk, encoding, callback);
|
return originalEnd(chunk, encoding, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (req.method != "HEAD") {
|
if (req.method != "HEAD") {
|
||||||
|
res.write = function (chunk, encoding, callback) {
|
||||||
|
if (!piping && chunk && !maximumCachedResponseSizeExceeded) {
|
||||||
|
const processedChunk = Buffer.from(
|
||||||
|
chunk,
|
||||||
|
typeof encoding === "string" ? encoding : undefined
|
||||||
|
).toString("latin1");
|
||||||
|
if (
|
||||||
|
maximumCachedResponseSize !== null &&
|
||||||
|
maximumCachedResponseSize !== undefined &&
|
||||||
|
responseBody.length + processedChunk.length >
|
||||||
|
maximumCachedResponseSize
|
||||||
|
) {
|
||||||
|
maximumCachedResponseSizeExceeded = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
responseBody += processedChunk;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (err) {
|
||||||
|
maximumCachedResponseSizeExceeded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalWrite(chunk, encoding, callback);
|
||||||
|
};
|
||||||
|
|
||||||
res.on("pipe", (src) => {
|
res.on("pipe", (src) => {
|
||||||
|
piping = true;
|
||||||
src.on("data", (chunk) => {
|
src.on("data", (chunk) => {
|
||||||
if (!maximumCachedResponseSizeExceeded) {
|
if (!maximumCachedResponseSizeExceeded) {
|
||||||
const processedChunk = Buffer.from(chunk).toString("latin1");
|
const processedChunk = Buffer.from(chunk).toString("latin1");
|
||||||
|
@ -194,6 +246,10 @@ module.exports = function (req, res, logFacilities, config, next) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.on("unpipe", () => {
|
||||||
|
piping = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
next(); // Continue with normal processing
|
next(); // Continue with normal processing
|
||||||
|
@ -212,6 +268,9 @@ module.exports.configValidators = {
|
||||||
cacheVaryHeaders: (value) =>
|
cacheVaryHeaders: (value) =>
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.every((element) => typeof element === "string"),
|
value.every((element) => typeof element === "string"),
|
||||||
|
cacheIgnoreHeaders: (value) =>
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.every((element) => typeof element === "string"),
|
||||||
maximumCachedResponseSize: (value) =>
|
maximumCachedResponseSize: (value) =>
|
||||||
typeof value === "number" || value === null
|
typeof value === "number" || value === null
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,12 +14,9 @@ jest.mock("../src/utils/cacheControlUtils.js", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("SVR.JS Cache mod", () => {
|
describe("SVR.JS Cache mod", () => {
|
||||||
let req, res, logFacilities, config, next, resWriteHead, resEnd;
|
let req, res, logFacilities, config, next;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resWriteHead = jest.fn();
|
|
||||||
resEnd = jest.fn();
|
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {},
|
headers: {},
|
||||||
|
@ -29,8 +26,9 @@ describe("SVR.JS Cache mod", () => {
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
headers: {},
|
headers: {},
|
||||||
writeHead: resWriteHead,
|
writeHead: jest.fn(),
|
||||||
end: resEnd,
|
write: jest.fn(),
|
||||||
|
end: jest.fn(),
|
||||||
setHeader: jest.fn(),
|
setHeader: jest.fn(),
|
||||||
getHeaderNames: jest.fn(() => []),
|
getHeaderNames: jest.fn(() => []),
|
||||||
getHeaders: jest.fn(() => ({})),
|
getHeaders: jest.fn(() => ({})),
|
||||||
|
@ -42,6 +40,7 @@ describe("SVR.JS Cache mod", () => {
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
cacheVaryHeaders: ["accept"],
|
cacheVaryHeaders: ["accept"],
|
||||||
|
cacheIgnoreHeaders: [],
|
||||||
maximumCachedResponseSize: 1024
|
maximumCachedResponseSize: 1024
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -95,6 +94,11 @@ describe("SVR.JS Cache mod", () => {
|
||||||
// Reset mocks for the second invocation
|
// Reset mocks for the second invocation
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
next.mockReset();
|
next.mockReset();
|
||||||
|
res.setHeader = jest.fn();
|
||||||
|
res.removeHeader = jest.fn();
|
||||||
|
res.writeHead = jest.fn();
|
||||||
|
res.write = jest.fn();
|
||||||
|
res.end = jest.fn();
|
||||||
|
|
||||||
// Second request: retrieve from cache
|
// Second request: retrieve from cache
|
||||||
parseCacheControl.mockReturnValue({});
|
parseCacheControl.mockReturnValue({});
|
||||||
|
@ -107,14 +111,69 @@ describe("SVR.JS Cache mod", () => {
|
||||||
"The response is cached."
|
"The response is cached."
|
||||||
);
|
);
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "HIT");
|
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "HIT");
|
||||||
expect(resWriteHead).toHaveBeenCalledWith(200, {
|
expect(res.writeHead).toHaveBeenCalledWith(200, {
|
||||||
"cache-control": "max-age=300",
|
"cache-control": "max-age=300",
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
});
|
});
|
||||||
expect(resEnd).toHaveBeenCalledWith(
|
expect(res.end).toHaveBeenCalledWith(
|
||||||
Buffer.from("cached response body", "latin1"),
|
Buffer.from("cached response body", "latin1")
|
||||||
undefined,
|
);
|
||||||
undefined
|
expect(next).not.toHaveBeenCalled(); // No middleware should be called
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should cache the response and serve it on subsequent requests, while ignoring some headers", () => {
|
||||||
|
req.headers.host = "ignore.test.com";
|
||||||
|
req.headers.accept = "application/json";
|
||||||
|
|
||||||
|
// Headers to ignore
|
||||||
|
config.cacheIgnoreHeaders = ["x-header-ignored"];
|
||||||
|
|
||||||
|
parseCacheControl.mockReturnValue({});
|
||||||
|
parseVary.mockReturnValue(["accept"]);
|
||||||
|
shouldCacheResponse.mockReturnValue(true);
|
||||||
|
|
||||||
|
// Mock cache-control headers
|
||||||
|
res.getHeaders.mockReturnValue({ "cache-control": "max-age=300" });
|
||||||
|
|
||||||
|
// First request: cache the response
|
||||||
|
mod(req, res, logFacilities, config, next);
|
||||||
|
|
||||||
|
// Simulate the first response
|
||||||
|
res.writeHead(200, {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-header-ignored": "no"
|
||||||
|
});
|
||||||
|
res.end("cached response body");
|
||||||
|
|
||||||
|
// Assertions for the first request
|
||||||
|
expect(next).toHaveBeenCalled(); // Proceed to next middleware during first request
|
||||||
|
|
||||||
|
// Reset mocks for the second invocation
|
||||||
|
jest.clearAllMocks();
|
||||||
|
next.mockReset();
|
||||||
|
res.setHeader = jest.fn();
|
||||||
|
res.removeHeader = jest.fn();
|
||||||
|
res.writeHead = jest.fn();
|
||||||
|
res.write = jest.fn();
|
||||||
|
res.end = jest.fn();
|
||||||
|
|
||||||
|
// Second request: retrieve from cache
|
||||||
|
parseCacheControl.mockReturnValue({});
|
||||||
|
isCacheValid.mockReturnValue(true); // Simulate a valid cache entry
|
||||||
|
|
||||||
|
mod(req, res, logFacilities, config, next);
|
||||||
|
|
||||||
|
// Assertions for the second request
|
||||||
|
expect(logFacilities.resmessage).toHaveBeenCalledWith(
|
||||||
|
"The response is cached."
|
||||||
|
);
|
||||||
|
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "HIT");
|
||||||
|
expect(res.writeHead).toHaveBeenCalledWith(200, {
|
||||||
|
"cache-control": "max-age=300",
|
||||||
|
"content-type": "application/json"
|
||||||
|
});
|
||||||
|
expect(res.end).toHaveBeenCalledWith(
|
||||||
|
Buffer.from("cached response body", "latin1")
|
||||||
);
|
);
|
||||||
expect(next).not.toHaveBeenCalled(); // No middleware should be called
|
expect(next).not.toHaveBeenCalled(); // No middleware should be called
|
||||||
});
|
});
|
||||||
|
@ -122,12 +181,16 @@ describe("SVR.JS Cache mod", () => {
|
||||||
test("should validate config values correctly", () => {
|
test("should validate config values correctly", () => {
|
||||||
const validConfig = {
|
const validConfig = {
|
||||||
cacheVaryHeaders: ["accept", "user-agent"],
|
cacheVaryHeaders: ["accept", "user-agent"],
|
||||||
|
cacheIgnoreHeaders: ["set-cookie"],
|
||||||
maximumCachedResponseSize: 2048
|
maximumCachedResponseSize: 2048
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mod.configValidators.cacheVaryHeaders(validConfig.cacheVaryHeaders)
|
mod.configValidators.cacheVaryHeaders(validConfig.cacheVaryHeaders)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
mod.configValidators.cacheIgnoreHeaders(validConfig.cacheIgnoreHeaders)
|
||||||
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
mod.configValidators.maximumCachedResponseSize(
|
mod.configValidators.maximumCachedResponseSize(
|
||||||
validConfig.maximumCachedResponseSize
|
validConfig.maximumCachedResponseSize
|
||||||
|
@ -136,12 +199,16 @@ describe("SVR.JS Cache mod", () => {
|
||||||
|
|
||||||
const invalidConfig = {
|
const invalidConfig = {
|
||||||
cacheVaryHeaders: "invalid",
|
cacheVaryHeaders: "invalid",
|
||||||
|
cacheIgnoreHeaders: "invalid",
|
||||||
maximumCachedResponseSize: "invalid"
|
maximumCachedResponseSize: "invalid"
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
mod.configValidators.cacheVaryHeaders(invalidConfig.cacheVaryHeaders)
|
mod.configValidators.cacheVaryHeaders(invalidConfig.cacheVaryHeaders)
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
mod.configValidators.cacheIgnoreHeaders(invalidConfig.cacheIgnoreHeaders)
|
||||||
|
).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
mod.configValidators.maximumCachedResponseSize(
|
mod.configValidators.maximumCachedResponseSize(
|
||||||
invalidConfig.maximumCachedResponseSize
|
invalidConfig.maximumCachedResponseSize
|
||||||
|
|
Loading…
Reference in a new issue