Match If-None-Match per RFC 7232: honor '*', comma-separated lists, and weak comparison - #2517
januththedev wants to merge 2 commits into
Conversation
jeremyevans
left a comment
There was a problem hiding this comment.
Thank you for the patch. In general, this looks good, though I would remove some comments, and do some additional stripping.
| # | ||
| # The field value is either "*", which matches any current representation, or | ||
| # a comma separated list of entity-tags, any one of which may match. RFC 7232 | ||
| # Section 3.2 also requires the weak comparison function to be used, so a | ||
| # "W/" prefixed tag and a bare tag with the same opaque value are equivalent. |
There was a problem hiding this comment.
| # | |
| # The field value is either "*", which matches any current representation, or | |
| # a comma separated list of entity-tags, any one of which may match. RFC 7232 | |
| # Section 3.2 also requires the weak comparison function to be used, so a | |
| # "W/" prefixed tag and a bare tag with the same opaque value are equivalent. |
| # "*" matches as long as the server has a current representation for the | ||
| # target resource, which is guaranteed here since the status is 200. | ||
| return true if none_match.strip == '*' |
There was a problem hiding this comment.
| # "*" matches as long as the server has a current representation for the | |
| # target resource, which is guaranteed here since the status is 200. | |
| return true if none_match.strip == '*' | |
| none_match = none_match.strip | |
| return true if none_match == '*' |
| etag = headers[ETAG] | ||
| return false if etag.nil? | ||
|
|
||
| none_match.split(',').any? { |tag| weak_etag_match?(tag.strip, etag) } |
There was a problem hiding this comment.
| none_match.split(',').any? { |tag| weak_etag_match?(tag.strip, etag) } | |
| etag = etag.strip | |
| none_match.split(',').any? do |tag| | |
| tag.strip! | |
| weak_etag_match?(tag, etag) | |
| end |
|
Thanks — pushed def etag_matches?(none_match, headers)
# "*" matches as long as the server has a current representation for the
# target resource, which is guaranteed here since the status is 200.
none_match = none_match.strip
return true if none_match == '*'
etag = headers[ETAG]
return false if etag.nil?
etag = etag.strip
none_match.split(',').any? do |tag|
tag.strip!
weak_etag_match?(tag, etag)
end
endI also dropped the block comment above the method and the one on One thing worth checking on your side, since I can't: I also applied the same reasoning to the response I could not run the test suite. Ruby isn't installed on my machine, so this revision is unverified locally. My six added examples in |
5465f53 to
3b743fa
Compare
If-None-Matchis compared byte-for-byte, ignoring*, lists, and weak comparisonDescription
Rack::ConditionalGet#etag_matches?compares the wholeIf-None-Matchfield value against the responseETagwith plainString#==:Why that's wrong — three RFC 7232 §3.2 violations
If-None-Match: *is never honoured.*is a legal field value meaning "matches any current representation". Rack evaluates"W/\"abc\"" == "*"→false, so it returns200with the full body instead of304. Every HTTP cache revalidating a resource for which it stored no validator hits this. SinceConditionalGetonly reaches this code whenstatus == 200(conditional_get.rb:33), a current representation is guaranteed to exist, so*is unconditionally a match.If-None-Matchis a list. The grammar is#entity-tagor a comma-separated list. ForIf-None-Match: "4321", W/"1234"againstETag: W/"1234", Rack evaluates"W/\"1234\"" == "\"4321\", W/\"1234\""→false. Only an accidental exact whole-header match ever yields a 304.Strong comparison is used where weak is required. RFC 7232 §3.2: "A recipient MUST use the weak comparison function when comparing entity-tags for If-None-Match." Rack compares strings byte-for-byte, so
If-None-Match: W/"abc"does not matchETag: "abc".The in-code comment at line 53 already says "per RFC 7232", and
CHANGELOG.md:774records that maintainers treat this file as RFC-7232-conformant, so the current behaviour contradicts the project's own stated intent.The fix
Tests
Six new
itblocks intest/spec_conditional_get.rb:*with an ETag,*with no ETag, an entity-tag inside a comma-separated list, weak comparison, a negative case where no list member matches (→ 200), and a negative case with no response ETag and not*(→ 200).I also checked the four pre-existing
If-None-Matchspecs still hold under the new logic:'1234'vsETag: "1234"is weak-equal → 304, and'4321'vsETag: "1234"→ 200. Please confirm this when the suite is run — it is the main compatibility risk in the change.Upstream status
gh search prs/issues "conditional get" --repo rack/rack --state openreturns 0 results, and the 50 open issues and 26 open PRs contain nothing for conditional GET.Two real neighbouring defects I left alone:
Rack::MediaType.paramsraisingNoMethodErroron"text/plain;;charset=utf8"is already open upstream as PR #2508, andtag_multipart_encodingcomparingTEXT_PLAIN == type_subtypecase-sensitively is adjacent to open PR #2514. I also noted thatRack::Utils.q_valuesreturns[nil, 1.0]for an emptyAcceptelement, which makesRack::ShowExceptions#accepts_html?— the 500 path — crash on an attacker-controlledAcceptheader; that is a genuine robustness bug but a separate change.