fix: auto-wrap Error args in the logger + diagnostic property allowlist (#1734) - #1749
Merged
Conversation
…console wraps (#1734) Transplant of the kris/log-error-leak-sweep sweep onto main. #1737 (the errorForLog primitive + REST.ts fix) was squash-merged, so the original sweep branch's commit history no longer rebases cleanly; this reapplies its net diff directly on top of the squashed main. Enforces the invariant at the choke point instead of per call site: the HarperLogger level methods (plus the inherited log/dir/table) route every Error argument through errorForLog inside the level gate, so raw logger.error(error) - anywhere, including future/component/app code holding a server logger - cannot leak own-enumerable secrets into hdb.log. Explicit errorForLog wraps remain only on console.* calls where the error's provenance includes app/library code (JSONStream, socketRouter's uncaughtException, componentLoader app-load failures, loadRootComponents' package install) - console isn't auto-wrapped (rejected: it would replace the inspector's reported call-site line with the wrapper's). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mments (#1734) Surface a small allowlist of own-enumerable Error properties in the rendered log line - code, status, statusCode, errno, syscall - alongside the stack and cause chain. These are common diagnostic fields (Node error codes, HTTP status) that libraries/app code don't use to carry secrets, unlike arbitrary properties (axios' config/request with an Authorization header), which stay excluded. `path` is deliberately omitted - it can reveal internal filesystem layout and the message/stack already names the failing operation. Also folds in two review comments from #1737 that carried over to this branch (both left unresolved when that PR was squash-merged): - scope the util import to node:util for consistency - move errorToString's null-guard into errorToString itself rather than duplicating it in errorToLogString, since errorToString is called directly at several response-body call sites (REST.ts, http.ts, JSONStream, ResourceBridge) that get the same defensive benefit for free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Reviewed; no blockers found. |
…cts (#1734) gemini-code-assist review on #1749: isErrorLike's try/catch only protects the top-level sanitizeErrorArgs decision. Once an error is wrapped, rendering walks its cause chain unconditionally - a revoked Proxy or a throwing getter anywhere in that chain (attached by code this module doesn't control) throws inside errorToLogString, crashing the log write itself. Reproduced: `error?.stack` on a revoked Proxy throws even through optional chaining (?. only guards null/undefined, not trap exceptions). Wraps every property access in the render path (errorToString, loggablePropsSuffix, renderErrorLine, the cause-chain walk) in try/catch with safe fallbacks, so the logger genuinely cannot throw regardless of what a thrown Error's cause chain contains. errorToString is hardened at its own level (not just via callers) since it also has external callers building response bodies (REST.ts/http.ts/JSONStream). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kriszyp
marked this pull request as ready for review
July 10, 2026 11:55
kriszyp
requested review from
DavidCockerill and
cb1kenobi
and removed request for
Ethan-Arrowood and
heskew
July 10, 2026 11:55
cb1kenobi
reviewed
Jul 10, 2026
| function renderErrorLine(error: any): string { | ||
| try { | ||
| const base = typeof error?.stack === 'string' ? error.stack : errorToString(error); | ||
| return base + loggablePropsSuffix(error); |
Member
There was a problem hiding this comment.
Nit: diagnostic props are glued onto the last stack frame
base is the full multi-line error.stack, so base + loggablePropsSuffix(error) appends code=ENOENT errno=-2 syscall=open to the last stack-frame line, e.g. at open (node:fs:…) code=ENOENT errno=-2. Diagnostic fields read more clearly on their own line (they're the triage signal, and this keeps them off a frame that looks like part of the trace).
Suggested fix:
Suggested change
| return base + loggablePropsSuffix(error); | |
| const suffix = loggablePropsSuffix(error); | |
| return suffix ? `${base} | |
| ${suffix.trimStart()}` : base; |
—
Generated by Barber AI
cb1kenobi
approved these changes
Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the raw-
Errorlogging leak class (#1734) at its root. Harper's logger extends Node'sConsole; passing a rawErrorto a Console method formats it withutil.inspect, which dumps every own-enumerable property after the stack. Libraries/app code stash secrets on thrown Errors (axiosconfig/requestheaders, anhdb_secretused for an outboundAuthorizationheader), sologger.<level>(error)landed those credentials verbatim inhdb.logat default level — retrievable viaread_log.Two mechanisms, matched to the two leak surfaces:
HarperLoggerlevel methods — plus the inheritedlog/dir/table, which also bypassutil.inspect's guard otherwise — now route everyErrorargument througherrorForLog()(sanitizeErrorArgs) inside the level gate. This covers every logger call — all first-party code, future code, and component/app code holding aserver.logger— with no per-site changes needed. Gated-out calls pay nothing beyond an arg scan; allocation only happens when an Error is present; a revoked-Proxy arg can't make the logger throw (isErrorLiketry/catch).console.*calls, scoped by error provenance (4 sites).harper_loggercaptures both stdout and stderr intohdb.log, but Node's Console runsutil.inspectbefore the capture hook — soconsole.error(rawError)leaks identically. A sanitizing console subclass was rejected (overriding console methods makes the inspector report the override frame instead of the real call site), so wrapping is per-site — and scoped to where the error can actually originate in app/library code:JSONStream(serializes resource iterator output — the origin-fetch axios scenario of server/REST.ts logs the raw Error object to hdb.log — leaks secrets/sensitive data attached to escaping Errors #1734 flows through here),socketRouter's genericuncaughtException,componentLoaderapp-load failures,loadRootComponents'npm/pacoteinstall of user packages. Sites with a closed error set (fs/LMDB/pm2/config/our own code) stay raw — those errors cannot carry stashed credentials.Additionally, a small allowlist of diagnostic Error properties —
code,status,statusCode,errno,syscall— is now rendered alongside the stack/cause chain. These are common triage fields (Node error codes, HTTP status) that libraries don't use to carry secrets, unlike arbitrary properties (axios'config/request), which stay excluded.pathis deliberately not included — it can reveal internal filesystem layout and the message/stack already names the failing operation.History
This is a clean transplant of
kris/log-error-leak-sweep(formerly #1744) onto currentmain. #1744's base branch (kris/rest-log-error-leak-1734, the primitive-adding PR #1737) was squash-merged, so the original branch's commit history no longer rebases cleanly againstmain— reopening #1744 as-is would showREST.ts/http.ts/graphqlQuerying.tsas changed again, redundantly re-diffing #1737's already-merged content. This PR reapplies the sweep's net diff directly on top of the squashedmain; #1744 is closed as superseded. It also folds in two#1737review comments that were left unresolved when that PR merged (node:utilimport scope; moveerrorToString's null-guard intoerrorToStringitself — see commit messages) and adds the diagnostic-property allowlist per a follow-up request after #1744 was mistakenly closed instead of merged.Where to look
utility/logging/harper_logger.ts—sanitizeErrorArgs/isErrorLike(the auto-wrap) anderrorToLogString/loggablePropsSuffix(the allowlist) are the load-bearing changes. Detection isinstanceof Error || util.types.isNativeError(cross-realm safe), wrapped in try/catch for exotic objects (revoked Proxy) where eveninstanceofcan throw.JSONStream.ts,socketRouter.ts,componentLoader.ts,loadRootComponents.js) — mechanical; only the error arg getserrorForLog(...)/harperLogger.errorForLog(...), message strings/data args untouched.JSONStream'serrorToStringresponse pushes stay raw (client-facing body, not a log line) and are unaffected by the allowlist (allowlist only applies insideerrorToLogString, used for log lines).logger.error({ err })) is not rewritten — deep-walking every logged structure isn't worth the per-call cost, and the server/REST.ts logs the raw Error object to hdb.log — leaks secrets/sensitive data attached to escaping Errors #1734 threat is raw thrown errors escaping directly to the logger.Known gap (accepted)
Component/app code calling global
console.error(err)directly (not through Harper's logger) still leaks via the stdio capture — only our own console sites are wrapped. Fixing that class requires the console subclass rejected above for inspector-ergonomics reasons, or an upstream-Node-level answer.Testing
44 unit tests (was 9 before this work started): auto-wrap (sole-arg/any-position/cause-chain secret exclusion, non-Error objects untouched, level gating preserved,
loggerWithTagcoverage,log/dir/tablebypass closed, revoked-Proxy no-throw) + the diagnostic-property allowlist (code/status/statusCode/errno/syscall surfaced,pathexcluded, siblings-still-excluded, cause-chain coverage). Built todistand ran against it (mocha's--conditions=typestrippath can silently fall back to staledist): 44 passing.oxlint+prettierclean.Cross-model review
Thorough mode ran on an earlier iteration of this sweep (Codex found 3 missed sites + the
log/dir/table/revoked-Proxy issues fixed in the auto-wrap design here; the Harper domain pass found the console-capture surface and non-standard logger bindings that informed this design). Gemini (agy) leg timed out on both attempts — no second-model coverage from Gemini on this iteration.Generated by an LLM (Claude Sonnet 5 / Opus 4.8 / Fable 5 — multi-session).