Accept Header Versioning — Custom Media Types for API Versioning
In this tutorial, you'll learn about Accept Header. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Accept header versioning embeds API version information in the HTTP Accept header using custom media types, keeping URLs clean while supporting content negotiation.
What You'll Learn
By the end of this lesson, you will implement custom media types for API versioning, parse Accept headers for version extraction, handle version-based Serialization, and return proper 406 responses.
Why It Matters
Accept header versioning follows REST principles by keeping resource URIs stable while varying representation format through content negotiation.
Real-World Use
Durga Antivirus Pro uses Accept: application/vnd.durga.scan.v2+json for its v2 scan API, keeping the /api/scan endpoint URI unchanged across versions.
Custom Media Type Format
import re
from typing import Dict, Optional, Tuple
class AcceptHeaderParser:
def __init__(self, vendor_prefix: str = "vnd.durga"):
self.vendor_prefix = vendor_prefix
def parse_accept(self, accept: str) -> Optional[Dict]:
media_types = [
m.strip().split(";")[0].strip()
for m in accept.split(",")
]
for media_type in media_types:
result = self._parse_media_type(media_type)
if result:
return result
return None
def _parse_media_type(self, media_type: str
) -> Optional[Dict]:
pattern = (
rf"application/{self.vendor_prefix}\."
rf"(\w+)\.v(\d+)\+(\w+)"
)
match = re.match(pattern, media_type)
if match:
return {
"resource": match.group(1),
"version": int(match.group(2)),
"format": match.group(3),
}
return None
def build_accept_header(self, resource: str,
version: int,
fmt: str = "json") -> str:
return (f"application/{self.vendor_prefix}."
f"{resource}.v{version}+{fmt}")
parser = AcceptHeaderParser()
result = parser.parse_accept(
"application/vnd.durga.scan.v2+json"
)
print(f"Parsed: {result}")
header = parser.build_accept_header("scan", 2)
print(f"Header: {header}")
Content Negotiation Router
Route requests based on Accept header version.
from typing import Dict, Optional, Callable
class ContentNegotiationRouter:
def __init__(self, vendor_prefix: str = "vnd.api"):
self.vendor_prefix = vendor_prefix
self.handlers: Dict[str, Dict[int, Callable]] = {}
def register(self, resource: str,
version: int,
handler: Callable):
if resource not in self.handlers:
self.handlers[resource] = {}
self.handlers[resource][version] = handler
def route(self, path: str, accept: str,
request: Dict) -> Dict:
parsed = self._parse_accept(accept)
if not parsed:
return {"status": 406,
"body": {"error": "Media type not acceptable"}}
resource = parsed["resource"]
version = parsed["version"]
resource_handlers = self.handlers.get(resource)
if not resource_handlers:
return {"status": 406,
"body": {"error": f"Unknown resource: {resource}"}}
handler = resource_handlers.get(version)
if not handler:
return {"status": 406,
"body": {"error": f"Version {version} not supported"}}
return handler(request)
def _parse_accept(self, accept: str) -> Optional[Dict]:
import re
pattern = (
rf"application/{self.vendor_prefix}\."
rf"(\w+)\.v(\d+)\+(\w+)"
)
for mt in accept.split(","):
match = re.match(pattern, mt.strip())
if match:
return {
"resource": match.group(1),
"version": int(match.group(2)),
"format": match.group(3),
}
return None
cnr = ContentNegotiationRouter()
cnr.register("scan", 1, lambda r: {
"status": 200, "body": {"version": "v1"}})
cnr.register("scan", 2, lambda r: {
"status": 200, "body": {"version": "v2"}})
result = cnr.route("/api/scan",
"application/vnd.api.scan.v2+json", {})
print(f"Content negotiated: {result['body']['version']}")
Response Serialization by Version
Serialize responses differently based on version.
from typing import Dict, Any, Optional, Callable
import json
class VersionedSerializer:
def __init__(self):
self.serializers: Dict[str, Dict[int, Callable]] = {}
def register(self, resource: str,
version: int,
serializer: Callable):
if resource not in self.serializers:
self.serializers[resource] = {}
self.serializers[resource][version] = serializer
def serialize(self, resource: str,
version: int,
data: Dict) -> str:
resource_serializers = self.serializers.get(resource)
if not resource_serializers:
return json.dumps(data)
serializer = resource_serializers.get(version)
if not serializer:
serializer = resource_serializers.get(
max(resource_serializers.keys())
)
if serializer:
return serializer(data)
return json.dumps(data)
def v1_scan_serializer(self, data: Dict) -> str:
return json.dumps({
"scan_id": data.get("id"),
"status": data.get("state"),
})
def v2_scan_serializer(self, data: Dict) -> str:
return json.dumps({
"id": data.get("id"),
"state": data.get("state"),
"result": data.get("result"),
})
ser = VersionedSerializer()
ser.register("scan", 1, ser.v1_scan_serializer)
ser.register("scan", 2, ser.v2_scan_serializer)
data = {"id": "abc", "state": "completed", "result": "clean"}
v1_result = ser.serialize("scan", 1, data)
v2_result = ser.serialize("scan", 2, data)
print(f"V1: {v1_result}")
print(f"V2: {v2_result}")
Common Mistakes
Mistake 1: Non-Standard Media Types
Custom media types should follow the vendor tree convention: application/vnd.company.resource.vN+format.
Mistake 2: Ignoring Quality Values
Accept headers can include quality values (q=0.9). Parse and respect them for proper negotiation.
Mistake 3: No 406 Response
When no acceptable version is found, return 406 Not Acceptable with the list of supported media types.
Mistake 4: Case Sensitivity
HTTP headers are case-insensitive but media type parameters may not be. Normalize to lowercase.
Mistake 5: Breaking Cacheability
Content negotiation requires Vary: Accept for proper Caching. Without it, caches serve wrong versions.
Practice Questions
- What is the format of a custom media type for versioning?
- How does the Vary: Accept header affect caching?
- What HTTP status code indicates an unacceptable version?
- How do quality values in Accept headers work?
- Why do custom media types follow the vendor tree convention?
Challenge
Build an Accept header versioning system that parses custom media types (application/vnd.api.resource.vN+json), routes to the correct version handler, serializes responses appropriately per version, returns 406 for unsupported versions, and sets Vary: Accept for proper caching.
FAQ
Mini Project
Build a complete Accept header versioning system with custom media type Parsing, versioned response serializers, a Vary: Accept header policy, proper 406 error handling for unsupported versions, and support for multiple API resources.
What's Next
Learn about Custom Header Versioning for a simpler header-based approach, or explore Media Type Versioning for deeper content negotiation patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro