Skip to content

fix: do not let wildcards select values excluded with q=0 in Negotiate - #10661

Open
wleeaf wants to merge 1 commit into
codeigniter4:developfrom
wleeaf:negotiate-wildcard-excluded-values
Open

wleeaf wants to merge 1 commit into
codeigniter4:developfrom
wleeaf:negotiate-wildcard-excluded-values

Conversation

@wleeaf

@wleeaf wleeaf commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Description
When the header holds a wildcard, Negotiate returns the first supported value, even if the same header excludes that value with q=0. In RFC 9110, a weight of 0 means "not acceptable" (section 12.4.2), and a more specific entry overrides a wildcard (sections 12.5.1 and 12.5.3). Examples:

  • Accept-Encoding: gzip;q=0, * with encoding(['gzip', 'br']) returns gzip.
  • Accept: application/json;q=0, */* with media(['application/json', 'text/html'], true) returns application/json.
  • Accept: text/*, text/html;q=0 with media(['text/html', 'text/plain'], true) returns text/html.
  • charset() behaves the same way, and so does language(), with and without $strictLocaleNegotiation. For example, Accept-Language: en;q=0, * gives en.

getBestMatch() and getBestLocaleMatch() now skip values that the header excludes. A private isExcluded() applies the most specific value in the header that matches a supported value (RFC 9110, section 12.5.1): media ranges match by type, subtype and their own parameters, so text/*;q=0 excludes text/html and text/html;q=0 excludes text/html;charset=utf-8, and language ranges use RFC 4647 basic filtering, so en;q=0 excludes en-US. A more specific range with a positive weight still wins: text/html, text/*;q=0 selects text/html, and en;q=0, en-GB;q=0.5, * selects en-GB. Wildcards, broader matches and the strict locale fallbacks all skip excluded values. When nothing matches, $strictMatch = false still falls back to the first supported value, as documented and as testBestMatchOnlyLowQuality expects.

testWildcardsSkipExcludedValues, testMediaRangesExcludeMatchingValues, testMoreSpecificRangesTakePrecedence and testLanguageRangesExcludeRegionalLocales in tests/system/HTTP/NegotiateTest.php cover media(), charset(), encoding() and language() in both locale modes, with positive controls. They fail without the change.

tests/system/HTTP/ passes. php-cs-fixer, PHPStan and Rector (dry run) report nothing for the changed files.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@carson-codeigniter4 carson-codeigniter4 Bot added the bug Verified issues on the current code behavior or pull requests that will fix them label Oct 25, 2026

@michalsn michalsn left a comment

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.

Comment thread system/HTTP/Negotiate.php Outdated
Comment thread system/HTTP/Negotiate.php
@wleeaf
wleeaf force-pushed the negotiate-wildcard-excluded-values branch 2 times, most recently from 04f1211 to 78e6fb1 Compare October 26, 2026 11:48
@wleeaf

wleeaf commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto develop. The description is updated for the wider scope.

Comment thread system/HTTP/Negotiate.php Outdated
Comment thread system/HTTP/Negotiate.php Outdated
@wleeaf
wleeaf force-pushed the negotiate-wildcard-excluded-values branch 2 times, most recently from d2dba59 to ab58f75 Compare October 26, 2026 23:35
@wleeaf

wleeaf commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto develop again.

@wleeaf
wleeaf force-pushed the negotiate-wildcard-excluded-values branch from ab58f75 to 045a0d4 Compare October 27, 2026 09:00
A wildcard or a broader range could select a supported value even when
the header excluded that value with q=0. For example,
`Accept-Encoding: gzip;q=0, *` selected gzip, `Accept: text/*;q=0, */*`
selected text/html, `Accept: text/html;q=0, */*` selected
text/html;charset=utf-8, and `Accept-Language: en;q=0, *` selected en-US,
with and without strict locale negotiation.

A supported value is now excluded when the most specific value in the
header that matches it has q=0 (RFC 9110, Section 12.5.1), so
`text/html, text/*;q=0` still selects text/html. A media range matches the
media types of its type and subtype that have its parameters, and a
language range matches the tags that equal it or start with it followed
by "-" (RFC 4647, Section 3.3.1). A type/subtype range is more specific
than a type/* range, which is more specific than */*, and a range with
more parameters is more specific than one of the same kind with fewer, so
`text/*;charset=utf-8;q=0, text/*` excludes text/html;charset=utf-8.
These comparisons ignore case, except for parameter values other than
charset (RFC 9110, Section 8.3). Wildcards, broader matches and the strict
locale fallbacks skip excluded values. Without a match, the fallback to
the first supported value when `$strictMatch` is false is unchanged.
@wleeaf
wleeaf force-pushed the negotiate-wildcard-excluded-values branch from 045a0d4 to 3f468d2 Compare October 27, 2026 09:00
@wleeaf

wleeaf commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto develop after #10660. Only the changelog conflicted; both entries are kept. NegotiateTest (24 tests) and tests/system/HTTP pass, and PHPStan, Rector and php-cs-fixer are clean on the changed files.

This branch has not been deployed

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

Labels

bug Verified issues on the current code behavior or pull requests that will fix them

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants