# Content-Location Header

> Learn how Content-Location indicates an alternate URL for returned content. Useful for content negotiation and identifying canonical resource locations.

Source: https://howhttpworks.com/headers/content-location
Last reviewed: 2026-10-05

> **TL;DR:** `Content-Location` tells the client the direct URL of what it just received. If `/report` negotiates JSON, the server can send `Content-Location: /report.json`. It's metadata only: no redirect, no canonical-URL declaration, no extra cache entry.

## What is Content-Location?

Its main job is naming the variant a server picked: a request to `/report` might negotiate JSON and come back with `Content-Location: /report.json`. It describes the response body; the request URL stays what it was. See [RFC 9110 §8.7](https://www.rfc-editor.org/rfc/rfc9110#section-8.7).

## How Content-Location Works

Requests and responses on this page are examples, with bodies omitted:

```http
GET /report HTTP/1.1
Host: example.com
Accept: application/json
```

```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /report.json
Vary: Accept
```

The address bar stays at `/report`. The server should also serve this representation at `/report.json`; the header is a pointer, so you still have to build the route.

## Syntax

```http
Content-Location: /report.json
Content-Location: report.json
Content-Location: https://example.com/report.json
```

Relative values resolve against the request's target URI. A leading slash starts at the origin root; `report.json` resolves relative to the target path.

For a response to `https://example.com/reports/current`, `summary.json` resolves to `https://example.com/reports/summary.json`; `/summary.json` resolves to `https://example.com/summary.json`. Resolve it against the request URL, not against the page that made the fetch.

It also leaves the base URL for relative links inside an HTML document alone. Naming the representation and resolving the document's links are separate things.

## Common Examples

A negotiated `/welcome` response might point to `/welcome.fr.html`. A `/posts/latest` response might point to the specific post it returned. The content at either URL may change later.

| Request target | Possible returned field | Meaning |
| --- | --- | --- |
| `/welcome` | `/welcome.fr.html` | Selected French representation |
| `/posts/latest` | `/posts/42` | Post currently returned by the alias |
| `/report` | `/report.json` | Selected JSON representation |

The alias and the post it points to live on different timelines. Publish a new post and `/posts/latest` returns that one, while `/posts/42` keeps naming the old one. And `/posts/42` itself can be edited, unless your application promises otherwise.

## Real-World Scenarios

In a `201 Created` response, `Location` identifies the created resource. If `Content-Location` has the same value, the response body is a current representation of that resource. If it differs, the body might be a status report about the operation instead. [MDN illustrates the distinction](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Location).

Here the body of a creation response is the new report itself:

```http
HTTP/1.1 201 Created
Location: /reports/42
Content-Location: /reports/42
Content-Type: application/json
```

A purchase that returns a receipt is different: the created order and the receipt in the body have their own URLs.

```http
HTTP/1.1 201 Created
Location: /orders/42
Content-Location: /receipts/42
Content-Type: application/json
```

Clients should go by the API contract when choosing which URI to fetch. A creation response's body might be the new object, or it might be something else entirely.

## Server Implementation

These Express routes serve both the negotiation endpoint and its identified variant:

```javascript
const report = { status: 'ready' }
app.get('/report.json', (req, res) => res.json(report))
app.get('/report', (req, res) => {
  res.vary('Accept')
  if (!req.accepts('application/json')) return res.status(406).end()
  res.set('Content-Location', '/report.json')
  res.json(report)
})
```

Set headers before `res.json()` sends the response. [Express documents `req.accepts()`](https://expressjs.com/en/5x/api/request/#req.accepts) and [response methods](https://expressjs.com/en/5x/api/response/).

A FastAPI application can return an alias and a direct endpoint for the same fixture:

```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()
post = {'id': 42, 'title': 'Release notes'}

@app.get('/posts/42')
def specific_post():
    return post

@app.get('/posts/latest')
def latest_post():
    return JSONResponse(post, headers={'Content-Location': '/posts/42'})
```

When returning a response object directly, put headers on **that object**. [FastAPI distinguishes this from setting headers on its injected temporary `Response`](https://fastapi.tiangolo.com/advanced/response-headers/). The alias is a real endpoint that returns data, not a redirect. In a real app, swap the fixture for the database record you selected, and make sure the direct endpoint enforces the same access rules.

For a fixed Django alias, set the representation identifier on the returned object:

```python
from django.http import JsonResponse

def latest_post(request):
    response = JsonResponse({'id': 42, 'title': 'Release notes'})
    response['Content-Location'] = '/posts/42'
    return response
```

Map `/posts/42` to a view that returns the post too. The header won't register a URL pattern for you.

## Getting Content-Location right

A shared cache stores the `/report` response under `/report`, not under `/report.json` as well. [RFC 9111 §2](https://www.rfc-editor.org/rfc/rfc9111#section-2) bases cache keys on the request method and target URI, with negotiated variants differentiated using `Vary`. If you want aliasing, configure it on your cache and test it.

Be careful with a value pointing at another origin: the server sending it may not control that URL. RFC 9110 §8.7 warns that a client can't work out ownership from the field alone.

Invalidation works differently again. After a successful unsafe request such as POST, a cache must invalidate the request target and **may** invalidate same-origin URIs named in `Location` or `Content-Location`. It must not invalidate URIs on another origin this way. [RFC 9111 §4.4](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.4) has the rules. So naming `/receipts/42` in a POST response is a hint, and some caches will keep serving their old copy of the receipt.

## Content-Location vs Location

`Location` supplies a redirect target or identifies a newly created resource. `Content-Location` describes the enclosed representation. Both can appear on a `201`; `Content-Location` is not restricted to `200` responses.

## Common Use Cases

Use the field when a negotiated variant or an operation's returned report has a useful retrieval URL. For a redirect, send the appropriate `3xx` status and `Location` instead.

Clients that send it on requests hit a gotcha. `PUT /report` with `Content-Location: /report.fr.html` is still a PUT to `/report`, not an update to just the French variant; RFC 9110 §8.7 requires the server to keep the original request semantics. To update one variant, send the PUT to that variant's URI. On the server, treat an incoming Content-Location as context, never as a new target.

## Testing Content-Location

```bash
curl -sS -D - -o /dev/null -H 'Accept: application/json' https://example.com/report
curl -sS -D - -o /dev/null https://example.com/report.json
```

Check that the variant endpoint exists and returns the same representation. `curl -L` follows redirects only; it won't fetch the `Content-Location` URL for you.

In a same-origin browser client, inspect and resolve the field explicitly:

```javascript
const response = await fetch('/report', {
  headers: { Accept: 'application/json' }
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const value = response.headers.get('Content-Location')
if (value) {
  const representation = new URL(value, response.url)
  console.log(representation.href)
}
```

[`response.url`](https://developer.mozilla.org/en-US/docs/Web/API/Response/url) supplies the final response URL, including redirects. Logging the URL has no side effects: no navigation, no caching. If you do fetch it, check the destination against your application's rules first. Cross-origin JavaScript needs the server to expose `Content-Location` through CORS before it can read this field.

## Related Headers

- [Location](https://howhttpworks.com/headers/location)
- [Content-Type](https://howhttpworks.com/headers/content-type)
- [Vary](https://howhttpworks.com/headers/vary)
- [ETag](https://howhttpworks.com/headers/etag)
