feat(backend): add deprecation middleware and API versioning support
- 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
This commit is contained in:
107
backend/app/middleware/deprecation.py
Normal file
107
backend/app/middleware/deprecation.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user