Problem
The content app always returns a full response (200 with body or 302 redirect) regardless of whether the content has changed since the client last requested it. This prevents edge caches (Akamai, nginx) from using conditional requests to revalidate cached content — every request results in a full download or redirect, even for immutable artifacts that never change.
Proposed Solution
Add support for If-Modified-Since conditional requests and Last-Modified response headers to the content handler. When a client sends If-Modified-Since and the requested content has not changed, Pulp should return 304 Not Modified with no body.
The Last-Modified value should be set to RepositoryContent.pulp_created — the timestamp of when the specific content unit was added to the repository being served. This is more accurate than using the repository version creation time, since some repositories get new versions frequently. pulp_python's Simple API already uses this same approach for its upload_time field.
Implementation
Both the non-cached and Redis-cached content serving paths need to handle this.
Non-cached path (_match_and_stream)
After ContentGuard authorization passes and the content artifact is resolved, but before building the response:
- Look up
RepositoryContent.pulp_created for the content being served
- Set
Last-Modified header on all 200 responses
- If the request includes
If-Modified-Since and the content has not been modified since that datetime, return 304 Not Modified (no body)
Redis-cached path (AsyncContentCache)
- Include a
last_modified timestamp in the cached entry JSON (alongside headers, status, expires, type)
- On cache hit, after ContentGuard authorization passes, check
If-Modified-Since against the stored last_modified timestamp
- If not modified, return 304 instead of reconstructing the full cached response
- If modified or no
If-Modified-Since header, return the cached response as normal with Last-Modified header set
Response behavior summary
| Condition |
Response |
| Not authorized |
403 Forbidden |
Authorized, no If-Modified-Since |
200 with body + Last-Modified header |
Authorized, content changed since If-Modified-Since |
200 with body + Last-Modified header |
Authorized, content not changed since If-Modified-Since |
304 Not Modified (no body) |
Use Case
This enables edge caching architectures where a CDN (e.g., Akamai with Centralized Authorization) or a reverse proxy (e.g., nginx with proxy_cache_revalidate) caches content at the edge and uses lightweight conditional requests to Pulp for revalidation. Pulp still performs authorization on every request, but avoids transferring the full binary when the cached copy is still valid.
Related Code
pulpcore/content/handler.py — Handler.stream_content(), _match_and_stream(), _build_response_from_content_artifact()
pulpcore/cache/cache.py — AsyncContentCache, make_response(), make_entry()
pulpcore/responses.py — ArtifactResponse
Problem
The content app always returns a full response (200 with body or 302 redirect) regardless of whether the content has changed since the client last requested it. This prevents edge caches (Akamai, nginx) from using conditional requests to revalidate cached content — every request results in a full download or redirect, even for immutable artifacts that never change.
Proposed Solution
Add support for
If-Modified-Sinceconditional requests andLast-Modifiedresponse headers to the content handler. When a client sendsIf-Modified-Sinceand the requested content has not changed, Pulp should return304 Not Modifiedwith no body.The
Last-Modifiedvalue should be set toRepositoryContent.pulp_created— the timestamp of when the specific content unit was added to the repository being served. This is more accurate than using the repository version creation time, since some repositories get new versions frequently. pulp_python's Simple API already uses this same approach for itsupload_timefield.Implementation
Both the non-cached and Redis-cached content serving paths need to handle this.
Non-cached path (
_match_and_stream)After ContentGuard authorization passes and the content artifact is resolved, but before building the response:
RepositoryContent.pulp_createdfor the content being servedLast-Modifiedheader on all 200 responsesIf-Modified-Sinceand the content has not been modified since that datetime, return304 Not Modified(no body)Redis-cached path (
AsyncContentCache)last_modifiedtimestamp in the cached entry JSON (alongsideheaders,status,expires,type)If-Modified-Sinceagainst the storedlast_modifiedtimestampIf-Modified-Sinceheader, return the cached response as normal withLast-Modifiedheader setResponse behavior summary
If-Modified-SinceLast-ModifiedheaderIf-Modified-SinceLast-ModifiedheaderIf-Modified-SinceUse Case
This enables edge caching architectures where a CDN (e.g., Akamai with Centralized Authorization) or a reverse proxy (e.g., nginx with
proxy_cache_revalidate) caches content at the edge and uses lightweight conditional requests to Pulp for revalidation. Pulp still performs authorization on every request, but avoids transferring the full binary when the cached copy is still valid.Related Code
pulpcore/content/handler.py—Handler.stream_content(),_match_and_stream(),_build_response_from_content_artifact()pulpcore/cache/cache.py—AsyncContentCache,make_response(),make_entry()pulpcore/responses.py—ArtifactResponse