fix: return 200 from HEAD on the proxy root index - #25
Merged
Conversation
HEAD / returned 404 NoSuchBucket with an empty <BucketName> while GET / returned the 200 index page, because head_object treated an empty target name as a missing bucket rather than the root index that target_dispatcher serves. Mirror target_dispatcher's root condition (including the virtual 'www' host) and reply 200 with the media type Accept would have negotiated, plus Vary: Accept so shared caches keep the HTML and XML variants apart. Also add the leading slash to the route path so it matches the GET route. get_target already strips it, so that part is behavior-neutral.
Member
|
Nice catch, thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
HEAD /returns404 NoSuchBucketwhileGET /returns the200index page for the same URL:Reproduces on HTTP/1.1, with browser
User-Agent/Acceptheaders, and directly against the backend, so it is the app and not nginx or HTTP/2.The 404 body is a
NoSuchBucketerror with an empty bucket name. Byte math confirms it — a bucket name of 30 characters yields 204 bytes, and204 - 30 = 174:Root cause
target_dispatcher(GET) special-cases an empty target name as "serve the proxy index":head_objecthas no such case, so the same empty target name falls into the missing-bucket path:The two handlers therefore disagree about whether
/exists. RFC 9110 §9.3.2 requires HEAD to return the same status and header fields GET would have sent.Fix
Mirror
target_dispatcher's root condition inhead_object, including the virtualwwwhost, and reply200with the media typeAcceptwould have negotiated —_prefers_htmlis reused so HEAD and GET cannot drift apart.Vary: Acceptis set for the same reason as #23 / #24:/serves two representations, and nginx'sproxy_cachestores HEAD responses too, so the variants must be kept apart. That PR covers the GET fork points; HEAD is outside its scope, and the two do not overlap textually.The route path also gains its missing leading slash so it matches the GET route.
get_targetalready doesremoveprefix('/'), so that part is behavior-neutral.Testing
test_head_root_indexasserts the status, the negotiated content type, andVaryfor bothAccepttypes, and compares HEAD's status and content type against GET's on the same URL. Verified it fails without the app change (assert 404 == 200) and passes with it.Full suite: 71 passed.
Out of scope
HEAD /{bucket}/returnscontent-type: application/xmlunconditionally, even thoughGET /{bucket}/serves HTML to browsers. Pre-existing and untested, so left alone here — happy to follow up if you want HEAD to negotiate there too.🤖 Generated with Claude Code