Skip to content
Control Plane Labs

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/index

What 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.

Other 3xx codes