Sunset Headers for API Versioning
In this tutorial, you'll learn about Sunset Headers. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Sunset headers are HTTP response headers that inform clients when an API version or feature will be removed, following the proposed HTTP Sunset Header standard.
What You'll Learn
By the end of this lesson, you will implement Sunset headers, combine them with deprecation warnings, and build client-side tooling to react to scheduled removals.
Why It Matters
Sunset headers give clients an automated way to know when a version will stop working. Clients can parse these headers and schedule migrations without manual monitoring.
Real-World Use
GitHub returns Sunset headers on deprecated APIs with the date the endpoint will stop working. API management platforms like Kong support adding these headers automatically.
Sunset Header Flow
sequenceDiagram
Client->>API: GET /v1/users
API->>Client: 200 + Sunset: Sat, 31 Dec 2026
Client->>Client: Parse Sunset header
Client->>Client: Schedule Migration before date
Client->>API: GET /v2/users
API->>Client: 200 (no sunset header)
Sunset Header Implementation
# sunset_headers.py
from datetime import datetime, timezone, timedelta
from typing import Dict, Optional
class SunsetHeaderService:
def __init__(self):
self.policies: Dict[str, Dict] = {}
def set_sunset(self, version: str, sunset_date: str,
successor: str, reason: str = "deprecated"):
self.policies[version] = {
"sunset": sunset_date,
"successor": successor,
"reason": reason,
}
def build_headers(self, version: str) -> Dict[str, str]:
policy = self.policies.get(version)
if not policy:
return {}
headers = {
"Sunset": policy["sunset"],
"Link": f'<{policy["successor"]}>; rel="successor-version"',
}
sunset_dt = self._parse_date(policy["sunset"])
if sunset_dt:
days = (sunset_dt - datetime.now(timezone.utc)).days
if days <= 90:
headers["Warning"] = f'299 sunset "{days} days until removal"'
return headers
def _parse_date(self, date_str: str) -> Optional[datetime]:
formats = [
"%a, %d %b %Y",
"%Y-%m-%d",
]
for fmt in formats:
try:
return datetime.strptime(date_str, fmt).replace(tzinfo=timezone.utc)
except ValueError:
continue
return None
def days_until_sunset(self, version: str) -> Optional[int]:
policy = self.policies.get(version)
if not policy:
return None
sunset_dt = self._parse_date(policy["sunset"])
if not sunset_dt:
return None
return (sunset_dt - datetime.now(timezone.utc)).days
service = SunsetHeaderService()
service.set_sunset("v1", "2026-12-31", "/docs/v2-migration")
headers = service.build_headers("v1")
for key, value in headers.items():
print(f"{key}: {value}")
print(f"\nDays to sunset: {service.days_until_sunset('v1')}")
Expected output:
Sunset: 2026-12-31
Link: </docs/v2-migration>; rel="successor-version"
Warning: 299 sunset "<n> days until removal"
Days to sunset: <n>
Client-Side Sunset Parsing
# sunset_client.py
from datetime import datetime, timezone
from typing import Dict, Optional
class SunsetClient:
def __init__(self):
self.known_sunsets: Dict[str, datetime] = {}
self.warnings: list = []
def parse_response(self, url: str, headers: Dict):
sunset = headers.get("Sunset")
if not sunset:
return
try:
sunset_dt = datetime.strptime(sunset, "%Y-%m-%d").replace(tzinfo=timezone.utc)
except ValueError:
try:
sunset_dt = datetime.strptime(sunset, "%a, %d %b %Y").replace(tzinfo=timezone.utc)
except ValueError:
return
self.known_sunsets[url] = sunset_dt
days = (sunset_dt - datetime.now(timezone.utc)).days
if days < 0:
self.warnings.append(f"PAST: {url} sunset was {abs(days)} days ago")
elif days < 30:
self.warnings.append(f"URGENT: {url} sunsets in {days} days")
elif days < 90:
self.warnings.append(f"WARN: {url} sunsets in {days} days")
else:
self.warnings.append(f"INFO: {url} sunsets in {days} days")
client = SunsetClient()
client.parse_response("/api/v1/users", {"Sunset": "2026-12-31"})
client.parse_response("/api/v1/posts", {"Sunset": "2030-01-01"})
for w in client.warnings:
print(w)
print(f"\nTracked URLs: {len(client.known_sunsets)}")
Expected output:
INFO: /api/v1/users sunsets in <n> days
INFO: /api/v1/posts sunsets in <n> days
Tracked URLs: 2
Sunset Policy Enforcement
# sunset_enforcer.py
from datetime import datetime, timezone
from typing import Dict, Optional, Tuple
class SunsetEnforcer:
def __init__(self):
self.sunsets: Dict[str, str] = {}
def schedule(self, version: str, date_str: str):
self.sunsets[version] = date_str
def enforce(self, version: str) -> Tuple[int, Dict]:
policy = self.sunsets.get(version)
if not policy:
return 200, {"status": "active"}
try:
sunset_dt = datetime.strptime(policy, "%Y-%m-%d").replace(tzinfo=timezone.utc)
except ValueError:
return 200, {"status": "active"}
now = datetime.now(timezone.utc)
if now > sunset_dt:
return 410, {
"error": "Gone",
"message": f"Version {version} was removed on {policy}",
"successor": "/api/v2",
}
days_left = (sunset_dt - now).days
return 200, {
"status": "active",
"version": version,
"sunset": policy,
"days_left": days_left,
}
enforcer = SunsetEnforcer()
enforcer.schedule("v1", "2026-12-31")
enforcer.schedule("v0", "2020-01-01")
for ver in ["v1", "v0", "v2"]:
status, body = enforcer.enforce(ver)
print(f"{ver}: {status} - {body.get('status', body.get('error'))}")
Expected output:
v1: 200 - active
v0: 410 - Gone
v2: 200 - active
Common Mistakes
1. Using Non-Standard Date Formats
Always use RFC 1123 format (Sat, 31 Dec 2026 23:59:59 GMT) or ISO 8601 (2026-12-31) for Sunset headers.
2. No Successor Link
Clients need to know what to migrate to. Always include a Link header with rel="successor-version".
3. Not Updating Sunset Dates
If you extend a sunset deadline, update the header. Clients parse the header and may migrate unnecessarily.
4. Removing the Version on Sunset Day
Give a grace period after the sunset date. Some clients may have clock skew or delayed migration. Return 410 after the grace period.
5. Not Logging Sunset Header Delivery
Track which clients received Sunset headers to know who has been warned and who has migrated.
Practice Questions
1. What does the Sunset HTTP header indicate?
The date when an API version or feature will be removed from service.
2. What format should the Sunset header value use?
RFC 1123 format (e.g., Sat, 31 Dec 2026 23:59:59 GMT) or ISO 8601 date.
3. How do clients know the replacement for a sunset endpoint?
The Link header with rel="successor-version" provides the URL to the replacement.
4. What HTTP status code should sunset endpoints return after removal?
410 Gone, indicating the resource is permanently gone and will not return.
Challenge
Build a sunset header monitoring system that tracks all active Sunset headers across your API, sends alerts to client contacts, and validates migration completion before the sunset date.
FAQ
Mini Project: Sunset Dashboard
# sunset_dashboard.py
from datetime import datetime, timezone, timedelta
class SunsetDashboard:
def __init__(self):
self.endpoints = {}
def register(self, endpoint: str, sunset_str: str, successor: str):
dt = datetime.strptime(sunset_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
self.endpoints[endpoint] = {"sunset": dt, "successor": successor}
def summary(self):
now = datetime.now(timezone.utc)
for ep, info in sorted(self.endpoints.items(), key=lambda x: x[1]["sunset"]):
days = (info["sunset"] - now).days
status = "PAST" if days < 0 else f"{days}d left"
print(f"{ep:30s} sunset={info['sunset'].strftime('%Y-%m-%d')} {status:10s} -> {info['successor']}")
dash = SunsetDashboard()
dash.register("/api/v1/users", "2026-12-31", "/api/v2/users")
dash.register("/api/v1/posts", "2027-06-30", "/api/v2/posts")
dash.summary()
Expected output:
/api/v1/posts sunset=2027-06-30 <n>d left -> /api/v2/posts
/api/v1/users sunset=2026-12-31 <n>d left -> /api/v2/users
What's Next
You understand sunset headers. Next, learn about versioning database schemas, then explore versioning microservices.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro