Set cache headers on REST API repository archive endpoint

What does this MR do and why?

The web archive controller (Projects::RepositoriesController#archive) emits tuned Cache-Control and a strong ETag so repository archives are edge-cacheable, but the REST API archive endpoint (GET /api/v4/projects/:id/repository/archive) set none of its own and fell back to Rails' framework default:

cache-control: max-age=0, private, must-revalidate

That made API-fetched archives non-cacheable by shared caches/CDNs and always reach the origin, even for public projects (contributing to high CDN BYPASS rates).

This MR makes the API endpoint emit the same caching semantics as the web UI:

  • Extracts the shared caching logic into Gitlab::Repositories::ArchiveCacheControl, used by both the web controller and the Grape API so they stay consistent.
  • The API now emits the same public/private Cache-Control (with s-maxage, stale-while-revalidate, stale-if-error), a strong ETag, and returns 304 Not Modified for matching conditional requests.
  • The ETag varies by include_lfs_blobs and exclude_paths, since those parameters change the archive contents.
  • The public flag is derived from the anonymous :download_code ability, so private and internal repositories stay private and are never stored by shared caches.
  • The behavior is gated behind the api_repository_archive_cache_headers feature flag (gitlab_com_derisk, default off) for a safe rollout.

Security consideration

The shared-cacheable (public) decision mirrors the existing web controller exactly and is keyed to the anonymous download-code ability, not current_user. Private and internal repositories remain private, so shared caches never store them.

How to test locally

Substitute your GDK host and a project ID in the examples below.

With the feature flag off (default)

Start with a private project:

  1. Request the archive:
    curl --header "PRIVATE-TOKEN: <your_token>" -v -o /dev/null \
      "https://gdk.test:3443/api/v4/projects/<project_id>/repository/archive.zip"
  2. Observe Cache-Control: max-age=0, private, must-revalidate in the response.
  3. Make the project public and repeat step 1. Observe the Cache-Control header is unchanged.

With the feature flag enabled

  1. In bin/rails console, run Feature.enable(:api_repository_archive_cache_headers), then wait ~1 minute.
  2. Request the archive for the private project:
    curl --header "PRIVATE-TOKEN: <your_token>" -v -o /dev/null \
      "https://gdk.test:3443/api/v4/projects/<project_id>/repository/archive.zip"
  3. Observe Cache-Control: max-age=60, private, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60.
  4. Make the project public.
  5. Request the archive again (no token needed for a public project):
    curl -v -o /dev/null \
      "https://gdk.test:3443/api/v4/projects/<project_id>/repository/archive.zip"
  6. Observe Cache-Control: max-age=60, public, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60.

Test coverage

  • New unit spec: spec/lib/gitlab/repositories/archive_cache_control_spec.rb.
  • API request specs for the caching headers, 304 handling, ETag variation by include_lfs_blobs/exclude_paths, and the flag-off fallback: spec/requests/api/repositories_spec.rb.
  • Existing web controller caching specs still pass after the refactor.

References

Edited by Stan Hu

Merge request reports

Loading