fix: decode SSE events without line-length limit and surface transport errors - #15
Merged
Conversation
…t errors The alevinval/sse decoder capped lines at bufio.MaxScanTokenSize (64KiB) and returned io.EOF for every scanner failure. A streamed message whose SSE data line exceeded 64KiB - or any transport error mid-stream - silently ended the stream as if it had completed, handing the caller partial data with no error. Replace it with an internal bufio.Reader-based event decoder: no line-length limit, read errors propagate, and an EOF that interrupts a partially-read event surfaces as io.ErrUnexpectedEOF instead of a clean close.
The SSE grammar permits LF, CRLF, or bare CR as end-of-line. The previous
ReadString('\n')-based line reader only recognized LF, so a CR-only stream
was accumulated as one growing line: no events were delivered while the
connection was open, and closure surfaced as io.ErrUnexpectedEOF. Replace
it with a CR-aware readLine that consumes all three terminators, matching
the alevinval/sse decoder this branch replaced.
Peek(1) after a bare CR blocks on the underlying connection when the CR is the last byte the server has flushed, deferring event delivery until the next event or EOF. Track a pending CR on the decoder and swallow a leading LF on the next read instead, matching the WHATWG SSE parsing algorithm. The CR-only regression test now flushes one event and holds the connection open until it is received.
hanxiaop
approved these changes
Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR fixes two silent-truncation defects in
DoStreamingRequest's SSE decoding path:alevinval/ssedecoder readslines with a default
bufio.Scanner, which caps a line atbufio.MaxScanTokenSize(64KiB). Each streamed message arrives as a single
data: {json}line, so any messagewhose JSON exceeds ~64KiB made
Scan()fail withbufio.ErrTooLong.io.EOF. The decoder's read loop neverchecks
scanner.Err(); on any failure — the 64KiB overflow above, or a transport error(connection reset) mid-stream — it returns
io.EOF, whichDoStreamingRequesttreatsas successful completion (
close(resCh)).Combined effect: a stream carrying one oversized message, or a stream cut mid-flight,
ends exit-0 with partial data. The caller cannot distinguish it from a complete,
successful stream. #14 fixed the case where the server terminates the stream with an
{"error": ...}event; these two failures happen below that layer, on the client's ownread path, so no error event is ever seen.
How
Replace the
alevinval/ssedecoder with a small internalbufio.Reader-based SSE eventdecoder (
sseEventDecoderinpkg/grpc/gateway/request.go):ReadString('\n')grows as needed.errChinstead of closingresCh.io.ErrUnexpectedEOF; only an EOF at a clean event boundary is a normal end-of-stream.data:lines of one event join with\n,a single leading space after the colon is trimmed, comment/heartbeat lines (
: ...)and non-data fields are ignored, and both
\nand\r\nline endings are accepted(the alevinval decoder handled these the same way).
Scope: only the SSE decode path used by server-streaming RPC responses.
DoRequest(unary),
doHTTPStreamingRequest(google.api.HttpBodystreams, which useio.Copy),and
wrapStreamingResponseErrorare untouched.alevinval/sseremains a test-onlydependency (
marshaller_test.gouses it to decode the marshaller's output).Testing
New regression tests in
pkg/grpc/gateway/request_test.go:TestDoStreamingRequest_LargeEventsTestDoStreamingRequest_TruncatedStreamSurfacesErrorTestDoStreamingRequest_SSEFramingdata:linesAll three fail against the previous decoder and pass with the fix: