Skip to content

Hunt for handler panics

The module recovers a handler panic at the goroutine it owns and restarts the subscription under its own policy. That is a backstop, and a backstop is not a strategy. This is the strategy.

func TestMyHandlerSurvivesTheCorpus(t *testing.T) {
    test.Handler(t, myHandler)
}

That is the whole integration. test.Handler runs your handler against every case in the corpus and fails the test if any of them panics.

Add a fuzz target

func FuzzMyHandler(f *testing.F) {
    for _, c := range test.Corpus() {
        f.Add(c.Delivery.Event.Data)
    }

    f.Fuzz(func(t *testing.T, data []byte) {
        test.Fuzz(t, myHandler, data)
    })
}

The corpus seeds it, so the toolchain starts from inputs known to be awkward rather than from nothing.

What the corpus contains, and why these

The panics that actually happen in a message handler are predictable, which is what makes enumerating them once here worthwhile instead of rediscovering them in every consumer.

Case The mistake it finds
Data is nil, or empty indexing or unmarshalling without checking
Data is not JSON at all assuming the content type was honoured
Data is JSON but not an object json.Unmarshal into a struct, then dereferencing
Data is an object with none of the expected fields dereferencing a pointer field that stayed nil
a payload far larger than expected a fixed-size buffer, or an allocation nobody bounded
only the required attributes set reading Subject or DataContentType unconditionally
a nil extension map writing to Extensions without allocating it

Every one is a shape a real broker can deliver. Nothing in the corpus is impossible, only unwelcome.

A returned error is not a failure

test.Handler fails on a panic and not on a returned error.

That distinction is the one the whole module rests on. A handler returning an error is the contract working: the error is returned, counted and logged, and the message is not retried. A handler panicking is a defect. A harness that conflated them would report a working handler as broken.

It proves nothing, and that is not modesty

A corpus and a fuzzer are testing, not verification. Passing means no panic was found on those inputs, never that your handler is panic-free.

That wording is deliberate. If this helper let somebody believe otherwise, the module's panic counter would read as impossible rather than as unexpected, and a number nobody believes is a number nobody watches.

Where the untrusted input usually is

Anywhere the payload originates outside your own service. A bot processing chat messages, a gateway accepting webhooks, an ingest path reading somebody else's export. Those are the handlers worth fuzzing, and the reason this corpus is in the module rather than in the one consumer that thought of it.