From 3f0d5c8c964abc41f1829684e6694b1fd42d76a3 Mon Sep 17 00:00:00 2001 From: 1445043649 <> Date: Sun, 28 Jun 2026 19:37:36 +0800 Subject: [PATCH] feat(gateway): X-API-Deprecated header for legacy /api/* paths (Stage 1 PR5) Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/gateway/app.py | 4 ++ backend/app/gateway/deprecation_middleware.py | 31 +++++++++++++ backend/tests/test_api_deprecation_header.py | 46 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 backend/app/gateway/deprecation_middleware.py create mode 100644 backend/tests/test_api_deprecation_header.py diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 4b559e03..c1afb214 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.gateway.auth_middleware import AuthMiddleware from app.gateway.config import get_gateway_config from app.gateway.csrf_middleware import CSRFMiddleware +from app.gateway.deprecation_middleware import ApiDeprecationMiddleware from app.gateway.deps import langgraph_runtime from app.gateway.routers import ( agents, @@ -356,6 +357,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an # CSRF: Double Submit Cookie pattern for state-changing requests app.add_middleware(CSRFMiddleware) + # Deprecation: stamp X-API-Deprecated on unversioned /api/* responses + app.add_middleware(ApiDeprecationMiddleware) + # CORS: when GATEWAY_CORS_ORIGINS is set (dev without nginx), add CORS middleware. # In production, nginx handles CORS and no middleware is needed. cors_origins_env = os.environ.get("GATEWAY_CORS_ORIGINS", "") diff --git a/backend/app/gateway/deprecation_middleware.py b/backend/app/gateway/deprecation_middleware.py new file mode 100644 index 00000000..87908583 --- /dev/null +++ b/backend/app/gateway/deprecation_middleware.py @@ -0,0 +1,31 @@ +"""Marks responses to legacy unversioned /api/* paths as deprecated. + +Stamps ``X-API-Deprecated: `` on any /api/* response that is +neither versioned (/api/v1/*) nor the LangGraph SDK surface +(/api/langgraph/*). Sunset date is the track-2 contract (2027-01-01). +""" + +from __future__ import annotations + +from collections.abc import Callable + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import ASGIApp + +API_SUNSET_DATE = "2027-01-01" + + +def _is_deprecated_path(path: str) -> bool: + return path.startswith("/api/") and not path.startswith("/api/v1/") and not path.startswith("/api/langgraph/") + + +class ApiDeprecationMiddleware(BaseHTTPMiddleware): + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + response = await call_next(request) + if _is_deprecated_path(request.url.path): + response.headers["X-API-Deprecated"] = API_SUNSET_DATE + return response diff --git a/backend/tests/test_api_deprecation_header.py b/backend/tests/test_api_deprecation_header.py new file mode 100644 index 00000000..eb68caf6 --- /dev/null +++ b/backend/tests/test_api_deprecation_header.py @@ -0,0 +1,46 @@ +"""Deprecation header tests (Stage 1 PR5).""" + +from __future__ import annotations + +from starlette.testclient import TestClient + + +def _make_app(): + from fastapi import FastAPI + + from app.gateway.deprecation_middleware import ApiDeprecationMiddleware + + app = FastAPI() + app.add_middleware(ApiDeprecationMiddleware) + + @app.get("/api/threads") + async def legacy(): + return {"ok": True} + + @app.get("/api/v1/threads") + async def versioned(): + return {"ok": True} + + @app.get("/api/langgraph/info") + async def lg(): + return {"ok": True} + + return app + + +def test_legacy_path_gets_deprecation_header(): + client = TestClient(_make_app()) + r = client.get("/api/threads") + assert r.headers.get("X-API-Deprecated") == "2027-01-01" + + +def test_versioned_path_no_header(): + client = TestClient(_make_app()) + r = client.get("/api/v1/threads") + assert "X-API-Deprecated" not in r.headers + + +def test_langgraph_path_no_header(): + client = TestClient(_make_app()) + r = client.get("/api/langgraph/info") + assert "X-API-Deprecated" not in r.headers