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

  1. Treat errors.Is(err, io.EOF) as a clean stream end and reconnect/resubscribe with backoff
  2. Upgrade agent and server to matching versions so handlers return nil instead of EOF on stream end
  3. Check agent container health/restarts (`docker ps`, logs) if EOFs occur repeatedly
  4. 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

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)