Repository navigation
fix(declarative): detect gzip payloads in GzipParser instead of trusting headers - #1124
Conversation
Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1787193372-gzip-decoder-passthrough#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1787193372-gzip-decoder-passthroughPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes inconsistent gzip handling in the declarative GzipDecoder between Connector Builder “Test read” and real syncs by making GzipParser detect gzip content from the payload (magic bytes) instead of relying on response headers.
Changes:
- Update
GzipParserto peek the first bytes of the stream, gunzip only when gzip magic bytes are present, otherwise pass the stream through unchanged (including support for non-seekable/short-read streams via a prefixed wrapper). - Make
create_gzip_decoderuseGzipParserin both Builder and sync modes (including as theby_headersfallback parser) for consistent behavior. - Add unit tests covering gzip payload detection, pass-through behavior, short-read streams, nested gzip parsers, and factory behavior across Builder vs sync modes.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py |
Implements payload-based gzip detection in GzipParser and introduces a prefixed stream wrapper to support non-seekable streams. |
airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py |
Switches gzip decoder construction to use GzipParser consistently in Builder and sync modes (including as fallback). |
unit_tests/sources/declarative/decoders/test_composite_decoder.py |
Adds focused GzipParser unit tests for gzip detection, short reads, non-seekable streams, empty payloads, and nested gzip parsers. |
unit_tests/sources/declarative/parsers/test_model_to_component_factory.py |
Adds factory-level tests ensuring create_gzip_decoder handles gzip payloads across header shapes and Builder vs sync modes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
PyTest Results (Fast)4 363 tests +97 4 352 ✅ +98 7m 54s ⏱️ +54s Results for commit 511654b. ± Comparison against base commit db6f309. This pull request removes 1 and adds 98 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
PyTest Results (Full)4 366 tests +97 4 354 ✅ +97 9m 50s ⏱️ - 1m 14s Results for commit 511654b. ± Comparison against base commit db6f309. This pull request removes 1 and adds 98 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Patrick Nilan (pnilan)
left a comment
There was a problem hiding this comment.
🤖 This PR review was generated by an AI Agent.
Requesting changes for two gzip parity/test-validity gaps and one test-hardening issue. The previously discussed typing-extensions concern is intentionally omitted because Pydantic already guarantees a sufficient version.
Co-Authored-By: bot_apk <apk@cognition.ai>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds gzip signature detection with stream-byte replay and updates decoder selection for transport and content compression. Tests cover seekable and non-seekable streams, short reads, plain payloads, nested gzip data, and HTTP responses. ChangesGzip decoder flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change makes gzip payload handling consistent across sync and Builder test reads, with targeted coverage for compressed, uncompressed, nested, empty, and non-seekable inputs; no actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The lock still resolved 7.28.1 while master and PyPI are on 7.28.2, so this branch was the only place holding the older build. 7.28.2 is a single fix — GzipParser detecting gzip payloads instead of trusting headers (airbytehq/airbyte-python-cdk#1124) — which this connector does not use, so the bump carries no behavioural change here. The manifest's declared version follows the pin. The requirement comment on the backoff caps still names 7.28.1, which is the floor that matters: that is the release shipping #1126, without which the wait cap costs token rotation. Suite: 237 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Requested by Syed Khadeer (Airbyte support) off Zendesk ticket 18622: a customer's Connector Builder Test read of a stream that downloads a gzipped CSV fails with
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1while a real sync of the same stream succeeds.0x1f 0x8bis the gzip magic — compressed bytes are reaching the UTF-8 CSV parser.Resolves https://github.com/airbytehq/oncall/issues/13362:
Cause:
create_gzip_decoderbuilds two different decoders depending on mode, and neither actually looks at the payload.The Builder branch assumed
requestshad already decompressedresponse.content, which only holds for transport-levelContent-Encoding: gzip. When the body is a gzip payload (e.g. an S3*.csv.gzdownload served asapplication/gziporbinary/octet-stream, as Apple App Store Connect analytics report segments are), nothing gunzips it in Builder mode. Measured onmainwith a singleGzipDecoder(CsvDecoder), against an undecoded raw stream (decode_content=False, matching howrequestsconstructsresponse.rawin production sync mode):Content-Type: application/gzipUnicodeDecodeError0x8bContent-Type: binary/octet-streamUnicodeDecodeError0x8bUnicodeDecodeError0x8bContent-Encoding: gzipContent-Encoding: gzip+Content-Type: application/gzipUnicodeDecodeError0x8b (only one layer stripped)UnicodeDecodeError0x8b(An earlier revision of this table claimed the
Content-Encoding: gzipsync path raisedBadGzipFile; that measurement was taken withdecode_content=True, which pre-decompressed the fixture body — the production raw path succeeded. Corrected per review.)That asymmetry is why manifests in the wild carry double-nested
GzipDecoder(GzipDecoder(CsvDecoder))workarounds (source-amazon-adsships one next to a# TODO Fix me) — and why the single- vs. double-nesting workarounds are mutually exclusive depending on which header the server returns. Previously reported in airbytehq/oncall#7739, #11173, #11809 and airbytehq/airbyte#56988.Fix, in two parts:
GzipParserself-detecting — the behavior its own docstring already promised ("If the data is not gzipped, reset the pointer and pass the data to the inner parser as is") but never implemented.GzipParser.parsereads the 2-byte header (looping, sinceresponse.raw.read(2)may short-read), gunzips when it is\x1f\x8b, and otherwise hands the inner parser the untouched stream. Because the stream may be non-seekable (response.rawin sync mode), the consumed header is restored by wrapping it in a private_PrefixedStream(io.RawIOBase) inside anio.BufferedReader, which keeps it usable by bothgzip.GzipFileandCsvParser'sTextIOWrapper.create_gzip_decoder, treating the HTTP transport layer and the gzip representation as separate layers — a response can validly carry bothContent-Encoding: gzipandContent-Type: application/gzip, in which case the sync raw stream holds two gzip layers:A single
GzipDecodernow works for every combination in the table above, in both modes (pass-through makes the nested parser safe when only one layer is present). Existing double-nested manifests keep working: the innerGzipParsersees already-decompressed data and passes it through instead of raisingBadGzipFile.Test plan
GzipParserunit tests inunit_tests/sources/declarative/decoders/test_composite_decoder.py: gzipped payload and non-gzip pass-through, each over both a seekable and a truly non-seekable stream (seek()/tell()raiseio.UnsupportedOperation), a stream that returns one byte perread(), empty payload, and nestedGzipParser(GzipParser(CsvParser))over a singly-gzipped body.create_gzip_decodertests inunit_tests/sources/declarative/parsers/test_model_to_component_factory.pycoveringemit_connector_builder_messagesTrueandFalseagainst each header shape above, withpreload_content=False, decode_content=Falsefixtures matching the production raw path — including the double-layerContent-Encoding+Content-Typeregression case. Builder mode had no coverage at all before.poetry run pytest unit_tests/sources/declarative/decoders/ unit_tests/sources/declarative/parsers/test_model_to_component_factory.py,poetry run ruff format --check . && poetry run ruff check .,poetry run mypy --config-file mypy.ini airbyte_cdk.Link to Devin session: https://app.devin.ai/sessions/3b5024f8f9bf4de88ec1abad6fb688fb
Important
Auto-merge enabled.
This PR is set to merge automatically when all requirements are met.