Jens Geyer created THRIFT-6210:
----------------------------------

             Summary: Go: an unrecovered panic in a handler ends the whole 
server process
                 Key: THRIFT-6210
                 URL: https://issues.apache.org/jira/browse/THRIFT-6210
             Project: Thrift
          Issue Type: Improvement
          Components: Go - Library
            Reporter: Jens Geyer


h2. What happens

{{lib/go/thrift}} never calls {{recover()}} — there is no call anywhere in the 
package outside {{configuration_test.go}}. {{TSimpleServer}} runs each accepted 
connection on a goroutine of its own ({{simple_server.go:211}}) and reaches 
{{processor.Process}} from there with nothing in between:

{code}
go func() {
        defer p.wg.Done()
        defer cancel()
        defer client.Close()
        if err := p.processRequests(client); err != nil {
                ...
        }
}()
{code}

Go ends the whole process on an unrecovered panic in any goroutine, so a panic 
anywhere inside a handler takes the server down — every other connection with 
it, not only the one being served. Reproduced against master @ {{5c5e93e1e}} 
with a hand-written processor whose handler panics: one request, and the 
process exits with status 2.

{code}
panic: handler blew up

goroutine 50 [running]:
main.boomFunc.Process(...)
main.(*proc).Process(...)
github.com/apache/thrift/lib/go/thrift.(*TSimpleServer).processRequests(...)
        lib/go/thrift/simple_server.go:368
github.com/apache/thrift/lib/go/thrift.(*TSimpleServer).innerAccept.func1()
        lib/go/thrift/simple_server.go:215
exit status 2
{code}

h2. The panic has to come from the handler, not from the wire

The peer-facing read path was swept for inputs that panic rather than return an 
error, using native Go fuzzing over the library on master @ {{5c5e93e1e}}: 
twelve targets covering TBinaryProtocol (strict and non-strict), 
TCompactProtocol, TJSONProtocol, TSimpleJSONProtocol, THeaderProtocol including 
its transform-ID and info-header parsing, TFramedTransport, a full server-side 
dispatch hop through TMultiplexedProcessor, and {{ParseTuuid}}. Enveloped and 
bare-struct entry points both. About 28 million executions in total (120s per 
target, coverage-guided), no panic in any target.

That matches what the code says: generated Go read code returns errors, and 
wire-supplied container counts and string lengths are bounds-checked before 
they size an allocation. So the panic has to originate in application code — a 
nil map write, a slice index, a failed type assertion, or any library the 
handler calls.

That makes this a question about the library's default behaviour, not a defect 
reachable from outside.

h2. The same panic is survivable over HTTP

{{NewThriftHandlerFunc}} runs under {{net/http}}, which recovers per request, 
logs the stack and drops that one connection. So a handler bug that is merely 
an error for a Thrift-over-HTTP server is fatal for the socket server. The 
inconsistency is worth deciding about one way or the other.

h2. What an application can already do

No library change is needed to fix this in an application. Processor middleware 
covers it in about twenty lines, and keeps the connection by reporting a 
{{TApplicationException}} the way generated code does:

{code}
func recoverMiddleware(name string, next thrift.TProcessorFunction) 
thrift.TProcessorFunction {
        return thrift.WrappedTProcessorFunction{
                Wrapped: func(ctx context.Context, seqID int32, in, out 
thrift.TProtocol) (ok bool, err thrift.TException) {
                        defer func() {
                                if r := recover(); r != nil {
                                        x := thrift.NewTApplicationException(
                                                thrift.INTERNAL_ERROR,
                                                fmt.Sprintf("panic in %s", 
name),
                                        )
                                        out.WriteMessageBegin(ctx, name, 
thrift.EXCEPTION, seqID)
                                        x.Write(ctx, out)
                                        out.WriteMessageEnd(ctx)
                                        out.Flush(ctx)
                                        ok, err = true, nil
                                }
                        }()
                        return next.Process(ctx, seqID, in, out)
                },
        }
}

processor = thrift.WrapProcessor(processor, recoverMiddleware)
{code}

Verified: with the middleware the server survives, the client receives the 
exception, and the process stays up.

h2. The question

Should the library do this itself?

For: {{net/http}} does it, the blast radius is the whole process rather than 
one request, and one request ending one request is the behaviour every other 
Thrift binding already has — an uncaught exception on a per-connection thread 
ends that thread, not the runtime.

Against: a substantial part of the Go community holds that libraries should not 
swallow panics, on the grounds that program state after a panic is unknown and 
continuing can be worse than stopping. A server that keeps running with a 
corrupted invariant is a real cost, not a hypothetical one.

Options, roughly in order of how much they commit to:

# Leave the behaviour alone; document it, and ship the middleware above in the 
library as an opt-in (for example {{thrift.RecoverMiddleware}}), so 
applications do not each write their own.
# Recover per request inside {{TSimpleServer}} by default, with a knob to turn 
it off.
# Do nothing.

Option 1 looks like the smallest thing that removes the trap without deciding 
the argument for everybody, but this needs a maintainer call rather than a 
patch.

Related: THRIFT-5073 raised "we can log panic situation and return 
TApplicationException to client instead of EOF" as one motivation for processor 
interceptors. Middleware landed since, but no recovering middleware ships with 
the library.

Investigation and this description were AI-assisted (Claude Opus 5); the 
findings were verified by execution against master.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to