Keep raw blob bytes out of checkpoint decode error message
What does this MR do and why?
Gitlab::DuoWorkflow::ChannelValuesReconstructor.decode rescues Zlib::Error and JSON::ParserError and re-raises them as CorruptBlobError. Before this change, it built the message as "Failed to decode checkpoint blob: #{e.message}". Now it raises with the fixed message Failed to decode checkpoint blob. The raise still happens inside the rescue, so Ruby attaches the original error as cause. exception.cause_class (for example JSON::ParserError or Zlib::DataError) is still logged.
Why the message was a problem
GitLab runs Oj in rails mode. When parsing fails in that mode, Oj appends the whole input to the error message (... in '<raw input>'). It also force-tags the string as UTF-8. A checkpoint blob can inflate to bytes that are not JSON. One example is msgpack, which an AI Gateway from before the switch to JSON blobs sends. Those raw bytes then end up inside the CorruptBlobError message. The message is not valid UTF-8 and can be megabytes long.
Effect before the fix
Ai::DuoWorkflows::CreateCheckpointService#start_channel_from_blobs rescues CorruptBlobError on purpose and calls Gitlab::ErrorTracking.track_exception. A bad __start__ blob should only cost the goal backfill. But logging that exception calls Gitlab::Json.dump on the payload. That call raises EncodingError: Invalid Unicode [...] at 94 because of the raw bytes in the message. Nothing rescues this second exception, so the checkpoint write returns 500. The same failure happens wherever a CorruptBlobError is logged, including the GraphQL query log.
Effect after the fix
The message is plain ASCII. Logging succeeds. The write path tolerates the bad blob as intended and returns success. Reads that hit an undecodable blob still raise CorruptBlobError. A corrupt blob must fail the read rather than return inconsistent values. Only the message content changes.
Spec
ee/spec/lib/gitlab/duo_workflow/channel_values_reconstructor_spec.rb gets a new example. A blob whose data inflates to msgpack bytes raises CorruptBlobError with the fixed message and a JSON::ParserError cause.
Reproduction
With Zlib::Deflate.deflate("\x8b\xa4plan".b) as blob data:
# before
Gitlab::Json.dump('exception.message' => error.message)
# => EncodingError: Invalid Unicode [8b a4 70 6c 61] at 94
# after
Gitlab::Json.dump('exception.message' => error.message)
# => "{\"exception.message\":\"Failed to decode checkpoint blob\"}"Offset 94 is 34 bytes of Failed to decode checkpoint blob: plus 60 bytes of Oj's Empty input (after ) at line 1, column 1 [parse.c:1266] in '.
References
- Fixes issue 629535.
- Found through ai-assist merge request 6915. Its downstream
feature-testspipeline hit the 500.
How to set up and validate locally
-
Check out
masterand start a console withbin/rails console. -
Run this snippet:
blob = Zlib::Deflate.deflate("\x8b\xa4plan".b) begin Gitlab::DuoWorkflow::ChannelValuesReconstructor.decode(blob) rescue StandardError => e puts e.class puts e.cause.class puts e.message.valid_encoding? puts Gitlab::Json.dump('exception.message' => e.message) end -
Confirm the output.
e.message.valid_encoding?printsfalse.Gitlab::Json.dumpraisesEncodingError: Invalid Unicode [8b a4 70 6c 61] at 94. -
Check out this branch, restart the console, and run the same snippet.
-
Confirm the output.
e.classprints theCorruptBlobErrorclass.e.cause.classprintsJSON::ParserError.e.message.valid_encoding?printstrue.Gitlab::Json.dumpprints{"exception.message":"Failed to decode checkpoint blob"}. -
Run the spec:
bundle exec rspec ee/spec/lib/gitlab/duo_workflow/channel_values_reconstructor_spec.rb.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
- I have evaluated the MR acceptance checklist for this MR.
Closes #629535 (closed)