Skip to content

Never pick a media type refused with q=0 in accepts/2 - #6882

Closed
wleeaf wants to merge 2 commits into
phoenixframework:mainfrom
wleeaf:accepts-refused-q0
Closed

wleeaf wants to merge 2 commits into
phoenixframework:mainfrom
wleeaf:accepts-refused-q0

Conversation

@wleeaf

@wleeaf wleeaf commented Oct 25, 2026

Copy link
Copy Markdown

A media range with q=0 means the client does not accept that type (RFC 9110, section 12.4.2). accepts/2 only gave q=1 entries priority, and once nothing better matched it fell back to the remaining entries, including the q=0 ones. Wildcards didn't look at refusals either:

  • Accept: application/json;q=0 with accepts(conn, ["json"]) set the json format instead of raising Phoenix.NotAcceptableError.
  • Accept: */*;q=0 did the same.
  • Accept: application/json;q=0, */* with ["json", "text"] picked json through */*. The same happened when */* came first.
  • Accept: text/html;q=0, */* with ["html", "json"] picked html through the browser fallback.

With this change, entries with q=0 are never used, and formats refused by a specific media type are taken out of the candidates before matching. So neither a wildcard nor the */* html fallback can pick them. If nothing is left, Phoenix.NotAcceptableError is raised as before, and exception.accepts still lists every format the server offered. Headers without a q=0 entry give the same results as before.

The new test "never picks a media type refused with q=0" in test/phoenix/controller/controller_test.exs fails without the change (json is picked instead of text). mix format --check-formatted and mix compile --warnings-as-errors are clean, and test/phoenix/controller/controller_test.exs passes (72 tests).

A media range with q=0 means the client does not accept it (RFC 9110,
section 12.4.2), but accepts/2 still used such entries once nothing better
matched. "Accept: application/json;q=0" set the json format, and so did
"*/*;q=0". A wildcard also picked a format the header refused, for example
json for "application/json;q=0, */*" and html for "text/html;q=0, */*".

Entries with q=0 are no longer used, and formats refused by a specific
media type are not picked through a wildcard or the html fallback. When
nothing else matches, Phoenix.NotAcceptableError is raised.
@SteffenDE

Copy link
Copy Markdown
Member

Three cases worth discussing if we touch this code

  • Accept: */*;q=0 with ["html", "json"] returns HTML instead of raising. Another example: application/json, */*;q=0 returns HTML instead of JSON.
  • Accept: text/*;q=0, */* with ["text", "json"] returns text instead of JSON.
  • Accept: text/html;level=1;q=0, text/html with ["html"] now raises while it did not before

A format is refused when the most specific media range matching its media
type has q=0 (RFC 9110, section 12.5.1), so "*/*;q=0" and "text/*;q=0" now
refuse the formats they cover, while "text/html, text/*;q=0" still accepts
html and "text/html;level=1;q=0" no longer refuses text/html. The */*
browser fallback to html only applies when */* has a positive q and html
is not refused.
@wleeaf

wleeaf commented Oct 26, 2026

Copy link
Copy Markdown
Author

All three are handled now (6b2b37c). accepts/2 gives each candidate format the q of the most specific media range matching its media type (RFC 9110, section 12.5.1): type/subtype, then type/*, then */*. A range with parameters such as text/html;level=1 doesn't match the bare text/html, and a format whose q is 0 is refused. The */* browser fallback to html only applies when */* has a positive q and html isn't refused.

  1. */*;q=0 with ["html", "json"] raises again, and application/json, */*;q=0 picks json.
  2. text/*;q=0, */* with ["text", "json"] picks json.
  3. text/html;level=1;q=0, text/html with ["html"] picks html again.

text/html, text/*;q=0 still picks html, and the existing tests are unchanged. A new test covers these cases.

@SteffenDE

Copy link
Copy Markdown
Member

I'm wondering about the performance implications, since we now do more work for each request - cc @josevalim

@josevalim

Copy link
Copy Markdown
Member

The accepts header has been historically a pain to deal with because browsers and clients sent all kinds of things expecting different results. Generally speaking, I'd be fine with following the RFC to the letter, but accept has been anything but consistent, and trying to deal with a case of q=0 which will always lead to a failure only adds complexity. So I am 👎, thanks.

@wleeaf

wleeaf commented Oct 26, 2026

Copy link
Copy Markdown
Author

Understood, thanks for taking a look. Closing.

@wleeaf wleeaf closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants