#807·lura

router/gin: context.Canceled should return 499, not 500

Author: PedrOmarDevCreated Sep 2, 2026Updated Sep 2, 2026

Problem

When a client disconnects before KrakenD (or any Lura-based gateway) finishes proxying the upstream response, the Go runtime surfaces the abort as context.Canceled. The Gin endpoint handler in CustomErrorEndpointHandler currently falls through to errF(err), which maps to server.DefaultToHTTPError and returns 500 Internal Server Error.

This is semantically incorrect: the server did not fail — the client left. Logging and alerting systems downstream count these as server errors, producing false positives in error-rate dashboards and on-call alerts.

Expected behaviour

The handler should return 499 Client Closed Request for context.Canceled. This is the convention established by nginx and adopted by every major API gateway and load balancer. It signals to any observer that the fault was on the client side, not the server.

Proposed fix

Inside the if response == nil branch of CustomErrorEndpointHandler (router/gin/endpoint.go):

  1. Add "errors" to the import block (alphabetical, after "context").
  2. Check errors.Is(err, context.Canceled) before the responseError type-assert.
    • If true → c.Status(499).
    • Skip ErrorResponseWriter (client is gone; writing a body is wasteful and can produce spurious errors in logs).
  3. All other errors — including context.DeadlineExceeded — are not affected. Server-side timeouts remain 500.

errors.Is is used rather than a direct equality check so that wrapped errors (e.g. fmt.Errorf("...: %w", context.Canceled)) are also caught.

Why engine.ContextWithFallback = true makes this reliable

Lura sets engine.ContextWithFallback = true (router/gin/engine.go). This makes Gin propagate the request context's cancellation into c itself, so when the client disconnects the underlying context.Context is cancelled and the proxy returns context.Canceled as a first-class error — not wrapped in a Gin-internal type.

Known limitation: multi-backend endpoints

proxy.mergeError (proxy/merging.go) wraps upstream errors but does not implement Unwrap(), so errors.Is(err, context.Canceled) returns false for endpoints with more than one backend. Those endpoints will continue to return 500 on client abort until mergeError gains Unwrap(). Single-backend endpoints (the vast majority) are fully covered.

Files affected

  • router/gin/endpoint.go
  • router/gin/endpoint_test.go (new tests for direct, wrapped, deadline, and generic error cases)