300 Multiple Choices
Redirection response defined by RFC 9110 §15.4.1.
Last updated July 27, 2026
Common causes at a glance
- Apache mod_negotiation with MultiViews finding several matching files for one path
- A request for an extensionless path that maps to index.html, index.de.html and index.pdf
- An API returning a disambiguation list when an identifier matches multiple records
- A hand-rolled content-negotiation layer that cannot satisfy the Accept header
Reproduce it
curl -i -H 'Accept: text/html' https://www.example.com/manual/indexWhat 300 Multiple Choices means
300 is the status code for reactive negotiation: rather than the server
choosing a representation from your Accept headers, it hands back a menu
and lets the agent choose. A document available as English HTML, German HTML, and PDF
returns 300 with the three URLs, and the user agent either presents the choice or
resolves it automatically.
That never happened. No browser renders a machine-readable choice list, and the header
fields proposed for carrying one were dropped before HTTP/1.1 was finished. What
survives is Apache’s mod_negotiation emitting a human-readable HTML menu,
plus the occasional API that reaches for 300 when a lookup is ambiguous.
What the spec says
RFC 9110 §15.4.1 says a server with a preferred
choice SHOULD send Location, and that for methods other than HEAD it
SHOULD send content listing the alternatives. Deliberately, no format for that list is
standardised, which is precisely why automatic selection never shipped. The section’s
own note records that the URI and Alternates header fields
proposed for the job were dropped for lack of deployment, and offers
RFC 8288 Link relations of type alternate as a
workaround while calling it a chicken-and-egg problem. Reactive negotiation in general
is §12.2.
What actually causes it
In practice 300 nearly always traces back to Apache. Turn on
Options MultiViews, drop report.html and
report.pdf in the same directory, and a request for /report
with an Accept header that scores both variants equally produces a
negotiation menu instead of a file. The second source is an API author choosing 300 for
“your query matched three customers” — defensible by the letter of the spec, useless in
practice, since no client library does anything with it. Those APIs are better served
by 200 with a result array, or a 303 pointing
at the single correct record.
How to debug it
Ask for the alternatives explicitly and read what comes back:
curl -isS -H 'Accept: text/html;q=0.9, application/pdf;q=0.9' \
https://www.example.com/manual/index | head -30
If Location is present, curl with -L will follow it and you
will never notice the 300, so drop the flag while diagnosing. On Apache, the fastest
confirmation is Options -MultiViews on the directory: if the 300 turns
into a 404, negotiation was the cause and you need explicit paths
or a Redirect.
A 300 is also heuristically cacheable, so a CDN can pin the menu for a resource that
later gains one canonical representation. Check Vary and
Cache-Control with the header inspector before
assuming the origin is still producing it.
Working examples
curl
# No -L: following the Location hides the 300 you are trying to see.
curl -isS \
-H 'Accept: text/html;q=0.9, application/pdf;q=0.9' \
-H 'Accept-Language: en;q=0.9, de;q=0.9' \
https://www.example.com/manual/index | head -30
# Apache's negotiation menu also advertises variants in TCN/Alternates headers.
Python (requests)
import requests
resp = requests.get(
"https://www.example.com/manual/index",
headers={"Accept": "text/html;q=0.9, application/pdf;q=0.9"},
allow_redirects=False, # requests would not follow a 300 anyway
timeout=10,
)
print(resp.status_code, resp.headers.get("Location"))
# The alternatives list has no standard format; parse the body yourself.
if resp.status_code == 300:
print(resp.text[:500])
Node (fetch)
// fetch() treats 300 as a normal response: no automatic redirect,
// because there is no single answer to redirect to.
const resp = await fetch("https://www.example.com/manual/index", {
headers: { Accept: "text/html;q=0.9, application/pdf;q=0.9" },
redirect: "manual",
});
console.log(resp.status, resp.headers.get("location"));
if (resp.status === 300) console.log((await resp.text()).slice(0, 500));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.