Skip to content

Fail soft on operations API domain socket bind failure; warn on path-length overflow#1907

Draft
kriszyp wants to merge 2 commits into
mainfrom
fix/operations-api-domain-socket-path-length
Draft

Fail soft on operations API domain socket bind failure; warn on path-length overflow#1907
kriszyp wants to merge 2 commits into
mainfrom
fix/operations-api-domain-socket-path-length

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 23, 2026

Copy link
Copy Markdown
Member

What / why

A Unix domain socket path over ~107 bytes (Linux) / ~103 bytes (macOS) fails at listen() time with EINVAL. server/threads/threadServer.js's listenOnPorts() wrapped every listener — the operations API's domain socket and its TCP port alike — in one Promise.all(), so a single EINVAL rejected the whole batch and propagated out of startHTTPThreads(), which bin/run.ts's main() catches by calling process.exit(1) — killing the entire Harper process over what should be a non-fatal loss of a convenience mirror.

A long rootPath (e.g. any .claude/worktrees/<name> checkout — this fix was developed in one) is enough on its own to trigger this with the operations API's default relative domain socket name (operations-server), with nothing in the default output explaining why.

Changes

  • server/threads/threadServer.js: a domain socket listen failure now logs and resolves instead of rejecting, in both the Node and Bun listen loops, so the rest of the batch (TCP ports, other domain sockets) is unaffected.
  • validation/configValidator.ts: adds getDomainSocketPathLengthWarning, which resolves operationsApi.network.domainSocket against rootPath and flags a byte length past the platform's sun_path limit. It's a plain function rather than a Joi schema rule, because Joi skips a field's own .custom() whenever that field's value came from .default() — exactly the common case here (domain socket left unset, only rootPath is long).
  • config/configUtils.ts: calls the warning check after config validation and logs (non-blocking) — a lost domain socket shouldn't refuse to start Harper the way a real config error does; the TCP port is unaffected either way.

Test notes

  • unitTests/server/threads/threadServerListenOnPorts.test.js (new): confirms listenOnPorts() resolves rather than rejects when a domain socket listen fails, and that a sibling TCP listener in the same batch still comes up. Verified these fail against the pre-fix code (rejected with the simulated EINVAL).
  • unitTests/validation/configValidator.test.js: unit tests for getDomainSocketPathLengthWarning covering relative/absolute paths, the disabled (false) case, and the exact byte boundary.
  • unitTests/config/configUtils.test.js: confirms validateConfig logs a warning (not a thrown error) when the resolved domain socket path is too long.
  • Ran test:unit:config, test:unit:validation-equivalent, and the affected unitTests/server/** files locally — all green, no regressions.

Risks / open questions

  • The path-length check is a warning, not a hard validation error, by design (see rationale above) — flagging in case reviewers feel a long-path config should instead fail config validation outright.
  • Didn't touch server/operationsServer.ts, which the originating finding cited — that file's try/catch around serverRegistration.http(...) never actually reaches the failing listen() call (that happens later, on the main thread, in threadServer.js's listenOnPorts()), so no change was needed there.

Generated with Claude Code (Sonnet 5).

🤖 Generated with Claude Code

A Unix domain socket path over ~107 bytes (Linux) fails at listen()
time with EINVAL. server/threads/threadServer.js's listenOnPorts()
wrapped every listener — domain socket and TCP alike — in one
Promise.all(), so a single EINVAL rejected the whole batch and
propagated out of startHTTPThreads(), which bin/run.ts's main()
catches by calling process.exit(1) — killing the entire process over
what should be a non-fatal loss of a convenience mirror.

A long rootPath (e.g. any `.claude/worktrees/<name>` checkout, which
this very fix was developed in) is enough on its own to trigger this
with the operations API's default relative domain socket name.

- threadServer.js: a domain socket listen failure now logs and
  resolves instead of rejecting, in both the Node and Bun listen
  loops, so the rest of the batch (TCP ports, other domain sockets)
  is unaffected.
- configValidator.ts: adds getDomainSocketPathLengthWarning, which
  resolves operationsApi.network.domainSocket against rootPath and
  flags a byte length past the platform's sun_path limit. It's a
  plain function rather than a Joi schema rule, because Joi skips a
  field's own .custom() whenever the field's value came from
  .default() — exactly the common case here (domainSocket left
  unset). configUtils.ts calls it after validation and logs
  (non-blocking): a lost domain socket shouldn't refuse to start
  Harper the way a real config error does.

Refs harper-1839-sql-selectall-pk-sort

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a fail-soft mechanism for Unix domain socket binding errors during startup, preventing startup abortion when a domain socket path exceeds the platform's limit (103 bytes on macOS, 107 bytes on Linux). It adds path length validation warnings in configUtils.ts and configValidator.ts, along with corresponding unit tests. The reviewer suggests updating getDomainSocketPathLengthWarning to accept an optional platform parameter to facilitate robust, platform-independent unit testing of the path resolution logic on any CI host.

Comment thread validation/configValidator.ts Outdated
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Lets the Unix-socket path-length check be unit tested for
darwin/linux/win32 resolution from any CI host, instead of only ever
exercising process.platform's own branch. Defaults to process.platform
so callers are unaffected.

Addresses gemini-code-assist review feedback on PR #1907.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant