fix(pipeline): do not treat a 0 stream() return as EOF in streamReaderToWriter - #301
Open
detail-app[bot] wants to merge 1 commit into
Conversation
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a small, self-contained bug fix correcting zero-byte stream handling for compressed bodies while preserving oversized-body rejection. Regression tests cover the affected gzip/zstd paths and the existing size limit. You can add or adjust custom eligibility rules. Learn more. |
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.
Detail bug report: View on Detail
Fixes ENG-702
Bug
streamReaderToWriter(src/pipeline/pipeline.zig) — the bounded reader→writer copy used byexec.processBufferedto decode a compressed client body into an arena buffer for policy evaluation — treated a0return fromReader.streamas end-of-stream (if (bytes == 0) break;). This violates thestd.Io.Reader.VTablecontract: a0return "does not indicate end of stream" — onlyerror.EndOfStreamis EOF.The std gzip and zstd decompressors select an indirect reader vtable when given a non-zero decode buffer (the production path always allocates one). That vtable's
streamdecodes a block into the reader's internal buffer and unconditionally returns0; the bytes appear on the nextstreamcall. So the loop broke immediately withtotal_bytes == 0, the trailing excess probe then read the buffered byte, and the function spuriously returnederror.BodyTooLargefor every non-empty gzip/zstd body on the buffered path.The bug is deterministic. Affected routes are the JSON variants that route to
pipe_buffered(datadog/api/v2/seriesmetrics JSON and OTLP JSON/v1/logs|metrics|traces), but only when policies are configured for the signal (thepoliciesActiveForgate). On httpz, the spurious error surfaces to the client as413; on stdio, it's caught by the existing fail-open branch which forwards the raw compressed body unevaluated (a silent policy bypass). The existing unit tests missed it because they usedstd.Io.Reader.fixed, whose vtable never returns0mid-stream.Fix
Only terminate the loop on
error.EndOfStream(nowreturn total_bytesdirectly — EOF needs no excess probe). A0return no longer breaks the loop; it loops again to drain the reader's freshly-filled internal buffer. The trailingreadSliceShortexcess probe is retained for thetotal_bytes >= max_bytesexit, so themax_bytesbound is still enforced (genuine oversize still returnserror.BodyTooLarge). This mirrors the correct pattern already used byencoding.zig'sdecodeAll.Testing
encoding.Decoderreader (the uncovered production path) rather thanstd.Io.Reader.fixed: gzip and zstd each copy a 10 KB body verbatim withmax_byteswell above the body, and a gzip oversize case confirmserror.BodyTooLargeis still returned when the decoded body genuinely exceeds the bound. The decompressor-path tests fail withBodyTooLargeat the excess probe when the buggyif (bytes == 0) break;line is temporarily restored, confirming causation.zig fmt --check,ziglint,zig build, andzig build test -Doptimize=ReleaseSafeall pass (522/523 with the 1 pre-existing unrelated S3 e2e skip; no regressions).policiesActiveFor(.metric)is true: gzip and zstd compressed Datadog v2 Series JSON POSTs to/api/v2/seriesreturn202(was413) and reach a local echo upstream; withmax_decoded_bytesset below the decoded body the request still returns413(bound preserved); with nopolicy_providersthe raw body passes through unchanged (173 raw bytes); and a corrupt gzip body still fails open withReadFailed(notBodyTooLarge). The same compressed+policy request against the stdio frontend returns202with nobuffered transform failed openwarn and forwards a re-encoded body (161 bytes, not the raw 173), confirming valid compressed bodies no longer trigger the spurious fail-open bypass.Automatic Fixes PRs can be configured here.