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 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
+ }