node-caching-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +87 -0
- package/index.js +23 -0
- package/package.json +20 -0
- package/src/args.js +30 -0
- package/src/cache.js +25 -0
- package/src/clear-cache-client.js +36 -0
- package/src/server.js +113 -0
- package/src/state.js +18 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohd Faizan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Caching Proxy
|
|
2
|
+
|
|
3
|
+
A CLI tool that starts a proxy server. It forwards requests to a real origin
|
|
4
|
+
server and caches the responses. If the same request comes in again, it's
|
|
5
|
+
served straight from the cache instead of hitting the origin server.
|
|
6
|
+
|
|
7
|
+
No npm dependencies — uses only Node.js's built-in `http`/`https` modules,
|
|
8
|
+
so `node index.js ...` works with nothing to install.
|
|
9
|
+
|
|
10
|
+
## File structure
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
caching-proxy/
|
|
14
|
+
├── index.js entry point: parses args, picks a mode
|
|
15
|
+
└── src/
|
|
16
|
+
├── args.js CLI argument parsing + usage text
|
|
17
|
+
├── state.js shared temp-file so --clear-cache can
|
|
18
|
+
│ find the running server's port
|
|
19
|
+
├── cache.js the cache store itself (Map wrapper)
|
|
20
|
+
├── server.js the proxy server: cache check, forward
|
|
21
|
+
│ to origin, cache the response
|
|
22
|
+
└── clear-cache-client.js sends the "clear cache" request to a
|
|
23
|
+
running server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Start the proxy:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node index.js --port 3000 --origin http://dummyjson.com
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Now requests to your proxy are forwarded to the origin:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl http://localhost:3000/products/1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- First request -> forwarded to `http://dummyjson.com/products/1`, response is cached.
|
|
41
|
+
Response header: `X-Cache: MISS`
|
|
42
|
+
- Second request to the same path -> served from cache, origin is not hit.
|
|
43
|
+
Response header: `X-Cache: HIT`
|
|
44
|
+
|
|
45
|
+
Clear the cache without restarting the server:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node index.js --clear-cache
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
1. **Parse CLI args** — `--port`, `--origin`, or `--clear-cache`.
|
|
54
|
+
2. **Cache store** — an in-memory `Map` keyed by `"METHOD path+query"`
|
|
55
|
+
(e.g. `GET /products/1`), storing `{ status, headers, body }`.
|
|
56
|
+
3. **On each request:**
|
|
57
|
+
- If it's a `GET` and the key is in the cache -> write back the cached
|
|
58
|
+
status/headers/body, add `X-Cache: HIT`.
|
|
59
|
+
- Otherwise -> forward the request (method, headers, body) to the origin
|
|
60
|
+
via `http`/`https`, buffer the response, store it in the cache if it
|
|
61
|
+
was a `GET`, and return it with `X-Cache: MISS`.
|
|
62
|
+
4. **`--clear-cache`** — a separate CLI invocation. It reads the running
|
|
63
|
+
server's port from a small state file in the OS temp dir, then POSTs to
|
|
64
|
+
an internal `/__internal_clear_cache__` route on that same server, which
|
|
65
|
+
calls `cache.clear()`. This is what lets `--clear-cache` actually affect
|
|
66
|
+
the live server's cache rather than just this process's own (empty) one.
|
|
67
|
+
|
|
68
|
+
## Design notes / things worth knowing if you extend this
|
|
69
|
+
|
|
70
|
+
- Only `GET` requests are cached. Caching `POST`/`PUT`/`DELETE` responses is
|
|
71
|
+
usually wrong since those requests aren't idempotent/safe.
|
|
72
|
+
- The cache key includes the query string, so `/items?page=1` and
|
|
73
|
+
`/items?page=2` are cached separately.
|
|
74
|
+
- `Content-Length` from the origin is preserved as-is since we buffer the
|
|
75
|
+
full body before responding, so it stays accurate.
|
|
76
|
+
- `Transfer-Encoding` is stripped from cached/forwarded headers since we're
|
|
77
|
+
sending a fixed-length buffered body, not a stream.
|
|
78
|
+
- The cache is in-memory and per-process: restarting the proxy always
|
|
79
|
+
starts with an empty cache.
|
|
80
|
+
|
|
81
|
+
## Possible extensions
|
|
82
|
+
|
|
83
|
+
- Add a TTL (max-age) so cache entries expire automatically.
|
|
84
|
+
- Add `--clear-cache` for a *specific* path instead of the whole cache.
|
|
85
|
+
- Persist the cache to disk so it survives restarts.
|
|
86
|
+
- Respect the origin's own `Cache-Control`/`ETag` headers instead of
|
|
87
|
+
caching everything unconditionally.
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Entry point: parse CLI args, then hand off to whichever module handles
|
|
4
|
+
// that mode. All the real logic lives in ./src/*.
|
|
5
|
+
|
|
6
|
+
import { parseArgs, printUsage } from "./src/args.js";
|
|
7
|
+
import { startServer } from "./src/server.js";
|
|
8
|
+
import { clearRunningCache } from "./src/clear-cache-client.js";
|
|
9
|
+
|
|
10
|
+
const args = parseArgs(process.argv.slice(2));
|
|
11
|
+
|
|
12
|
+
const missingRequiredFlags = !args.clearCache && (!args.port || !args.origin);
|
|
13
|
+
|
|
14
|
+
if (args.help || missingRequiredFlags) {
|
|
15
|
+
printUsage();
|
|
16
|
+
process.exit(args.help ? 0 : 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (args.clearCache) {
|
|
20
|
+
clearRunningCache();
|
|
21
|
+
} else {
|
|
22
|
+
startServer(Number(args.port), args.origin);
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-caching-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI caching reverse proxy that forwards requests to an origin server and caches responses, using only Node.js core modules.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"caching-proxy": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": ["cli", "proxy", "cache", "http", "reverse-proxy"],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Mohd Faizan",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=16"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/args.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Turns raw CLI words (process.argv) into a clean options object.
|
|
2
|
+
// e.g. ["--port", "3000", "--origin", "http://x.com"]
|
|
3
|
+
// -> { port: "3000", origin: "http://x.com", clearCache: false, help: false }
|
|
4
|
+
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const args = { port: null, origin: null, clearCache: false, help: false };
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const a = argv[i];
|
|
10
|
+
if (a === "--port" || a === "-p") args.port = argv[++i];
|
|
11
|
+
else if (a === "--origin" || a === "-o") args.origin = argv[++i];
|
|
12
|
+
else if (a === "--clear-cache") args.clearCache = true;
|
|
13
|
+
else if (a === "--help" || a === "-h") args.help = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return args;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function printUsage() {
|
|
20
|
+
console.log(`
|
|
21
|
+
Caching Proxy CLI
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
caching-proxy --port <number> --origin <url> Start the proxy server
|
|
25
|
+
caching-proxy --clear-cache Clear the cache of a running proxy
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
caching-proxy --port 3000 --origin http://dummyjson.com
|
|
29
|
+
`);
|
|
30
|
+
}
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The actual cache store. Kept separate from server.js so the "what is a
|
|
2
|
+
// cache entry / how do we key it" logic doesn't get tangled up with the
|
|
3
|
+
// networking code.
|
|
4
|
+
|
|
5
|
+
export function createCache() {
|
|
6
|
+
const store = new Map(); // key: "METHOD path" -> { status, headers, body }
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
key(method, url) {
|
|
10
|
+
return `${method} ${url}`;
|
|
11
|
+
},
|
|
12
|
+
has(key) {
|
|
13
|
+
return store.has(key);
|
|
14
|
+
},
|
|
15
|
+
get(key) {
|
|
16
|
+
return store.get(key);
|
|
17
|
+
},
|
|
18
|
+
set(key, value) {
|
|
19
|
+
store.set(key, value);
|
|
20
|
+
},
|
|
21
|
+
clear() {
|
|
22
|
+
store.clear();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// This runs as its OWN process invocation (`node index.js --clear-cache`).
|
|
2
|
+
// It has no access to a running server's in-memory cache directly, so it
|
|
3
|
+
// looks up that server's port from the shared state file, then sends it
|
|
4
|
+
// an HTTP request telling it to clear its own cache.
|
|
5
|
+
|
|
6
|
+
import http from "http";
|
|
7
|
+
import { loadServerState } from "./state.js";
|
|
8
|
+
|
|
9
|
+
export function clearRunningCache() {
|
|
10
|
+
const state = loadServerState();
|
|
11
|
+
|
|
12
|
+
if (!state) {
|
|
13
|
+
console.error("No running caching-proxy server found.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const req = http.request(
|
|
18
|
+
{
|
|
19
|
+
hostname: "localhost",
|
|
20
|
+
port: state.port,
|
|
21
|
+
path: "/__internal_clear_cache__",
|
|
22
|
+
method: "POST",
|
|
23
|
+
},
|
|
24
|
+
(res) => {
|
|
25
|
+
res.on("data", () => {});
|
|
26
|
+
res.on("end", () => console.log("Cache cleared successfully."));
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
req.on("error", (err) => {
|
|
31
|
+
console.error("Failed to reach the running proxy:", err.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.end();
|
|
36
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The proxy server: listens for client requests, serves them from cache
|
|
2
|
+
// when possible, otherwise forwards to the origin server and caches the
|
|
3
|
+
// result.
|
|
4
|
+
|
|
5
|
+
import http from "http";
|
|
6
|
+
import https from "https";
|
|
7
|
+
import { URL } from "url";
|
|
8
|
+
import { createCache } from "./cache.js";
|
|
9
|
+
import { saveServerState } from "./state.js";
|
|
10
|
+
|
|
11
|
+
const CLEAR_CACHE_ROUTE = "/__internal_clear_cache__";
|
|
12
|
+
|
|
13
|
+
export function startServer(port, originUrl) {
|
|
14
|
+
const cache = createCache();
|
|
15
|
+
const origin = new URL(originUrl);
|
|
16
|
+
const client = origin.protocol === "https:" ? https : http;
|
|
17
|
+
|
|
18
|
+
const server = http.createServer((req, res) => {
|
|
19
|
+
if (isClearCacheRequest(req)) {
|
|
20
|
+
return handleClearCache(cache, res);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cacheKey = cache.key(req.method, req.url);
|
|
24
|
+
|
|
25
|
+
if (req.method === "GET" && cache.has(cacheKey)) {
|
|
26
|
+
return serveFromCache(cache.get(cacheKey), res);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
forwardToOrigin({ req, res, origin, client, cache, cacheKey });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
server.listen(port, () => {
|
|
33
|
+
saveServerState({ port });
|
|
34
|
+
console.log(`Caching proxy listening on port ${port}`);
|
|
35
|
+
console.log(`Forwarding to origin: ${originUrl}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
process.on("SIGINT", () => server.close(() => process.exit(0)));
|
|
39
|
+
|
|
40
|
+
return server;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isClearCacheRequest(req) {
|
|
44
|
+
return req.method === "POST" && req.url === CLEAR_CACHE_ROUTE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleClearCache(cache, res) {
|
|
48
|
+
cache.clear();
|
|
49
|
+
console.log("Cache cleared.");
|
|
50
|
+
res.writeHead(200);
|
|
51
|
+
res.end("Cache cleared");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function serveFromCache(cached, res) {
|
|
55
|
+
res.writeHead(cached.status, { ...cached.headers, "X-Cache": "HIT" });
|
|
56
|
+
res.end(cached.body);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function forwardToOrigin({ req, res, origin, client, cache, cacheKey }) {
|
|
60
|
+
// Request bodies arrive in chunks; buffer them fully before forwarding.
|
|
61
|
+
const chunks = [];
|
|
62
|
+
req.on("data", (c) => chunks.push(c));
|
|
63
|
+
req.on("end", () => {
|
|
64
|
+
const body = Buffer.concat(chunks);
|
|
65
|
+
sendToOrigin({ req, res, origin, client, cache, cacheKey, body });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sendToOrigin({ req, res, origin, client, cache, cacheKey, body }) {
|
|
70
|
+
const forwardHeaders = { ...req.headers, host: origin.host };
|
|
71
|
+
|
|
72
|
+
const proxyReq = client.request(
|
|
73
|
+
{
|
|
74
|
+
protocol: origin.protocol,
|
|
75
|
+
hostname: origin.hostname,
|
|
76
|
+
port: origin.port || (origin.protocol === "https:" ? 443 : 80),
|
|
77
|
+
path: req.url,
|
|
78
|
+
method: req.method,
|
|
79
|
+
headers: forwardHeaders,
|
|
80
|
+
},
|
|
81
|
+
(originRes) => handleOriginResponse({ originRes, res, req, cache, cacheKey })
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
proxyReq.on("error", (err) => {
|
|
85
|
+
console.error("Proxy error:", err.message);
|
|
86
|
+
res.writeHead(502);
|
|
87
|
+
res.end("Bad Gateway: could not reach origin server");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (body.length) proxyReq.write(body);
|
|
91
|
+
proxyReq.end();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleOriginResponse({ originRes, res, req, cache, cacheKey }) {
|
|
95
|
+
const resChunks = [];
|
|
96
|
+
originRes.on("data", (c) => resChunks.push(c));
|
|
97
|
+
originRes.on("end", () => {
|
|
98
|
+
const responseBody = Buffer.concat(resChunks);
|
|
99
|
+
const headers = { ...originRes.headers };
|
|
100
|
+
delete headers["transfer-encoding"];
|
|
101
|
+
|
|
102
|
+
if (req.method === "GET") {
|
|
103
|
+
cache.set(cacheKey, {
|
|
104
|
+
status: originRes.statusCode,
|
|
105
|
+
headers,
|
|
106
|
+
body: responseBody,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.writeHead(originRes.statusCode, { ...headers, "X-Cache": "MISS" });
|
|
111
|
+
res.end(responseBody);
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/state.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A small JSON file in the OS temp dir. It's the only way two separate
|
|
2
|
+
// process invocations (the running server, and a later `--clear-cache`
|
|
3
|
+
// call) can find each other - they share this file, not memory.
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
const STATE_FILE = path.join(os.tmpdir(), "caching-proxy-state.json");
|
|
10
|
+
|
|
11
|
+
export function saveServerState({ port }) {
|
|
12
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify({ port }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadServerState() {
|
|
16
|
+
if (!fs.existsSync(STATE_FILE)) return null;
|
|
17
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
|
|
18
|
+
}
|