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):
- Define a server-streaming RPC in a proto file:
rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse);
- Generate both gRPC and HTTP transports via
protoc-gen-go-http.
- Configure an HTTP server with a timeout:
httpSrv := http.NewServer(http.Timeout(10 * time.Second))
- Implement the streaming handler to send events over 30 seconds.
- 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:
- Add a streaming-aware wrapper that skips/disables the per-request timeout for streaming handlers.
- Use
context.WithoutCancel or similar mechanism to detach the timeout from the streaming response writer.
- 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)
What happened:
HTTP server applies
Timeoutuniformly 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
streamServerInterceptorintentionally omits the timeout for streaming calls (only unary calls getctx, 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):
protoc-gen-go-http.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:Meanwhile, in
transport/grpc/server.go, the interceptor explicitly distinguishes:References:
Suggested fix direction:
The HTTP transport layer needs awareness of whether a handler is streaming vs unary. Possible approaches:
context.WithoutCancelor similar mechanism to detach the timeout from the streaming response writer.Environment: