Skip to content

feat: let the content app return 304 Not Modified for unchanged files - #7995

Open
carlosthe19916 wants to merge 4 commits into
pulp:mainfrom
carlosthe19916:add-if-modified-since-content-app
Open

carlosthe19916 wants to merge 4 commits into
pulp:mainfrom
carlosthe19916:add-if-modified-since-content-app

Conversation

@carlosthe19916

@carlosthe19916 carlosthe19916 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • After content guards allow the request, the content app can answer If-Modified-Since with 304 Not Modified and no body, so an edge cache can keep its copy instead of downloading the file again.
  • Last-Modified is when that unit joined the served repository version (RepositoryContent.pulp_created). Disk mtime moves on copy/restore, and the version’s created time would make every cached file look stale on each publish.
  • This only applies when Pulp sends the bytes (filesystem or ArtifactResponse). Object-storage 302s get neither Last-Modified nor Cache-Control: public, because a signed URL must not be stored or reused as if it were the file.

Fixes #7929.

Guards still run first. A matching If-Modified-Since is 304; otherwise the response is 200 with Last-Modified and the body. If there is no membership row (publish-generated metadata, remote-only distribution), Pulp omits Last-Modified and never returns 304.

Artifact 200s and 304s include Cache-Control: public, max-age=0, must-revalidate so the edge revalidates on every use. Plugins can still override headers with content_headers_for. This change does not add ETag.

_serve_ca is the single place that looks up the timestamp and may 304 (published files, pass-through, repository versions, plugin content_handler(), grace-period fallback, pull-through). Grace-period fallback uses the old publication’s version so the timestamp is not the current one. On-demand streams 304 before opening the remote. A stream that has already started writing is never turned into a 304.

aiohttp’s FileResponse would overwrite Last-Modified with disk mtime. PulpFileResponse stops that, does not send a disk-time ETag, and does not clear If-Range.

When Redis is on, the handler returns a cacheable 200 and the cache layer owns the 304: it stores last_modified, 304s a hit without rebuilding the response, and never writes a 304 into Redis.

Not in this PR: 304 on S3/Azure/GCS redirects, ETag / If-None-Match, or a fallback timestamp for metadata with no membership row.

Test plan

  • Handler: 200 vs 304; no handler 304 when Redis is on; 302s stay unmodified; 304 beats an unsatisfiable Range; on-demand 304 before the remote fetch; timestamp from repository membership
  • PulpFileResponse does not 304 on file mtime and does not clear If-Range
  • Redis stores last_modified, 304s a hit without rebuilding, and never caches a 304

@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch 3 times, most recently from 2246d02 to 95c8958 Compare August 20, 2026 14:05
@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch 3 times, most recently from 2e9b68b to a193f54 Compare August 21, 2026 08:44
@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch from a193f54 to 46bc167 Compare August 21, 2026 09:20
@carlosthe19916
carlosthe19916 marked this pull request as ready for review August 21, 2026 09:22
@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch from d1101e2 to 6a6872c Compare August 25, 2026 11:02
@gerrod3

gerrod3 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

I really don't like what the AI wrote. Here's how I would have done it: 5164e37

The AI is somehow doing too much and too little at the same time. Lots of refactoring and not even implementing full functionality for the cloud backends. And man, AI really does love adding tests... After I spent so much effort to reduce the test suite time too.

Can you implement something similar to what I wrote? I haven't fully tested it and the utility method isn't all there, but it's a better approach, imo. Also, can you do some tests with the queries I wrote for finding the Last-Modified time vs the ones you used? I recall _content_relationships being a big bottleneck on services, so not sure it's a good idea to call it for every request again, but maybe the content=ca.content filter speeds it up.

Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch from 74965fa to c1c6694 Compare September 16, 2026 09:49
@carlosthe19916
carlosthe19916 marked this pull request as ready for review September 16, 2026 09:58
Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
@carlosthe19916
carlosthe19916 force-pushed the add-if-modified-since-content-app branch from 059b327 to 4b8d232 Compare September 16, 2026 10:39
Comment thread pulpcore/app/util.py Outdated

def check_request_was_modified(request, last_modified, etag=None):
if_none_match = request.headers.get("If-None-Match")
if if_none_match is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if if_none_match is not None:
if if_none_match:

Comment thread pulpcore/app/util.py Outdated
Comment on lines +721 to +723
for client_etag in if_none_match.split(","):
if client_etag.strip() == etag:
return False

@decko decko Sep 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for client_etag in if_none_match.split(","):
if client_etag.strip() == etag:
return False
client_etags = [etag == client_etag.strip() for client_etag in if_none_match.split(",")]
return not any(client_etags)

Comment thread pulpcore/app/util.py Outdated
Comment on lines +726 to +731
if not last_modified:
return True

if_modified_since = request.headers.get("If-Modified-Since")
if not if_modified_since:
return True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since those blocks are just simple checks, I believe they should be at the top of the function. It would avoid the if_none_match loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add If-Modified-Since / 304 Not Modified support to the content app

3 participants