amir20/dozzle · warning
EOF error while converting gRPC to error
Error message
EOF error while converting gRPC to error: %w
What it means
rpcErrToErr translates a gRPC status with code Unknown and message "EOF" into a wrapped io.EOF. A server-side streaming handler returning io.EOF surfaces over gRPC as Unknown/"EOF", and this wrapper makes the underlying EOF visible (with %w) so callers can detect normal stream end.
Solutions
- Treat errors.Is(err, io.EOF) as a clean stream end and reconnect/resubscribe with backoff
- Upgrade agent and server to matching versions so handlers return nil instead of EOF on stream end
- Check agent container health/restarts (`docker ps`, logs) if EOFs occur repeatedly
- Use the client's auto-reconnect flows (container store resubscribes) rather than treating EOF as fatal
Example fix
// before
err := <-errCh
if err != nil { log.Fatal().Err(err).Msg("stream failed") }
// after
err := <-errCh
if errors.Is(err, io.EOF) {
time.Sleep(time.Second) // backoff and resubscribe
go resubscribe()
return
} Defensive patterns
Strategy: retry
Try / catch
if err != nil {
if errors.Is(err, io.EOF) {
time.Sleep(backoff)
go resubscribe() // clean stream end, reconnect
return
}
log.Error().Err(err).Msg("stream failed")
} Prevention
- Match agent and server versions so handlers end streams with nil, not EOF
- Always treat errors.Is(err, io.EOF) from agent streams as reconnectable
- Monitor agent container restart counts; frequent EOFs often accompany restarts
When it happens
Trigger: Any streaming RPC call (StreamEvents, StreamStats, StreamNewContainers, sendLogs) whose server handler returned io.EOF: the agent-side stream generator closed cleanly but the server returned the EOF as a status error instead of nil.
Common situations: Agent restarting mid-stream; watching logs of a container whose stream ended; agent version where ListContainers/Events handler returns EOF on client disconnect; transient EOF right after connect during agent startup.
Related errors
AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07).
Data as JSON: /api/errors/7ffaee08617c3f93.
Report an issue: GitHub.
Appendix: source
Thrown at internal/agent/client.go:118
if len(parts) == 3 {
group = parts[2]
}
return parts[0], name, group, nil
}
func rpcErrToErr(err error) error {
if err == nil {
return nil
}
status, ok := status.FromError(err)
if !ok {
return err
}
if status.Code() == codes.Unknown && status.Message() == "EOF" {
return fmt.Errorf("EOF error while converting gRPC to error: %w", io.EOF)
}
switch status.Code() {
case codes.Canceled:
return fmt.Errorf("canceled: %v with %w", status.Message(), context.Canceled)
case codes.DeadlineExceeded:
return fmt.Errorf("deadline exceeded: %v with %w", status.Message(), context.DeadlineExceeded)
case codes.Unknown:
return fmt.Errorf("unknown error: %v with %w", status.Message(), err)
case codes.OK:
return nil
default:
return fmt.Errorf("unknown code: %v with %w", status.Code(), err)
}
}
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (<-chan *container.LogEvent, error) {
stream, err := c.client.LogsBetweenDates(ctx, &pb.LogsBetweenDatesRequest{View on GitHub (pinned to d9463cbe21)