Summary
The PyPI metadata APIs (Simple API and JSON Metadata API) need conditional request support for Akamai Centralized Authorization, which requires If-Modified-Since / Last-Modified headers. This also enables client and edge cache efficiency.
Primary driver: Akamai Centralized Authorization requires If-Modified-Since for cache revalidation while Pulp retains authorization control.
Background: Akamai Centralized Authorization
Akamai Centralized Authorization allows edge caches to cache content while Pulp performs authorization on every request:
Cache hit flow:
- Client sends GET with credentials to Akamai
- Akamai sends
If-Modified-Since conditional request to Pulp (with client's auth headers)
- Pulp runs ContentGuard authorization
- If authorized and content unchanged: Pulp returns
304 Not Modified (no body) → Akamai serves cached content
- If authorized and content changed: Pulp returns
200 with new content → Akamai updates cache
- If not authorized: Pulp returns
403 → Akamai blocks request
Key requirement: Akamai sends If-Modified-Since for revalidation, so Pulp must support Last-Modified + If-Modified-Since (not just ETag).
Current State
Simple API (/simple/{package}/)
- ✅ Has
ETag and If-None-Match → 304 support
- ✅ Has
Cache-Control: max-age=600, public
- ❌ Missing
Last-Modified header
- ❌ Missing
If-Modified-Since → 304 handling
JSON Metadata API (/pypi/{package}/json/)
- ❌ No
ETag
- ❌ No
Last-Modified
- ❌ No
Cache-Control
- ❌ No conditional request handling
Proposed Implementation
Both SimpleView and MetadataView need:
-
Last-Modified header on all 200 responses
- Use the same repo-version-based timestamp as the current ETag function
- Value: timestamp when the repository version was created
-
If-Modified-Since conditional request handling:
a. Run ContentGuard authorization (always, on every request)
b. If not authorized → return 403 (stop here)
c. If authorized and If-Modified-Since present:
- If content not modified since that datetime → return 304 (no body)
- If content modified or header invalid → return 200 with content
d. If no If-Modified-Since header → return 200 with content
-
Cache-Control headers:
- Simple index pages:
public, max-age=300 (5 minutes - mutable metadata)
- JSON metadata:
public, max-age=900 (15 minutes - matches pypi.org)
-
ETag support (JSON Metadata API only, for pypi.org parity):
- Add
ETag header and If-None-Match → 304 handling
- Use the same
_etag_func already implemented for SimpleView
Security Requirement
Authorization MUST be checked on every request, including conditional requests.
Order of operations:
- Run ContentGuard authorization using client credentials from request headers
- If not authorized → return 403 Forbidden (stop here)
- If authorized → check
If-Modified-Since or If-None-Match
- Return 304 or 200 based on conditional check
This ensures unauthorized clients cannot:
- Determine if content exists
- Learn modification timestamps
- Bypass authorization checks
pypi.org Behavior Comparison
For reference, pypi.org (Warehouse) uses ETag exclusively:
| Endpoint |
ETag |
Last-Modified |
If-None-Match→304 |
Cache-Control |
/pypi/{pkg}/json |
✅ |
❌ never sent |
✅ |
max-age=900, public |
/simple/{pkg}/ |
✅ |
❌ never sent |
✅ |
max-age=600, public |
Warehouse doesn't send Last-Modified because it doesn't need edge-cache revalidation (Fastly handles it differently). Pulp needs both validators:
Last-Modified / If-Modified-Since for Akamai Centralized Authorization
ETag / If-None-Match for pypi.org parity (optional enhancement)
Implementation Notes
- Both views are Django views served by pulp-api, NOT the content app
- They will NOT inherit
If-Modified-Since support from pulpcore #7929 (which applies to content app binary downloads only)
- Reuse the existing
_etag_func from pulp_python/app/pypi/views.py
- Use Django's
@condition decorator (supports both etag_func and last_modified_func)
Testing
Add functional tests mirroring test_simple_cache_etag_conditional_request:
For SimpleView:
- Test
If-Modified-Since with matching timestamp → 304
- Test
If-Modified-Since with old timestamp → 200 with content
- Test no
If-Modified-Since header → 200 with content
- Test
Last-Modified header present on 200 responses
For MetadataView:
- Same tests as SimpleView
- Test
If-None-Match with matching ETag → 304 (optional)
- Test
ETag header present on 200 responses (optional)
Related Issues
- pulpcore #7929: Adds
If-Modified-Since to content app for binary package downloads
- PULP-2262 (Jira): If-Modified-Since support for Akamai centralized authorization
- PULP-2125 (Jira): Epic for edge-cached content delivery
Files
pulp_python/app/pypi/views.py - SimpleView, MetadataView
pulp_python/tests/functional/api/test_simple_cache.py - conditional request tests
Summary
The PyPI metadata APIs (Simple API and JSON Metadata API) need conditional request support for Akamai Centralized Authorization, which requires
If-Modified-Since/Last-Modifiedheaders. This also enables client and edge cache efficiency.Primary driver: Akamai Centralized Authorization requires
If-Modified-Sincefor cache revalidation while Pulp retains authorization control.Background: Akamai Centralized Authorization
Akamai Centralized Authorization allows edge caches to cache content while Pulp performs authorization on every request:
Cache hit flow:
If-Modified-Sinceconditional request to Pulp (with client's auth headers)304 Not Modified(no body) → Akamai serves cached content200with new content → Akamai updates cache403→ Akamai blocks requestKey requirement: Akamai sends
If-Modified-Sincefor revalidation, so Pulp must supportLast-Modified+If-Modified-Since(not just ETag).Current State
Simple API (
/simple/{package}/)ETagandIf-None-Match→ 304 supportCache-Control: max-age=600, publicLast-ModifiedheaderIf-Modified-Since→ 304 handlingJSON Metadata API (
/pypi/{package}/json/)ETagLast-ModifiedCache-ControlProposed Implementation
Both
SimpleViewandMetadataViewneed:Last-Modifiedheader on all 200 responsesIf-Modified-Sinceconditional request handling:Cache-Controlheaders:public, max-age=300(5 minutes - mutable metadata)public, max-age=900(15 minutes - matches pypi.org)ETag support (JSON Metadata API only, for pypi.org parity):
ETagheader andIf-None-Match→ 304 handling_etag_funcalready implemented for SimpleViewSecurity Requirement
Authorization MUST be checked on every request, including conditional requests.
Order of operations:
If-Modified-SinceorIf-None-MatchThis ensures unauthorized clients cannot:
pypi.org Behavior Comparison
For reference, pypi.org (Warehouse) uses
ETagexclusively:ETagLast-ModifiedIf-None-Match→304Cache-Control/pypi/{pkg}/jsonmax-age=900, public/simple/{pkg}/max-age=600, publicWarehouse doesn't send
Last-Modifiedbecause it doesn't need edge-cache revalidation (Fastly handles it differently). Pulp needs both validators:Last-Modified/If-Modified-Sincefor Akamai Centralized AuthorizationETag/If-None-Matchfor pypi.org parity (optional enhancement)Implementation Notes
If-Modified-Sincesupport from pulpcore #7929 (which applies to content app binary downloads only)_etag_funcfrompulp_python/app/pypi/views.py@conditiondecorator (supports bothetag_funcandlast_modified_func)Testing
Add functional tests mirroring
test_simple_cache_etag_conditional_request:For SimpleView:
If-Modified-Sincewith matching timestamp → 304If-Modified-Sincewith old timestamp → 200 with contentIf-Modified-Sinceheader → 200 with contentLast-Modifiedheader present on 200 responsesFor MetadataView:
If-None-Matchwith matching ETag → 304 (optional)ETagheader present on 200 responses (optional)Related Issues
If-Modified-Sinceto content app for binary package downloadsFiles
pulp_python/app/pypi/views.py-SimpleView,MetadataViewpulp_python/tests/functional/api/test_simple_cache.py- conditional request tests