Skip to content

Content negotiation

Greg Bowler edited this page Apr 14, 2026 · 1 revision

Content negotiation is how the router chooses the best callback when more than one route matches the same request.

This is especially useful when the same URI can serve different representations, such as HTML for a browser and JSON for an API client.

Accept header

Browsers often send broad Accept headers. A normal page request may include values such as:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Without content negotiation, a callback that merely mentions application/xml could accidentally win over a page route. The router avoids that by comparing the quality values and selecting the best match.

Each callback can declare one or more accepted media types:

#[Any(path: "/report", accept: "text/html,application/xhtml+xml")]
public function html():void {}

#[Any(path: "/report", accept: "application/json,application/xml")]
public function data():void {}

When route() runs:

  1. callbacks that do not match the request method are ignored
  2. callbacks that do not match the request path are ignored
  3. callbacks whose accept value is incompatible with the request are ignored
  4. the remaining callbacks are ranked using the negotiated quality value

The callback with the highest quality wins.

*/* behaviour

If the request sends Accept: */*, the first matching compatible callback wins.

That keeps the result deterministic even when the client has not expressed any preference.

Media type declaration format

The accept argument is a comma-separated string, for example:

accept: "application/json,application/xml"

or:

accept: "text/html,application/xhtml+xml,application/xml;q=0.9"

When to use content negotiation

Use it when the same URI can return more than one representation:

  • HTML and JSON from the same route name
  • HTML and XML feeds
  • Human-facing pages and machine-facing endpoints that share a common path

If the URI already separates those concerns clearly, content negotiation may not be necessary.


The callback usually hands off to filesystem matching after it has been selected. That is covered in path matching.

Clone this wiki locally