- Home
- HTTP Headers
- Content-Location Header
Header
Content-Location Header
Learn how Content-Location indicates an alternate URL for returned content. Useful for content negotiation and identifying canonical resource locations.
Content-Location Header
TL;DR: Indicates an alternate URL where the exact same content can be accessed. Useful for content negotiation and showing the canonical location of returned data.
What is Content-Location?
The Content-Location header indicates an alternate URL where the exact same content can be accessed. It’s like saying “What you requested is available at this specific address too” or “Here’s the direct link to this particular version.”
This is useful for content negotiation, caching, and letting clients know the canonical URL for the returned content.
How Content-Location Works
Client requests a resource:
GET /api/posts/latest HTTP/1.1
Host: blog.example.com
Accept: application/json
```text
**Server responds with specific location:**
```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /api/posts/123
Cache-Control: max-age=3600
{
"id": 123,
"title": "Latest Post",
"content": "..."
}
The client now knows that /api/posts/123 is the permanent URL for this content.
Syntax
Content-Location: <url>
Content-Location: /path/to/resource
Content-Location: https://example.com/resource
```text
### Values
- **Relative URL** - Path relative to the requested URL
- **Absolute URL** - Complete URL including protocol and host
## Common Examples
### Latest Resource Redirect
```http
Content-Location: /api/articles/456
Points to the actual resource location.
Content Negotiation
Content-Location: /documents/report.pdf
```text
Indicates which variant was selected.
### Language Variant
```http
Content-Location: /page.es.html
Shows the specific language version delivered.
Versioned API
Content-Location: /api/v2/users/789
```text
Indicates the actual API version endpoint used.
## Real-World Scenarios
### Latest Post Endpoint
```http
GET /blog/latest HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Location: /blog/posts/2026-01-18-my-latest-post
Content-Type: text/html
ETag: "abc123"
<article>Latest post content...</article>
Content Negotiation Example
GET /documents/report HTTP/1.1
Host: docs.example.com
Accept: application/pdf
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Location: /documents/report.pdf
Vary: Accept
[PDF content]
```text
### Form Submission Result
```http
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name": "John Doe"}
HTTP/1.1 201 Created
Content-Location: /api/users/456
Location: /api/users/456
Content-Type: application/json
{"id": 456, "name": "John Doe"}
Language Selection
GET /welcome HTTP/1.1
Host: example.com
Accept-Language: fr-FR
HTTP/1.1 200 OK
Content-Language: fr
Content-Location: /welcome.fr.html
Vary: Accept-Language
<h1>Bienvenue</h1>
```javascript
## Server Implementation
### Express.js (Node.js)
```javascript
const express = require('express')
const app = express()
// Latest post endpoint
app.get('/blog/latest', async (req, res) => {
const latestPost = await getLatestPost()
res.setHeader('Content-Location', `/blog/posts/${latestPost.slug}`)
res.setHeader('Cache-Control', 'max-age=300')
res.json(latestPost)
})
// Content negotiation
app.get('/documents/report', (req, res) => {
const accept = req.headers.accept || ''
if (accept.includes('application/pdf')) {
res.setHeader('Content-Location', '/documents/report.pdf')
res.setHeader('Content-Type', 'application/pdf')
res.sendFile('./reports/report.pdf')
} else if (accept.includes('application/json')) {
res.setHeader('Content-Location', '/documents/report.json')
res.json(getReportData())
} else {
res.setHeader('Content-Location', '/documents/report.html')
res.send('<html>...</html>')
}
res.setHeader('Vary', 'Accept')
})
// Resource creation
app.post('/api/users', async (req, res) => {
const user = await createUser(req.body)
res.status(201)
res.setHeader('Location', `/api/users/${user.id}`)
res.setHeader('Content-Location', `/api/users/${user.id}`)
res.json(user)
})
// Language variants
app.get('/page', (req, res) => {
const lang = req.acceptsLanguages(['en', 'es', 'fr']) || 'en'
res.setHeader('Content-Language', lang)
res.setHeader('Content-Location', `/page.${lang}.html`)
res.setHeader('Vary', 'Accept-Language')
res.sendFile(`./pages/page.${lang}.html`)
})
Advanced Caching with Content-Location
app.get('/api/current-user', async (req, res) => {
const userId = req.session.userId
const user = await getUserById(userId)
// Tell client the specific resource URL
res.setHeader('Content-Location', `/api/users/${userId}`)
// This allows the client to cache it properly
res.setHeader('Cache-Control', 'private, max-age=600')
res.setHeader('ETag', generateETag(user))
res.json(user)
})
```javascript
### FastAPI (Python)
```python
from fastapi import FastAPI, Response, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/blog/latest")
async def get_latest_post(response: Response):
post = await get_latest_post_from_db()
response.headers["Content-Location"] = f"/blog/posts/{post.id}"
response.headers["Cache-Control"] = "max-age=300"
return post
@app.get("/documents/report")
async def get_report(request: Request, response: Response):
accept = request.headers.get('accept', '')
if 'application/pdf' in accept:
response.headers["Content-Location"] = "/documents/report.pdf"
return FileResponse("report.pdf")
elif 'application/json' in accept:
response.headers["Content-Location"] = "/documents/report.json"
return get_report_data()
else:
response.headers["Content-Location"] = "/documents/report.html"
return HTMLResponse("<html>...</html>")
@app.post("/api/users", status_code=201)
async def create_user(user: dict, response: Response):
new_user = await create_user_in_db(user)
response.headers["Location"] = f"/api/users/{new_user.id}"
response.headers["Content-Location"] = f"/api/users/{new_user.id}"
return new_user
Django
from django.http import JsonResponse, FileResponse
from django.views.decorators.vary import vary_on_headers
def latest_post(request):
post = Post.objects.latest('created_at')
response = JsonResponse({
'id': post.id,
'title': post.title,
'content': post.content
})
response['Content-Location'] = f'/blog/posts/{post.slug}'
response['Cache-Control'] = 'max-age=300'
return response
@vary_on_headers('Accept')
def get_document(request):
accept = request.META.get('HTTP_ACCEPT', '')
if 'application/pdf' in accept:
response = FileResponse(open('report.pdf', 'rb'))
response['Content-Location'] = '/documents/report.pdf'
elif 'application/json' in accept:
response = JsonResponse(get_report_data())
response['Content-Location'] = '/documents/report.json'
else:
response = HttpResponse('<html>...</html>')
response['Content-Location'] = '/documents/report.html'
return response
```nginx
### Nginx
```nginx
server {
listen 80;
server_name example.com;
# Latest resource
location = /latest {
# Use try_files to serve the actual latest file
try_files /content/latest.html =404;
add_header Content-Location "/content/latest.html";
}
# Content negotiation
location /report {
set $content_location "";
if ($http_accept ~* "application/pdf") {
set $content_location "/reports/report.pdf";
rewrite ^/report$ /reports/report.pdf break;
}
if ($http_accept ~* "application/json") {
set $content_location "/reports/report.json";
rewrite ^/report$ /reports/report.json break;
}
add_header Content-Location $content_location;
add_header Vary "Accept";
}
}
Best Practices
For Servers
1. Use for content negotiation results
# ✅ Tell client which variant was selected
Content-Type: application/pdf
Content-Location: /documents/report.pdf
Vary: Accept
```javascript
**2. Set Content-Location for dynamic endpoints**
```javascript
// ✅ Latest endpoint points to actual resource
app.get('/latest', (req, res) => {
const item = getLatest()
res.setHeader('Content-Location', `/items/${item.id}`)
res.json(item)
})
3. Use with caching headers
# ✅ Helps with cache key generation
Content-Location: /api/posts/123
ETag: "abc123"
Cache-Control: max-age=3600
```text
**4. Don't confuse with Location header**
```http
# POST creates resource
# Location = where to find the new resource
Location: /api/users/456
# Content-Location = URL of returned representation
Content-Location: /api/users/456
# They can be the same for 201 responses
5. Use relative URLs when possible
# ✅ Relative URL
Content-Location: /api/posts/123
# ⚠️ Absolute only when needed
Content-Location: https://example.com/api/posts/123
```javascript
### For Clients
**1. Use for cache optimization**
```javascript
const cache = new Map()
fetch('/api/latest-post').then((response) => {
const contentLocation = response.headers.get('Content-Location')
if (contentLocation) {
return response.json().then((data) => {
// Cache using the specific URL
cache.set(contentLocation, data)
return data
})
}
})
2. Update browser history if needed
fetch('/blog/latest').then((response) => {
const contentLocation = response.headers.get('Content-Location')
if (contentLocation) {
// Update URL without navigation
history.replaceState(null, '', contentLocation)
}
return response.text()
})
```javascript
**3. Handle relative URLs**
```javascript
function resolveContentLocation(requestUrl, contentLocation) {
if (contentLocation.startsWith('http')) {
return contentLocation
}
const base = new URL(requestUrl)
return new URL(contentLocation, base).href
}
Content-Location vs Location
Location Header (201, 3xx responses)
# 201 Created - where the new resource is
HTTP/1.1 201 Created
Location: /api/users/123
# 301 Redirect - where to go instead
HTTP/1.1 301 Moved Permanently
Location: /new-url
```text
### Content-Location Header (200 responses)
```http
# 200 OK - the URL of this specific content
HTTP/1.1 200 OK
Content-Location: /api/posts/456
# Both can be used together
HTTP/1.1 201 Created
Location: /api/users/789
Content-Location: /api/users/789
Common Use Cases
Latest Resource Pattern
GET /latest → Content-Location: /items/123
```text
### Content Negotiation
```http
GET /doc → Content-Location: /doc.pdf (based on Accept header)
Form Result
POST /create → Content-Location: /resources/new-id
```text
### Language Variants
```http
GET /page → Content-Location: /page.es.html (based on Accept-Language)
Uploaded File
POST /upload → Content-Location: /files/abc-def-ghi
```text
## Testing Content-Location
### Using curl
```bash
# Check Content-Location header
curl -I https://example.com/latest
# With content negotiation
curl -H "Accept: application/pdf" -I https://example.com/document
# Full request
curl -v https://example.com/latest
Using JavaScript
// Check Content-Location
fetch('https://api.example.com/latest').then((response) => {
const contentLocation = response.headers.get('Content-Location')
console.log('Content is available at:', contentLocation)
// Could use this for caching or URL updates
if (contentLocation) {
console.log('Canonical URL:', contentLocation)
}
return response.json()
})
// With content negotiation
fetch('https://example.com/document', {
headers: {
Accept: 'application/pdf'
}
}).then((response) => {
console.log('Content-Location:', response.headers.get('Content-Location'))
console.log('Content-Type:', response.headers.get('Content-Type'))
})
Related Headers
- Location - Redirect target or new resource location
- Content-Type - Media type of the content
- Vary - Which request headers affect the response
- ETag - Resource version identifier
Frequently Asked Questions
What is Content-Location?
Content-Location indicates the direct URL for the returned representation. After content negotiation, it shows where this specific variant can be accessed directly.
What is the difference between Content-Location and Location?
Location is for redirects (3xx) pointing to a different resource. Content-Location identifies the URL of the current representation, not a redirect target.
When should I use Content-Location?
Use it after content negotiation to show the direct URL for the selected variant. For example, if /doc returns HTML, Content-Location: /doc.html shows the specific version.
Does Content-Location affect caching?
Yes, caches may use Content-Location as an alternate key. The response can be reused for requests to either the original URL or the Content-Location URL.