On this page

Bug Class 6: Bad Responses, Not Outages

The dependency didn’t fail - it answered. Wrong. A field changed type, an error arrived as a 200 with an HTML body, a payload came back truncated. Your parser meets input it has never seen, and the blast radius is yours, not theirs.

The incident

A partner API ships a change: a numeric field becomes a string, and their load balancer starts serving errors as 200 OK with an HTML error page in the body. Your JSON parser panics. The panic takes the handler, the handler takes the goroutine pool, and a service that was “up” the whole time - theirs - takes down yours.

Why your tests miss it

Your mocks are generated from the contract, so they are always well-formed - that’s the point of contracts, and exactly why they can’t test this. Reality is not contract-shaped: proxies inject HTML, CDNs truncate, partners deploy on Friday.

The spec that closes it

Response rewriting - the dependency (real or mock) answers normally, then the proxy corrupts the reply on the way back:

partner_garbage = fault_assumption("partner_garbage",
    target = partner.main,
    rules  = [response(path = "/quote", status = 200,
                       body = '<html>502 Bad Gateway</html>')],
)

partner_wrong_type = fault_assumption("partner_wrong_type",
    target = partner.main,
    rules  = [response(path = "/quote", status = 200,
                       body = '{"price": "not-a-number"}')],
)

fault_matrix(
    scenarios = [checkout],
    faults    = [partner_garbage, partner_wrong_type],
    overrides = {
        (checkout, partner_garbage): lambda r: assert_eq(r.status, 502,
            "a bad upstream reply is an upstream error - and nothing crashes"),
    },
)

What to assert

  • No panics, ever: malformed input at the boundary is a handled error, not a crash - check the service is still healthy after the bad response.
  • The error is attributed correctly: a 502/424 pointing upstream, not a 500 that sends your own on-call hunting through your own code.
  • Fallbacks engage where they exist: cached value, default, degraded answer.

Which level

Protocol only - this class is response rewriting. Works identically on real dependencies and mock services: the response() rule rewrites whatever the upstream answered. See Choosing Fault Levels.

Learn the pieces