fix: read small object-storage artifact archives in get_artifact_file

What does this MR do and why?

On GitLab.com, reading any entry from a CI artifacts archive smaller than about 64 KB and stored in object storage fails with "Tool execution failed: new position is outside of file" in the get_artifact_file MCP tool. Security report artifacts (SAST, Secret Detection, Container Scanning) are typically a few KB, so agents could not read them. Reported with detailed measurements in #631136 (closed) (fails up to 35,329 bytes, works from 68,875 bytes).

rubyzip locates the end-of-central-directory record by seeking 64 KB back from the end of the archive (Zip::CentralDirectory#start_buf) and rescuing the Errno::EINVAL that a plain File raises when that lands before offset zero. Gitlab::HttpIO#seek raises a bare RuntimeError ("new position is outside of file") instead, which rubyzip does not rescue. Local-storage archives hand rubyzip a plain File and were never affected by this bug; only object storage goes through HttpIO via the Mcp::Tools::RangedZipIo adapter.

The fix has two parts:

  1. Mcp::Tools::RangedZipIo#seek now translates the RuntimeError into Errno::EINVAL, matching File semantics, so rubyzip's scan falls back to reading from offset zero exactly as it does for a small file on disk.
  2. The rescue in GetArtifactFileService#read_from_archive now also catches Errno::EINVAL, so a residual out-of-range seek from a corrupt central directory offset degrades to the existing "Could not read the artifacts archive… Download it from … instead." error rather than a generic failure.

How to verify locally

Run the two spec files added for this fix:

bundle exec rspec spec/services/mcp/tools/ranged_zip_io_spec.rb spec/services/mcp/tools/jobs/get_artifact_file_service_spec.rb

The service spec reads an entry from a sub-window-size archive in object storage through ranged requests, and fails with "new position is outside of file" without the fix. The RangedZipIo#seek spec adds two examples covering delegation and the RuntimeError-to-Errno::EINVAL translation.

This was also verified end to end on GDK: a 190-byte archive stored in MinIO (artifact file_store=2, confirming the read path goes through Gitlab::HttpIO), read through the real /api/v4/mcp endpoint via nginx with a tools/call of get_artifact_file, returns the entry content with isError: false.

References

#631136 (closed) !255762 (merged)

Closes #631136 (closed)

🤖 Generated with Claude Code

Merge request reports

Loading
Loading