Skip to content

Commit 8c4c2de

Browse files
fix(server): redact auth headers, gate stdout, await handlers, generic 500 (#190 round 2)
Addresses Codex round-1 review (2026-06-04T21:43Z, CHANGES_REQUESTED): 1. Authorization / x-api-key / cookie / proxy-authorization redacted in debug log via a small redactHeaders() helper. Same discipline as bootstrap-defense.mjs's audit-record contract: never spread raw headers to log code. [REDACTED] sentinel preserves "header was present" signal. 2. Gated all stdout/stderr behind CACHE_FIX_DEBUG. Removed the nine module-top console.log(config.X) lines (fired on every import, embedder-hostile) and the four ungated console.error() calls in the dispatcher / forwardRequest catch. All debug surfaces now flow through debugLog, which self-gates on process.env.CACHE_FIX_DEBUG === "1". 3. Dispatcher now awaits handleMessages / handleBootstrap inside an async IIFE wrapped by try/catch — rejections from preForward() or pipeline hooks no longer escape to unhandledRejection. 4. Fixed two misleading diagnostics: clientRes.url / .method are undefined (those are request fields); replaced with clientReq.method / .url in the pre.handled branch. Removed the bytesWritten lines (those are socket-lifetime counters, not per-response payloads). 5. 500 fallback body is generic ({"error":"internal_proxy_error"}) — no longer echoes error.message, which could leak internal paths or upstream URLs. Also: - Removed unused fs imports (readFileSync/writeFileSync/renameSync). - Wired mkdirSync for log-dir bootstrap (was imported but unwired). - LOG_PATH now overridable via CACHE_FIX_DEBUG_LOG for test isolation. - Live env read on every debugLog call (matches image-strip's #98 gate). Test coverage in test/proxy-server-debug-log.test.mjs: - /health hit with debug off creates no log file (no-noise control) - Authorization / x-api-key / cookie / proxy-authorization all redact - Non-sensitive header values still appear (over-redaction guard) - Module import produces no stdout when CACHE_FIX_DEBUG unset (spawn child) - Static: 500 body has no error.message echo; dispatcher awaits handlers Co-authored-by: Aleksandr Usenko <nisqatsi@users.noreply.github.com>
1 parent c05c3b1 commit 8c4c2de

2 files changed

Lines changed: 344 additions & 90 deletions

File tree

proxy/server.mjs

Lines changed: 80 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,47 @@ import { streamResponse, createTelemetryRecord } from "./stream.mjs";
66
import { loadExtensions, snapshotRegistry, runOnRequest, runOnResponseStart, runOnResponse, getFailedExtensions } from "./pipeline.mjs";
77
import { startWatcher } from "./watcher.mjs";
88

9-
// --------------------------------------------------------------------------
10-
// Debug logging (writes to ~/.claude/cache-fix-debug.log)
11-
// Set CACHE_FIX_DEBUG=1 to enable
12-
// --------------------------------------------------------------------------
13-
14-
import { appendFileSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
9+
// Debug logging — writes to ~/.claude/cache-fix-debug.log (override path with
10+
// CACHE_FIX_DEBUG_LOG). Self-gated on CACHE_FIX_DEBUG=1; a no-op otherwise.
11+
// Env is read on every call so tests (and operators flipping the flag at
12+
// runtime) see live behavior — same pattern as image-strip's #98 gate.
13+
import { appendFileSync, mkdirSync } from "node:fs";
1514
import { homedir } from "node:os";
16-
import { join } from "node:path";
17-
import util from 'util';
15+
import { dirname, join } from "node:path";
16+
import util from "node:util";
17+
18+
function debugLogPath() {
19+
return process.env.CACHE_FIX_DEBUG_LOG ||
20+
join(homedir(), ".claude", "cache-fix-debug.log");
21+
}
1822

19-
const LOG_PATH = join(homedir(), ".claude", "cache-fix-debug.log");
23+
// Never spread raw headers to the log: Authorization / x-api-key / cookies
24+
// must never persist to disk. Same discipline as bootstrap-defense.mjs's
25+
// audit-record contract — extract named scalars only.
26+
const SENSITIVE_HEADERS = new Set([
27+
"authorization",
28+
"x-api-key",
29+
"cookie",
30+
"set-cookie",
31+
"proxy-authorization",
32+
]);
33+
34+
function redactHeaders(headers) {
35+
const out = {};
36+
for (const [k, v] of Object.entries(headers || {})) {
37+
out[k] = SENSITIVE_HEADERS.has(k.toLowerCase()) ? "[REDACTED]" : v;
38+
}
39+
return out;
40+
}
2041

2142
function debugLog(...args) {
22-
if (!config.debug) return;
43+
if (process.env.CACHE_FIX_DEBUG !== "1") return;
44+
const path = debugLogPath();
45+
try { mkdirSync(dirname(path), { recursive: true }); } catch {}
2346
const line = `[${new Date().toISOString()}] ${util.format(...args)}\n`;
24-
try { appendFileSync(LOG_PATH, line); } catch {}
47+
try { appendFileSync(path, line); } catch {}
2548
}
2649

27-
// --------------------------------------------------------------------------
28-
29-
console.log(`config.port='${config.port}'`);
30-
console.log(`config.bind='${config.bind}'`);
31-
console.log(`config.upstream='${config.upstream}'`);
32-
console.log(`config.debug='${config.debug}'`);
33-
console.log(`config.httpsProxy='${config.httpsProxy}'`);
34-
console.log(`config.httpProxy='${config.httpProxy}'`);
35-
console.log(`config.noProxy='${config.noProxy}'`);
36-
console.log(`config.caFile='${config.caFile}'`);
37-
console.log(`config.rejectUnauthorized='${config.rejectUnauthorized}'`);
38-
3950
function collectBody(req) {
4051
return new Promise((resolve, reject) => {
4152
const chunks = [];
@@ -105,16 +116,11 @@ async function handleMessages(clientReq, clientRes) {
105116

106117
const pre = await preForward(clientReq, clientRes, abortController, extSnapshot, "messages");
107118
if (pre.handled) {
108-
debugLog("========================================");
109-
debugLog("[PROXY] handled internally without upstream request");
110-
debugLog("[PROXY -> CLAUDE] RESPONSE");
111-
debugLog("url:", clientRes.url);
112-
debugLog("method:", clientRes.method);
113-
debugLog("status:", clientRes.statusCode);
114-
debugLog("message:", clientRes.statusMessage);
115-
debugLog("bytes:", clientRes.socket?.bytesWritten || 0);
116-
debugLog("headers:", clientRes.getHeaders());
117-
return;
119+
debugLog("[PROXY] handled internally without upstream request",
120+
"method:", clientReq.method, "url:", clientReq.url,
121+
"status:", clientRes.statusCode,
122+
"response headers:", redactHeaders(clientRes.getHeaders()));
123+
return;
118124
}
119125
const { parsed, forwardBody, meta } = pre;
120126

@@ -129,7 +135,6 @@ async function handleMessages(clientReq, clientRes) {
129135
abortController.signal
130136
));
131137
} catch (err) {
132-
console.error("[PROXY] forwardRequest error:", err.message);
133138
debugLog("[PROXY] forwardRequest error:", err.message);
134139
if (abortController.signal.aborted) return;
135140
clientRes.writeHead(502, { "content-type": "application/json" });
@@ -142,17 +147,10 @@ async function handleMessages(clientReq, clientRes) {
142147
// socket carried the request without each one re-instrumenting upstream.
143148
meta._upstreamConnectionId = upstreamConnectionId ?? null;
144149

145-
debugLog("");
146-
debugLog("========================================");
147-
debugLog("[UPSTREAM -> PROXY -> CLAUDE] RESPONSE");
148-
debugLog("url:", upstreamRes.url);
149-
debugLog("method:", upstreamRes.method);
150-
debugLog("status1:", statusCode);
151-
debugLog("status2:", upstreamRes.statusCode);
152-
debugLog("message:", upstreamRes.statusMessage);
153-
debugLog("bytes:", upstreamRes.socket?.bytesWritten || 0);
154-
debugLog("upstream headers:", upstreamRes.headers);
155-
debugLog("proxy headers:", responseHeaders);
150+
debugLog("[UPSTREAM -> PROXY -> CLAUDE] RESPONSE",
151+
"status:", statusCode, "message:", upstreamRes.statusMessage,
152+
"upstream headers:", redactHeaders(upstreamRes.headers),
153+
"proxy headers:", redactHeaders(responseHeaders));
156154

157155
if (extSnapshot.length > 0) {
158156
const resCtx = { status: statusCode, headers: responseHeaders, meta };
@@ -329,52 +327,44 @@ function handleNotFound(_req, res) {
329327
*/
330328
export function createProxyServer() {
331329
return http.createServer((req, res) => {
332-
try {
333-
debugLog("");
334-
debugLog("========================================");
335-
debugLog("[CLAUDE -> PROXY] REQUEST");
336-
debugLog("method:", req.method);
337-
debugLog("url:", req.url);
338-
debugLog("headers:", req.headers);
339-
340-
const originalWrite = res.write;
341-
const originalEnd = res.end;
342-
343-
res.write = function (chunk, ...args) {
344-
debugLog(`[PROXY -> CLAUDE] Send chunk. Size: ${chunk ? chunk.length : 0} bytes`);
345-
return originalWrite.apply(res, [chunk, ...args]);
346-
};
347-
348-
res.end = function (chunk, ...args) {
349-
debugLog("[PROXY -> CLAUDE] Close connection (res.end)");
350-
return originalEnd.apply(res, [chunk, ...args]);
351-
};
352-
353-
if (req.method === "GET" && req.url === "/health") {
354-
return handleHealth(req, res);
355-
}
356-
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
357-
return handleMessages(req, res);
358-
}
359-
if (req.url?.startsWith("/api/claude_cli/bootstrap")) {
360-
return handleBootstrap(req, res);
361-
}
362-
console.error(`ERROR: handler not found for req.url='${req.url}' req.method='${req.method}'`);
363-
debugLog(`ERROR: handler not found for req.url='${req.url}' req.method='${req.method}'`);
364-
handleNotFound(req, res);
365-
}
366-
catch (error) {
367-
console.error("!!! REQUEST HANDLER ERROR !!!");
368-
console.error(error);
369-
debugLog("!!! REQUEST HANDLER ERROR !!!");
370-
debugLog(error);
371-
// Reply error 500 to prevent claude hanging
372-
if (!res.headersSent) {
373-
res.writeHead(500, { 'Content-Type': 'application/json' });
374-
res.end(JSON.stringify({ error: "Internal Proxy Error", message: error.message }));
330+
// Async IIFE: handleMessages/handleBootstrap return promises, so we have
331+
// to await them inside the try/catch — a bare return would let rejections
332+
// escape to unhandledRejection and (on Node 15+) crash the process.
333+
(async () => {
334+
try {
335+
debugLog("[CLAUDE -> PROXY] REQUEST",
336+
"method:", req.method, "url:", req.url,
337+
"headers:", redactHeaders(req.headers));
338+
339+
// Wrap res.write/res.end to log chunk-level activity when debug is on.
340+
// These are sync monkey-patches; the inner debugLog self-gates so the
341+
// overhead is negligible when CACHE_FIX_DEBUG is unset.
342+
const originalWrite = res.write;
343+
const originalEnd = res.end;
344+
res.write = function (chunk, ...args) {
345+
debugLog(`[PROXY -> CLAUDE] Send chunk. Size: ${chunk ? chunk.length : 0} bytes`);
346+
return originalWrite.apply(res, [chunk, ...args]);
347+
};
348+
res.end = function (chunk, ...args) {
349+
debugLog("[PROXY -> CLAUDE] Close connection (res.end)");
350+
return originalEnd.apply(res, [chunk, ...args]);
351+
};
352+
353+
if (req.method === "GET" && req.url === "/health") return handleHealth(req, res);
354+
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) return await handleMessages(req, res);
355+
if (req.url?.startsWith("/api/claude_cli/bootstrap")) return await handleBootstrap(req, res);
356+
debugLog("ERROR: handler not found for req.url=", req.url, "method=", req.method);
357+
handleNotFound(req, res);
358+
} catch (error) {
359+
debugLog("REQUEST HANDLER ERROR:", error?.message, error?.stack);
360+
// Generic body: do NOT echo error.message (may include internal paths,
361+
// upstream URLs, or other server state).
362+
if (!res.headersSent) {
363+
res.writeHead(500, { "content-type": "application/json" });
364+
res.end(JSON.stringify({ error: "internal_proxy_error" }));
365+
}
375366
}
376-
}
377-
367+
})();
378368
});
379369
}
380370

0 commit comments

Comments
 (0)