Skip to content

bug(transport/http): server-side timeout kills streaming connections (SSE/WebSocket) #3841

Description

@DrReMain

What happened:

HTTP server applies Timeout uniformly to all requests, including streaming ones (SSE, WebSocket). Any server-streaming or bidirectional-streaming RPC exposed via HTTP is forcibly disconnected after the configured timeout, making long-lived streaming connections unusable.

This is a design inconsistency with the gRPC transport: the gRPC streamServerInterceptor intentionally omits the timeout for streaming calls (only unary calls get ctx, cancel := transport.contextWithTimeout(c, timeout)), but the HTTP transport makes no such distinction.

What you expected to happen:

HTTP streaming connections (server-streaming / bidirectional-streaming) should behave consistently with gRPC streaming — the server-side timeout should not be applied to streaming RPCs, allowing long-lived connections to remain open as long as needed.

Both transports serve the same proto definition; the timeout behavior should be aligned.

How to reproduce it (as minimally and precisely as possible):

  1. Define a server-streaming RPC in a proto file:
    rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse);
  2. Generate both gRPC and HTTP transports via protoc-gen-go-http.
  3. Configure an HTTP server with a timeout:
    httpSrv := http.NewServer(http.Timeout(10 * time.Second))
  4. Implement the streaming handler to send events over 30 seconds.
  5. Observe:
    • gRPC client: receives all events correctly (stream has no timeout).
    • HTTP client (SSE): connection is killed after 10 seconds by the server timeout.

Anything else we need to know?:

Root cause analysis:

In transport/http/server.go, the timeout is applied to every request via middleware, with no check for whether the handler is streaming:

// transport/http/server.go — applies timeout to ALL requests
func (s *Server) handle(path string, h http1.Handler) {
    s.router.Handle(path, s.filterChain(path, h))
}

Meanwhile, in transport/grpc/server.go, the interceptor explicitly distinguishes:

// transport/grpc/server.go — stream interceptor does NOT apply timeout
func (s *Server) streamInterceptor() grpc.StreamServerInterceptor {
    // ... no timeout applied for streaming
}

References:

Suggested fix direction:

The HTTP transport layer needs awareness of whether a handler is streaming vs unary. Possible approaches:

  1. Add a streaming-aware wrapper that skips/disables the per-request timeout for streaming handlers.
  2. Use context.WithoutCancel or similar mechanism to detach the timeout from the streaming response writer.
  3. Allow per-route timeout configuration so streaming endpoints can set unlimited timeout.

Environment:

  • Kratos version: v3 (main branch, commit dc13681 and later)
  • Go version: go1.24+
  • OS: macOS / Linux
  • Others: Affects all HTTP streaming use cases (SSE, WebSocket via bidi-streaming)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions