Skip to content

HTTP server returns 503 instead of 499 when client disconnects during nested fiber execution #5968

@wewelll

Description

@wewelll

What version of Effect is running?

  • effect: 3.19.13
  • @effect/platform: 0.94.0
  • @effect/platform-node: 0.104.0

What steps can reproduce the bug?

The bug occurs when causeResponse receives a cause tree where a non-client-abort interrupt appears before clientAbortFiberId in traversal order:

const parentFiberId = yield* Effect.fiberId
const childInterrupt = Cause.interrupt(parentFiberId)
const clientAbortInterrupt = Cause.interrupt(HttpServerError.clientAbortFiberId)
const cause = Cause.sequential(childInterrupt, clientAbortInterrupt)
const [response] = yield* HttpServerError.causeResponse(cause)
// response.status is 503 (bug) instead of 499 (expected)

What is the expected behavior?

The server should return 499 (Client Closed Request) because clientAbortFiberId is present in the cause's interruptors.

What do you see instead?

The server returns 503 (Service Unavailable) because causeResponse only checks the first interrupt node encountered during Cause.reduce traversal.

Root Cause Analysis

The issue is in causeResponse which checks cause.fiberId === clientAbortFiberId on each interrupt node during traversal:

case "Interrupt": {
  if (acc[1]._tag !== "Empty") {
    return Option.none()
  }
  const response = cause.fiberId === clientAbortFiberId ? clientAbortError : serverAbortError
  return Option.some([Effect.succeed(response), cause] as const)
}

Cause.reduce uses left-first traversal for Sequential nodes. If another interrupt appears before clientAbortFiberId in the cause tree, the function returns 503 even though the client abort is present.

Fix

Pre-compute whether clientAbortFiberId exists anywhere in the cause tree before traversing:

const isClientAbort = HashSet.has(Cause.interruptors(cause), clientAbortFiberId)
// ... then use isClientAbort instead of checking individual node's fiberId

Note on Testing

Effect's runtime properly propagates interrupt IDs to child fibers. When a parent fiber is interrupted with clientAbortFiberId, child fibers also receive that same ID. This means real HTTP tests with Effect.all or nested fibers don't easily reproduce the problematic cause structure.

The bug manifests with specific cause tree structures that can occur in edge cases or be constructed synthetically for testing.

Additional information

This issue is particularly problematic in proxy/gateway scenarios where 503 suggests server unavailability and can trigger incorrect retry logic and alerting, while 499 correctly indicates the client closed the connection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions