You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
middleware/protobuf today handles application/x-protobuf serialization only — a thin wrapper around proto.Marshal / proto.Unmarshal. It does not implement gRPC. User question that prompted this spike (paraphrased): "Can celeris speak gRPC in all four communication patterns via the protobuf middleware?" — the honest answer is no, and four concrete gaps block all four patterns. Spike the design for closing them in v1.5.0.
The gaps
Per-pattern matrix of what celeris supports today:
Pattern
Unary
Server streaming
Client streaming
Bidirectional
HTTP/2 transport
✅
✅
✅
✅
Write response body
✅
✅ (StreamWriter)
✅
✅
Read request body incrementally
n/a
n/a
❌
❌
Write response trailers
❌
❌
❌
❌
gRPC length-prefix framing
❌
❌
❌
❌
Gap 1 — Outbound H2 trailers (blocks all four patterns)
gRPC terminates every call with a HEADERS frame carrying grpc-status and optionally grpc-message, sent after the DATA frames. Celeris' H2 parser correctly ingests inbound trailers (see protocol/h2/stream/processor.go:701-729) but there is no public API to emit outbound trailers. No Context.SetTrailer(...), no StreamWriter.WriteTrailer(...), no engine-side hook that writes a post-body HEADERS frame with END_STREAM.
Required work:
New public API: StreamWriter.WriteTrailer(headers [][2]string) error (+ Context helper).
Engine wiring for both native engines (epoll, iouring) to flush DATA then emit a trailing HEADERS frame with END_STREAM, preserving strict ordering on the write path.
engine/std bridge: map to http.ResponseWriter trailer protocol (pre-declare in Trailer header then set entries on the trailer map — stdlib's quirk).
Gap 2 — Incremental request-body reads (blocks client/bidirectional streaming)
Every current code path buffers the full request body before invoking the handler:
protocol/h2/stream/processor.go:817-826 dispatches the handler only when the client's END_STREAM flag arrives.
internal/conn/h1.go:260-304 accumulates the full H1 body before dispatch (correct for REST, fatal for streaming).
For server/unary streaming this is fine. For client and bidirectional streaming the handler must be able to pull messages off the stream as they arrive. Required work:
Streaming-request mode opt-in (route-scoped or handler-scoped — decide in spike).
H2 processor: dispatch handler on initial HEADERS (no END_STREAM) when route is streaming; deliver DATA frames to an io.Reader / channel that the handler drains.
Back-pressure via existing flow-control window (don't advance the WINDOW_UPDATE until the handler consumes).
std-engine bridge: stop io.ReadAll; expose r.Body directly. Flag the "body retained post-handler" policy for the zero-copy rawBody interaction (v1.5.0 already lands that).
Per-stream cancellation when the client half-closes while the handler is still writing.
Gap 3 — gRPC framing
Each gRPC message on the wire is [1B compressed][4B length BE][payload]. Trivial. Can live in the new middleware/grpc package as a helper pair:
TE: trailers requirement (gRPC clients send TE: trailers; servers MUST honor trailers when present).
All middleware-layer. Lives in the new middleware/grpc package.
Proposal
Option A: First-class in core. Add StreamWriter.WriteTrailer + streaming request mode to the core engines and ship middleware/grpc as a thin layer on top. Maximises performance (zero framework-to-gRPC adapter overhead), breaks no API, but forces trailer + streaming-body work into the core schedule.
Option B: Separate middleware/grpc submodule with its own H2 hijack. grpc middleware grabs the underlying conn via Context.Detach (WS/SSE pattern) and speaks gRPC directly, bypassing celeris' H2 stream handling. Clean isolation but loses route-level middleware composition.
Option C: Hybrid. Core gets WriteTrailer + streaming-body mode (narrow additions). middleware/grpc is a standalone package that uses those hooks. Simpler than A, cleaner than B.
Spike should converge on C unless a concrete issue kills it.
Exit criteria
Decision doc: chosen design option + API sketch committed to middleware/grpc/DESIGN.md.
Prototype serving unary end-to-end (trailer write path exercised) against the official grpc-go client.
Prototype serving server streaming (validates StreamWriter + trailer ordering).
Prototype serving client streaming (validates incremental body reads + backpressure).
Prototype serving bidirectional streaming (validates both halves concurrently, including cancellation on client half-close).
Interop pass: run a subset of grpc-go's conformance suite (interop_client) against celeris.
Benchmark: unary and server-streaming gRPC RPS vs google.golang.org/grpc's default transport on msr1.
Memory characterization: goroutines + RSS per concurrent bidirectional stream at 1 k / 10 k streams.
Decision: ship the streaming-body + trailer-write work in v1.5.0 core, or stage into v1.6.0 if the surface is bigger than size/L.
Critical files (likely touched)
protocol/h2/stream/stream.go — WriteTrailer on the response-writer interface.
v1.5.0 H1 body-path refactor (bodyBuf + NextRecvBuf) establishes the "handler needs an incremental reader" mental model on the H1 side; H2 streaming mode should align.
Context
middleware/protobuftoday handlesapplication/x-protobufserialization only — a thin wrapper aroundproto.Marshal/proto.Unmarshal. It does not implement gRPC. User question that prompted this spike (paraphrased): "Can celeris speak gRPC in all four communication patterns via the protobuf middleware?" — the honest answer is no, and four concrete gaps block all four patterns. Spike the design for closing them in v1.5.0.The gaps
Per-pattern matrix of what celeris supports today:
StreamWriter)Gap 1 — Outbound H2 trailers (blocks all four patterns)
gRPC terminates every call with a HEADERS frame carrying
grpc-statusand optionallygrpc-message, sent after the DATA frames. Celeris' H2 parser correctly ingests inbound trailers (seeprotocol/h2/stream/processor.go:701-729) but there is no public API to emit outbound trailers. NoContext.SetTrailer(...), noStreamWriter.WriteTrailer(...), no engine-side hook that writes a post-body HEADERS frame with END_STREAM.Required work:
StreamWriter.WriteTrailer(headers [][2]string) error(+ Context helper).END_STREAM, preserving strict ordering on the write path.engine/stdbridge: map tohttp.ResponseWritertrailer protocol (pre-declare inTrailerheader then set entries on the trailer map — stdlib's quirk).Gap 2 — Incremental request-body reads (blocks client/bidirectional streaming)
Every current code path buffers the full request body before invoking the handler:
protocol/h2/stream/processor.go:817-826dispatches the handler only when the client's END_STREAM flag arrives.engine/std/bridge.go:59-61callsio.ReadAll(r.Body).stdlib.go:48mirrors the same.internal/conn/h1.go:260-304accumulates the full H1 body before dispatch (correct for REST, fatal for streaming).For server/unary streaming this is fine. For client and bidirectional streaming the handler must be able to pull messages off the stream as they arrive. Required work:
io.Reader/ channel that the handler drains.io.ReadAll; exposer.Bodydirectly. Flag the "body retained post-handler" policy for the zero-copyrawBodyinteraction (v1.5.0 already lands that).Gap 3 — gRPC framing
Each gRPC message on the wire is
[1B compressed][4B length BE][payload]. Trivial. Can live in the newmiddleware/grpcpackage as a helper pair:No engine work needed — celeris' existing DATA-frame path already handles the byte-level shape.
Gap 4 — Protocol ergonomics
content-type: application/grpcandgrpc-encoding,grpc-accept-encoding,grpc-timeouthandling.error→grpc-status+grpc-message).TE: trailers; servers MUST honor trailers when present).All middleware-layer. Lives in the new
middleware/grpcpackage.Proposal
Option A: First-class in core. Add
StreamWriter.WriteTrailer+ streaming request mode to the core engines and shipmiddleware/grpcas a thin layer on top. Maximises performance (zero framework-to-gRPC adapter overhead), breaks no API, but forces trailer + streaming-body work into the core schedule.Option B: Separate
middleware/grpcsubmodule with its own H2 hijack. grpc middleware grabs the underlying conn viaContext.Detach(WS/SSE pattern) and speaks gRPC directly, bypassing celeris' H2 stream handling. Clean isolation but loses route-level middleware composition.Option C: Hybrid. Core gets
WriteTrailer+ streaming-body mode (narrow additions).middleware/grpcis a standalone package that uses those hooks. Simpler than A, cleaner than B.Spike should converge on C unless a concrete issue kills it.
Exit criteria
middleware/grpc/DESIGN.md.grpc-goclient.StreamWriter+ trailer ordering).interop_client) against celeris.google.golang.org/grpc's default transport on msr1.size/L.Critical files (likely touched)
protocol/h2/stream/stream.go—WriteTraileron the response-writer interface.protocol/h2/stream/processor.go:817-826+:947-957+:1012— dispatch timing for streaming bodies.internal/conn/h2.go— server preface / trailers emit.internal/conn/response.go(H1 adapter — ensureWriteTrailerfails cleanly on H1 conns).engine/std/bridge.go:59-76+stdlib.go:47-61— stop ReadAll; expose body reader; wire trailer-header protocol.middleware/grpc/— framing, status/error mapping, content-type checks.Related
bodyBuf+NextRecvBuf) establishes the "handler needs an incremental reader" mental model on the H1 side; H2 streaming mode should align.