fix(backend): recover panics in gRPC handlers instead of crashing the daemon - #970
Open
ericvicenti wants to merge 1 commit into
Open
fix(backend): recover panics in gRPC handlers instead of crashing the daemon#970ericvicenti wants to merge 1 commit into
ericvicenti wants to merge 1 commit into
Conversation
… daemon grpc-go does not recover handler panics, and neither server installs an interceptor that does, so any panic reachable from request-handling code takes down the whole process rather than failing one call. Both servers parse data that originated elsewhere — documents fetched through the public API, and blobs pushed by arbitrary peers over p2p — so "this can never happen" assertions in that code are assertions about other people's bytes. That is not hypothetical: #909/#910 fixed one such panic, an oversized move op in an imported document that crashed the production gateway repeatedly through plain GetDocument calls, turning a public URL into a button that restarted the node. Those PRs fixed that panic. This one addresses the class, so the next one we haven't found costs a failed RPC instead of an outage. Recovery is installed as the innermost interceptor on both servers, so the outer metrics and tracing interceptors observe a normal Internal error and account for the failed call, rather than having the panic unwind straight through them. The panic value and stack go to the log with the method name attached; the caller gets a bare Internal, since the library's default handler returns the whole stack over the wire. On the daemon server it is wired inside initGRPC rather than at the cmd/seed-daemon call site, so every embedder is covered, including the desktop app and the tests. go-grpc-middleware/v2 was already present as an indirect dependency, so this only promotes it to direct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Follow-up to #909 / #910, as suggested in juligasa's review. Those PRs fixed one panic; this closes the class.
The problem
grpc-go does not recover handler panics, and neither of our servers installs an interceptor that does —
daemon.gochains only otel + Prometheus,hmnet.goonly the metrics interceptors. So any panic reachable from request-handling code kills the whole process instead of failing one call.That matters here because both servers parse data that originated somewhere else:
So a
panic("BUG: this can never happen")deep in that code is not an assertion about our own state — it's an assertion about someone else's bytes, and they get to decide whether it holds.We already paid for this. The op-ID index overflow fixed in #909/#910 was exactly that shape: a real imported document (the Spanish civil code, a 6,411-block move op) crossed a limit our own writer could never reach, and
daemon_gatewaypanicked 7 times in 72 hours through plainGetDocumentcalls. A public URL was, in effect, a restart button.The fix
A small
backend/util/grpcrecoverypackage wrappinggo-grpc-middleware/v2/interceptors/recovery, installed on both servers.Ordering — recovery goes innermost (last in each chain).
grpc.ChainUnaryInterceptorappends, so the last interceptor added is the one closest to the handler. Recovering there means the outer metrics/tracing interceptors see an ordinaryInternalerror and record the failed RPC; recovering outermost would let the panic unwind through them and the call would go unaccounted for.What goes where. The panic value, the stack, and the method name go to the log. The caller gets a bare
codes.Internal— notably not the library's default handler, which returns the entire stack trace to the caller as the status message.Wiring point. On the daemon server it's inside
initGRPCrather than at thecmd/seed-daemoncall site, so every embedder is covered — desktop app and tests included, not just the CLI binary.Tests
Internal, the panic value and stack do not appear in the client-visible error, the log entry carries the method name, and — the actual point — a second call on the same connection still succeeds.debug.Stack()is called from inside the deferred recover, and the test pins that it still walks back past the runtime'spanic(frame into the handler. Without that it would only show the interceptor and the panic site would be lost.Notes
go-grpc-middleware/v2was already ingo.modas an indirect dep; this only promotes it to direct. No version change,go.sumuntouched.daemon,hmnet, and the new package pass (incl.-race), andgolangci-lint --new-from-rev=origin/main ./backend/...reports 0 issues.🤖 Generated with Claude Code