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-revalidateThat 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/privateCache-Control(withs-maxage,stale-while-revalidate,stale-if-error), a strongETag, and returns304 Not Modifiedfor matching conditional requests. - The
ETagvaries byinclude_lfs_blobsandexclude_paths, since those parameters change the archive contents. - The
publicflag is derived from the anonymous:download_codeability, so private and internal repositories stayprivateand are never stored by shared caches. - The behavior is gated behind the
api_repository_archive_cache_headersfeature 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:
- 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" - Observe
Cache-Control: max-age=0, private, must-revalidatein the response. - Make the project public and repeat step 1. Observe the
Cache-Controlheader is unchanged.
With the feature flag enabled
- In
bin/rails console, runFeature.enable(:api_repository_archive_cache_headers), then wait ~1 minute. - 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" - Observe
Cache-Control: max-age=60, private, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60. - Make the project public.
- 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" - 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,
304handling, ETag variation byinclude_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.