fix(low-code): classify text-decode failures in CompositeRawDecoder parsers - #1128
fix(low-code): classify text-decode failures in CompositeRawDecoder parsers#1128devin-ai-integration[bot] wants to merge 2 commits into
Conversation
Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
🤖 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/1787279627-decode-error-wrapping#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/1787279627-decode-error-wrappingPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
There was a problem hiding this comment.
Pull request overview
This pull request hardens the CDK’s declarative decoder parsers by consistently classifying text decoding failures (e.g., gzipped bytes reaching a text parser) into an AirbyteTracedException with a deterministic user-facing message and FailureType.system_error, instead of letting raw UnicodeDecodeError escape or being collapsed into a generic JSON parse failure.
Changes:
- Added a shared
_raise_decode_error(...)helper incomposite_raw_decoder.pyand used it in all byte-to-text decode sites across CSV/JSON/JSONL/JSON-items parsing. - Refactored
JsonParserto decode once up-front, ensuring decode errors are classified distinctly from JSON parse errors. - Added unit tests covering decode-failure classification for multiple parsers and pinned
JsonLineParser’s malformed-line skip behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py | Introduces a shared decode-error wrapper and applies it across all parsers that convert bytes to text; refactors JsonParser decode flow for correct error classification. |
| unit_tests/sources/declarative/decoders/test_composite_decoder.py | Adds regression tests ensuring decode failures raise AirbyteTracedException with FailureType.system_error and verifies JSONL malformed-line skipping remains unchanged. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Summary
When an endpoint returns bytes that aren't text in the parser's configured encoding (most commonly a still-gzipped body reaching a text parser), the declarative decoder parsers let the raw
UnicodeDecodeErrorescape. The user got a Python traceback —UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1— with noAirbyteTracedException, no user-facingmessage, and noFailureType.This adds one shared wrapper, modelled on the existing handling in
zipfile_decoder.py, and calls it from every place incomposite_raw_decoder.pythat turns bytes into text:Call sites:
CsvParser.parse(the error surfaces while iteratingcsv.DictReaderover theTextIOWrapper, so the iteration is what's guarded),JsonLineParser.parse(per line),_Utf8Recoder.read(theJsonItemsParsernon-UTF-8 path), andJsonParser.parse.JsonParseralso needed a small restructure: it previously decoded inside both_parse_orjsonand_parse_json, where a decode failure was swallowed intoOptional[Any] = Noneand collapsed into the generic "Response JSON data failed to be parsed"system_error. It now decodes once up front and passesstrto those two private helpers:The orjson-then-json fallback and the existing generic exception for genuinely malformed JSON are unchanged.
Why
system_errorand notconfig_errorconfig_errorwas the triage suggestion, on the grounds that the decoder configuration doesn't match what the endpoint returns and won't fix itself on retry. I went withsystem_errorinstead: the mismatch is between the connector's manifest and the payload, not between the user's config (credentials, parameters) and the payload. For any released connector the user cannot change the decoder, soconfig_errorwould attribute the failure to them and imply an action they can't take. This also matches how the neighbouring failure modes in this package are already classified (zipfile_decoder.py, andJsonParser's existing parse failure). Happy to flip it if maintainers read the ownership boundary differently.Behaviour notes
JsonLineParser'sjson.JSONDecodeErrorhandling is untouched: a validly-decodable but malformed line still logs a warning and is skipped. Only the previously-uncaughtUnicodeDecodeErrorpath changed, and it still fails the sync — just with a classified error. Nothing new is silently dropped; there is a new test pinning the skip behaviour.messagedeliberately omits the encoding value, byte offsets, and codec wording so it stays deterministic and usable as a log aggregation key; all of that detail lives ininternal_message.Declarative-First Evaluation
This change is in the CDK's parser layer, not in a connector manifest, so no declarative feature can deliver it.
RecordFilter,AddFields/RemoveFields, transformations,DatetimeBasedCursor,DefaultPaginator,SubstreamPartitionRouter, and$refoverrides all operate on records that have already been decoded and parsed — they never run when the byte-to-text step itself raises.HttpRequestererror handlers classify HTTP-level responses (status codes, response bodies) and do not see exceptions raised inside a decoder's parser. No custom Python component is added to any connector: the fix is entirely inside the shared CDK decoder that every declarative connector already uses, so connectors get the improved classification with no manifest change at all.Overlap with an in-flight PR
#1124 rewrites
GzipParser.parsein this same file to auto-detect whether the payload is actually gzipped — that's the root-cause fix for the originating incident, this one is the error-message hardening. I deliberately leftGzipParseralone, but both PRs insert a module-level helper just belowlogger = ..., so whichever lands second will likely need a trivial rebase in that one spot.Resolves https://github.com/airbytehq/oncall/issues/13363:
Requested via the
/ai-fixworkflow from AI Triage on that issue.Test plan
New tests in
unit_tests/sources/declarative/decoders/test_composite_decoder.py: a gzip payload fed directly toCsvParser,JsonParser,JsonLineParser, andJsonItemsParserassertsAirbyteTracedExceptionwithfailure_type == FailureType.system_error, the exactmessage, absence ofcodec/UnicodeDecodeError/0x8b/utf-8frommessage, and presence of the raw decode error plus the1f 8bsignature ininternal_message. Plus a test pinningJsonLineParser's malformed-line skip. All four decode tests fail onmain(rawUnicodeDecodeErrorfor CSV / JSON Lines / JSON Items, generic parse error for JSON).poetry run pytest unit_tests/sources/declarative/decoders/ -q— 66 passedpoetry run ruff format --check . && poetry run ruff check .— passedpoetry run mypy --config-file mypy.ini airbyte_cdk— passedLink to Devin session: https://app.devin.ai/sessions/9b4ca114294441bbad1c092e326b2b0e