- Add deprecation middleware for warning headers on sunset endpoints - Add jails_v2 router for API v2 migration path - Update CI workflow with new test coverage - Update API versioning documentation - Remove completed tasks from Tasks.md
108 lines
3.5 KiB
Python
108 lines
3.5 KiB
Python
"""Deprecation header middleware for versioned API endpoints.
|
|
|
|
Adds ``Deprecation``, ``Sunset``, and ``Link`` response headers to endpoints
|
|
that have been scheduled for removal, following RFC 8599 and the BanGUI
|
|
API_VERSIONING.md lifecycle policy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime # noqa: TC003 # Used in stringized type annotations (future annotations)
|
|
from typing import TYPE_CHECKING
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response as StarletteResponse
|
|
|
|
|
|
class DeprecatedEndpoint:
|
|
"""Describes a deprecated API endpoint and its removal schedule."""
|
|
|
|
__slots__ = ("path_prefix", "sunset_date", "successor_url")
|
|
|
|
def __init__(
|
|
self,
|
|
path_prefix: str,
|
|
sunset_date: datetime,
|
|
successor_url: str | None = None,
|
|
) -> None:
|
|
self.path_prefix = path_prefix
|
|
self.sunset_date = sunset_date
|
|
self.successor_url = successor_url
|
|
|
|
|
|
# Registry of deprecated endpoints.
|
|
# Add entries here when an endpoint is scheduled for removal.
|
|
# sunset_date must be a timezone-aware datetime in UTC.
|
|
_DEPRECATED_ENDPOINTS: list[DeprecatedEndpoint] = []
|
|
|
|
|
|
def register_deprecated_endpoint(
|
|
path_prefix: str,
|
|
sunset_date: datetime,
|
|
successor_url: str | None = None,
|
|
) -> None:
|
|
"""Register a deprecated endpoint for deprecation header injection."""
|
|
_DEPRECATED_ENDPOINTS.append(
|
|
DeprecatedEndpoint(path_prefix, sunset_date, successor_url)
|
|
)
|
|
|
|
|
|
def _is_deprecated(path: str) -> DeprecatedEndpoint | None:
|
|
for endpoint in _DEPRECATED_ENDPOINTS:
|
|
if path.startswith(endpoint.path_prefix):
|
|
return endpoint
|
|
return None
|
|
|
|
|
|
def _format_rfc5322(dt: datetime) -> str:
|
|
return dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
|
|
|
|
class DeprecationHeaderMiddleware(BaseHTTPMiddleware):
|
|
"""Inject deprecation headers on responses from deprecated endpoints.
|
|
|
|
For any response from a path registered in ``_DEPRECATED_ENDPOINTS``,
|
|
this middleware appends:
|
|
|
|
- ``Deprecation: true``
|
|
- ``Sunset: <RFC-5322 date>``
|
|
- ``Link: <<successor_url>>; rel="successor-version"`` (if successor_url set)
|
|
|
|
The middleware runs after the response is generated so it has access
|
|
to the final status code and can choose to only add headers for 2xx
|
|
responses (non-error responses from a deprecated endpoint are what
|
|
clients need to be warned about).
|
|
"""
|
|
|
|
async def dispatch(
|
|
self,
|
|
request: Request,
|
|
call_next: Callable[[Request], Awaitable[StarletteResponse]],
|
|
) -> StarletteResponse:
|
|
response: StarletteResponse = await call_next(request)
|
|
|
|
# Add deprecation headers for 2xx and 3xx responses from deprecated paths.
|
|
# Skipping 4xx/5xx avoids polluting error responses with deprecation headers.
|
|
if response.status_code < 200 or response.status_code >= 400:
|
|
return response
|
|
|
|
deprecated = _is_deprecated(request.url.path)
|
|
if deprecated is None:
|
|
return response
|
|
|
|
# All deprecation dates are stored in UTC.
|
|
sunset_str = _format_rfc5322(deprecated.sunset_date)
|
|
|
|
response.headers["Deprecation"] = "true"
|
|
response.headers["Sunset"] = sunset_str
|
|
|
|
if deprecated.successor_url:
|
|
response.headers["Link"] = f'<{deprecated.successor_url}>; rel="successor-version"'
|
|
|
|
return response
|