300 Multiple Choices
What is HTTP 300 Multiple Choices?
When you ask a website for something, sometimes that thing exists in different v...
Explain Like I’m 3
You asked for something, but there are many different versions of it! The website says ‘I have this in red, blue, and green - which one do you want?’ You have to pick which one you like!
Example: You ask for a toy, but the toy comes in 3 colors. Someone shows you all 3 colors and says ‘Pick which one you want!’ That’s what the website is doing - showing you choices!
Explain Like I’m 5
When you ask a website for something, sometimes that thing exists in different versions - like a book that comes in English, Spanish, and French, or a video in HD and regular quality. Instead of guessing which one you want, the website sends you a ‘300 Multiple Choices’ message with a list of all the versions. Then you (or your web browser) can pick the best one. It’s like going to an ice cream shop and the person says ‘We have chocolate, vanilla, and strawberry - which flavor do you want?’
Example: You want to download a song. The website has it in MP3 format and AAC format. Instead of just picking one for you, it shows you both options and lets you click on the format you prefer.
Jr. Developer
HTTP 300 Multiple Choices is a redirection status indicating the requested resource corresponds to multiple possible representations, each with its own specific location. The server provides a list of alternatives (typically as links in the response body or via Link headers) and may suggest a preferred choice via the Location header. This is called agent-driven negotiation - the client (or user) selects from the provided options. In practice, 300 is extremely rare. Modern systems use server-driven content negotiation instead - where the server automatically selects the best representation based on Accept, Accept-Language, and other request headers, returning 200 with that representation. You’ll almost never need to implement 300; server-driven negotiation with 200 is standard.
Example: A documentation site has /api/reference available as /api/reference.html, /api/reference.pdf, and /api/reference.json. Instead of automatically choosing based on Accept headers (server-driven), the server returns 300 with links to all three formats, letting the user click their preferred format.
Code Example
// Express.js returning 300 with format choices (rarely used)app.get('/api/docs/:page', (req, res) => { const page = req.params.page;
// Provide multiple format options const formats = [ { url: `/api/docs/${page}.html`, type: 'text/html' }, { url: `/api/docs/${page}.pdf`, type: 'application/pdf' }, { url: `/api/docs/${page}.json`, type: 'application/json' } ];
// Suggest default (HTML) res.status(300) .set('Location', `/api/docs/${page}.html`) .set('Link', formats.map(f => `<${f.url}>; rel="alternate"; type="${f.type}"`).join(', ')) .json({ message: 'Multiple representations available', choices: formats });});
// Better approach: server-driven negotiationapp.get('/api/docs/:page', (req, res) => { const page = req.params.page; const acceptedType = req.accepts(['html', 'json', 'pdf']);
// Automatically choose based on Accept header if (acceptedType === 'html') { return res.sendFile(`/docs/${page}.html`); } else if (acceptedType === 'json') { return res.json(docs[page]); } else if (acceptedType === 'pdf') { return res.sendFile(`/docs/${page}.pdf`); }
// Default to HTML if no preference res.sendFile(`/docs/${page}.html`);});Crash Course
300 Multiple Choices, defined in RFC 9110 Section 15.4.1 (obsoleting RFC 7231), indicates the target resource has more than one representation, each with its own specific identifier. The server provides information about alternatives so the user agent can select a preferred representation by redirecting to one of the provided identifiers. This is agent-driven (reactive) content negotiation - the client makes the choice after receiving the list. The server SHOULD include a Location header with the preferred choice and SHOULD provide a response payload listing available representations with their URIs. The response typically includes Link headers with rel=“alternate” for each option. In practice, 300 is almost never used because: (1) most browsers don’t have UI for handling multiple choices, requiring manual user intervention, (2) the additional round-trip slows down the interaction, (3) server-driven negotiation (using Accept headers to automatically select the best representation and return 200) is far more efficient and universally supported. Modern APIs use server-driven negotiation or explicit versioning in URLs rather than 300 responses.
Example: An API serves /resources/report in JSON, XML, and CSV formats. Instead of using Accept headers for server-driven negotiation, it returns 300 with a payload listing all three format URLs. The client then makes a second request to their preferred format. However, a better approach would be server-driven negotiation: the client sends Accept: application/json, and the server returns 200 with the JSON representation immediately, avoiding the extra round-trip.
Code Example
// Agent-driven negotiation with 300 (not recommended)const express = require('express');const app = express();
app.get('/reports/:id', (req, res) => { const reportId = req.params.id;
const choices = [ { url: `/reports/${reportId}.json`, type: 'application/json', description: 'JSON format for API consumption' }, { url: `/reports/${reportId}.xml`, type: 'application/xml', description: 'XML format for legacy systems' }, { url: `/reports/${reportId}.csv`, type: 'text/csv', description: 'CSV format for spreadsheet import' } ];
// Return 300 with choices res.status(300) .set('Location', `/reports/${reportId}.json`) // Suggest JSON .set('Link', choices.map(c => `<${c.url}>; rel="alternate"; type="${c.type}"` ).join(', ')) .json({ message: 'Multiple formats available', reportId, choices });});
// BETTER: Server-driven negotiation (recommended)app.get('/reports/:id', async (req, res) => { const reportId = req.params.id; const report = await db.getReport(reportId);
if (!report) { return res.status(404).json({ error: 'Report not found' }); }
// Check Accept header and respond accordingly const format = req.accepts(['json', 'xml', 'csv']);
res.set('Vary', 'Accept'); // Important for caching
switch (format) { case 'json': return res.status(200).json(report);
case 'xml': return res.status(200) .type('application/xml') .send(convertToXML(report));
case 'csv': return res.status(200) .type('text/csv') .attachment(`report-${reportId}.csv`) .send(convertToCSV(report));
default: // 406 Not Acceptable if none match return res.status(406).json({ error: 'Not Acceptable', supportedFormats: ['application/json', 'application/xml', 'text/csv'] }); }});
// Alternative: Explicit format in URL (most common)app.get('/reports/:id.:format', async (req, res) => { const { reportId, format } = req.params; const report = await db.getReport(reportId);
if (!report) { return res.status(404).json({ error: 'Report not found' }); }
switch (format) { case 'json': return res.json(report); case 'xml': return res.type('xml').send(convertToXML(report)); case 'csv': return res.type('csv').send(convertToCSV(report)); default: return res.status(40...Deep Dive
The 300 Multiple Choices status code, specified in RFC 9110 Section 15.4.1, represents agent-driven content negotiation where the target resource has multiple representations, each with its own specific identifier, and the server provides metadata about alternatives so the user agent can select a preferred representation. Per RFC 9110, if the server has a preferred choice of representation, it SHOULD generate a Location header field containing a preferred choice’s URI reference that the user agent MAY use for automatic redirection. For methods other than HEAD, the server SHOULD generate a payload containing a list of representation metadata and URI references from which the user or user agent can choose. The 300 response is cacheable by default unless otherwise indicated by method definition or explicit cache controls.
Technical Details
Content negotiation in HTTP exists in three forms: server-driven (proactive), where the server selects based on request headers like Accept, Accept-Language, Accept-Encoding; agent-driven (reactive), where the server returns 300 with a list and the client chooses; and transparent negotiation (hybrid), defined in RFC 2295 but never widely adopted. Agent-driven negotiation with 300 addresses scenarios where server-driven negotiation is insufficient - when the choice requires user input (language preference without Accept-Language header), when representations have different purposes rather than just formats (interactive vs. printable versions), or when listing all options provides value to the user.\n\nRFC 9110 recommends using Link headers with rel=“alternate” to indicate available representations, deprecating the older Alternates header (RFC 2295). The response format typically includes a Location header for the server’s preferred choice and a JSON or HTML payload listing all alternatives with their URIs, media types, and descriptions. Caching semantics for 300 follow standard rules - responses are cacheable by default for GET and HEAD, and the Vary header should list request headers that might affect the choice of alternatives presented.\n\nPractical limitations make 300 extremely rare in production: most browsers lack UI for presenting choices (they just render the HTML payload as a regular page with links), the additional round-trip increases latency significantly (200-500ms+ depending on network), automatic selection by user agents is inconsistent (some follow Location, others present the list), and developer tooling doesn’t well-support 300 debugging and testing. Modern alternatives include server-driven negotiation with Accept headers (99% of use cases), URL-based format specification like /resource.json vs /resource.xml (explicit and cacheable), query parameters for format selection like /resource?format=json (simple but less RESTful), and API versioning headers for backward compatibility (Content-Type: application/vnd.api+json;version=2).\n\nSecurity considerations for 300 include open redirect vulnerabilities if the list of choices is user-controlled or includes external URLs without validation, cache poisoning if the Vary header doesn’t correctly reflect which request headers affect the alternatives list, and information disclosure where listing all available representations might expose internal structure or formats not intended for all users. If implementing 300, validate all URIs in the alternatives list are internal and expected, set appropriate Vary headers (typically Vary: Accept, Accept-Language), limit the number of alternatives to prevent response bloat, and consider whether 300 adds value or just latency compared to server-driven negotiation.\n\nHistorical context: HTTP/1.0 (RFC 1945) included 300, HTTP/1.1 (RFC 2068, 2616, 7231) refined it, but adoption remained minimal due to poor browser support and superior alternatives…
Code Example
// Production-grade 300 implementation (rarely needed)\nconst express = require('express');\nconst crypto = require('crypto');\nconst app = express();\n\n// Middleware to handle content negotiation\nfunction multipleChoicesMiddleware(options) {\n return (req, res, next) => {\n // Store negotiation metadata on request\n req.negotiationChoices = options;\n next();\n };\n}\n\napp.get('/resources/:id', \n multipleChoicesMiddleware({\n formats: ['json', 'xml', 'csv', 'html'],\n defaultFormat: 'json'\n }),\n async (req, res) => {\n const resourceId = req.params.id;\n const resource = await db.getResource(resourceId);\n \n if (!resource) {\n return res.status(404).json({ error: 'Resource not found' });\n }\n \n // Try server-driven negotiation first\n const acceptedFormat = req.accepts(['json', 'xml', 'csv', 'html']);\n \n if (acceptedFormat && acceptedFormat !== '*/*') {\n // Client specified preference - use server-driven\n return serveFormat(res, resource, acceptedFormat);\n }\n \n // No Accept header or wildcard - return 300 with choices\n const baseUrl = \`${req.protocol}://${req.get('host')}/resources/${resourceId}\`;\n \n const choices = [\n {\n uri: \`${baseUrl}.json\`,\n type: 'application/json',\n language: 'en',\n length: JSON.stringify(resource).length,\n description: 'JSON format for programmatic access'\n },\n {\n uri: \`${baseUrl}.xml\`,\n type: 'application/xml',\n language: 'en',\n length: estimateXMLSize(resource),\n description: 'XML format for legacy system integration'\n },\n {\n uri: \`${baseUrl}.csv\`,\n type: 'text/csv',\n language: 'en',\n length: estimateCSVSize(resource),\n description: 'CSV format for spreadsheet import'\n },\n {\n uri: \`${baseUrl}.html\`,\n type: 'text/html',\n language: 'en',\n length: estimateHTMLSize(resource),\n description: 'HTML format for browser viewing'\n }\n ];\n \n // Generate ETag for the choices list\n const etag = crypto\n .createHash('md5')\n .update(JSON.stringify(choices))\n .digest('hex');\n \n // Check conditional request\n if (req.headers['if-none-match'] === etag) {\n return res.status(304).end();\n }\n \n // Build Link headers\n const linkHeaders = choices.map(choice => \n \`<${choice.uri}>; rel=\\\"alternate\\\"; type=\\\"${choice.type}\\\"; length=\\\"${choice.length}\\\"\`\n ).join(', ');\n \n res.status(300)\n .set({\n 'Location': \`${baseUrl}.json\`, // Suggest JSON as default\n 'Link': linkHeaders,\n 'Cache-Control': 'public, max-age=3600',\n 'Vary': 'Accept, Accept-Language',\n 'ETag': etag,\n 'Content-Type': 'application/json'\n })\n .json({\n code: 300,\n message: 'Multiple represent...Frequently Asked Questions
When should I use 300 Multiple Choices?
Almost never. Modern web development favors server-driven content negotiation (where the server automatically selects the best representation based on Accept headers) or explicit URL-based format specification (/resource.json vs /resource.xml). Use 300 only if you specifically need the client or user to manually choose from a list of representations, which is extremely rare.
What's the difference between 300 Multiple Choices and 301/302 redirects?
300 provides multiple options for the client to choose from (agent-driven), while 301 (Permanent Redirect) and 302 (Temporary Redirect) tell the client exactly where to go (server-driven). 301/302 cause automatic redirection to a single URL; 300 presents a list and waits for the client to pick one.
How do browsers handle 300 responses?
Most browsers don't have special UI for 300 responses. They typically render the response payload (if it's HTML with links) as a regular web page, or follow the Location header if provided. There's no standard 'choose from this list' dialog, which is one reason 300 is rarely used.
Is 300 Multiple Choices cacheable?
Yes, 300 responses are cacheable by default unless otherwise indicated by Cache-Control headers. However, you should set Vary headers appropriately (e.g., Vary: Accept, Accept-Language) to ensure caches don't serve incorrect alternative lists to different clients.
What's the alternative to using 300 Multiple Choices?
Use server-driven content negotiation: clients send Accept headers (Accept: application/json, Accept-Language: en), and the server returns 200 with the best matching representation. This is faster (no extra round-trip), universally supported, and how 99% of modern APIs and websites handle multiple formats. For APIs, explicit format in URLs (/resource.json) is also common and more explicit.
Common Causes
- A resource available in multiple file formats (JSON, XML, CSV, PDF, HTML)
- Content available in multiple languages without Accept-Language header
- Multiple versions or variants of a document (print version, interactive version)
- API resource with multiple representation options
- Documentation available in different formats (HTML docs, PDF manual, JSON API spec)
- Media available at different quality levels or resolutions
Implementation Guidance
- Replace with server-driven negotiation using Accept headers (recommended for 99% of cases)
- Use explicit format specification in URLs (/resource.json, /resource.xml) instead
- Implement automatic format selection based on Accept header, returning 200
- If using 300, include Location header with the recommended default choice
- Provide Link headers with rel=‘alternate’ for each available representation
- Include meaningful response payload (JSON or HTML) listing all options with descriptions
- Set Vary: Accept, Accept-Language to ensure proper caching
- Validate all alternative URIs to prevent open redirect vulnerabilities
- Consider if the extra round-trip of 300 provides value or just adds latency
- For most modern APIs, skip 300 entirely and use explicit URLs or Accept-based negotiation