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

How to set up and validate locally

  1. Check out master and start a console with bin/rails console.

  2. 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
  3. Confirm the output. e.message.valid_encoding? prints false. Gitlab::Json.dump raises EncodingError: Invalid Unicode [8b a4 70 6c 61] at 94.

  4. Check out this branch, restart the console, and run the same snippet.

  5. Confirm the output. e.class prints the CorruptBlobError class. e.cause.class prints JSON::ParserError. e.message.valid_encoding? prints true. Gitlab::Json.dump prints {"exception.message":"Failed to decode checkpoint blob"}.

  6. 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.

Closes #629535 (closed)

Merge request reports

Loading
Loading