tiime / api-deprecation-bundle
Symfony bundle for managing API endpoint deprecation with Deprecation, Sunset and Link HTTP headers, including brownout support
Package info
github.com/Tiime-Software/ApiDeprecationBundle
Type:symfony-bundle
pkg:composer/tiime/api-deprecation-bundle
Requires
- php: >=8.3
- dragonmantank/cron-expression: ^3.3
- symfony/config: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/event-dispatcher-contracts: ^3.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
Requires (Dev)
- infection/infection: *
- innmind/black-box: ^6.10
- nelmio/api-doc-bundle: ^5.9
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^10.0|^11.0
- psr/clock: ^1.0
- symfony/clock: ^6.4|^7.0|^8.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
Suggests
- nelmio/api-doc-bundle: For automatic OpenAPI documentation of deprecated endpoints
- dev-main
- dev-fix/duplicate-link-header
- dev-tech/rtk
- dev-rename-gone-after-sunset
- dev-feature/custom-error-response
- dev-fix/msi
- dev-feature/retry-after-header
- dev-feature/logging-integration
- dev-feature/percentage-brownout
- dev-feat_nelmio_doc_bridge
- dev-add-symfony-8-support
- dev-add-mutation-testing-infection
- dev-feat_property_based_testing
- dev-feat/phpstan
- dev-feat/github-actions-ci
This package is auto-updated.
Last update: 2026-03-13 16:39:15 UTC
README
Symfony bundle for managing API endpoint deprecation via standard HTTP headers, with brownout support (planned interruptions).HTTP Headers
| Header | RFC | Description |
|---|---|---|
Deprecation |
RFC 9745 | Indicates when the endpoint was deprecated |
Sunset |
RFC 8594 | Indicates when the endpoint will be removed |
Link |
RFC 9745 §3 | Points to deprecation documentation (rel="deprecation") |
Retry-After |
RFC 7231 §7.1.3 | Seconds until brownout window ends (brownout 410 responses only) |
Installation
composer require tiime/api-deprecation-bundle
Configuration
# config/packages/api_deprecation.yaml api_deprecation: enabled: true gone_after_sunset: true # return 410 Gone after the sunset date brownout_strategies: progressive: phases: - starts_before: '30 days' # activates 30 days before sunset cron: '0 10 * * 1' # every Monday at 10am duration: 15 # for 15 minutes - starts_before: '14 days' # activates 14 days before sunset cron: '0 */4 * * *' # every 4 hours duration: 30 # for 30 minutes - starts_before: '7 days' # activates 7 days before sunset cron: '0 * * * *' # every hour duration: 45 # for 45 minutes
Usage
Simple deprecation
Add the Deprecation header to an endpoint:
use Tiime\ApiDeprecationBundle\Attribute\ApiDeprecated; class UserController { #[ApiDeprecated(since: '2024-06-01')] public function list(): Response { // ... } }
Response:
HTTP/1.1 200 OK
Deprecation: @1717200000
With sunset and link to deprecation documentation
#[ApiDeprecated(
since: '2024-06-01',
sunset: '2025-01-01',
link: 'https://docs.example.com/api/v1/users-deprecation',
)]
public function list(): Response
{
// ...
}
Response:
HTTP/1.1 200 OK
Deprecation: @1717200000
Sunset: Wed, 01 Jan 2025 00:00:00 GMT
Link: <https://docs.example.com/api/v1/users-deprecation>; rel="deprecation"; type="text/html"
On an entire controller
The attribute can be placed on the class. Methods without their own attribute inherit from the class:
#[ApiDeprecated(since: '2024-06-01', sunset: '2025-06-01')] class LegacyUserController { public function list(): Response { /* ... */ } public function show(): Response { /* ... */ } }
A method-level attribute always takes priority over the class-level one.
Brownouts
Brownouts are planned, temporary interruptions of a deprecated endpoint. During a brownout window, the endpoint returns 410 Gone instead of the normal response. This forces API consumers to migrate to the new endpoint.
A brownout strategy is composed of phases. Each phase defines a cron expression, a duration, and how long before sunset it activates. This allows you to progressively increase pressure on consumers as the sunset date approaches.
Defining a strategy
api_deprecation: brownout_strategies: progressive: phases: - starts_before: '30 days' cron: '0 10 * * 1' # Monday at 10am duration: 15 - starts_before: '7 days' cron: '0 */2 * * *' # every 2 hours duration: 30
Referencing the strategy in the attribute
The brownout parameter references the name of a strategy defined in the configuration. A sunset date is required when a brownout is configured (phases activate relative to that date).
#[ApiDeprecated(
since: '2024-06-01',
sunset: '2025-01-01',
link: 'https://docs.example.com/api/v1/users-deprecation',
brownout: 'progressive',
)]
public function list(): Response
{
// ...
}
Behavior during a brownout
During the brownout window, the response is:
HTTP/1.1 410 Gone
Retry-After: 540
Deprecation: @1717200000
Sunset: Wed, 01 Jan 2025 00:00:00 GMT
Link: <https://docs.example.com/api/v1/users-deprecation>; rel="deprecation"; type="text/html"
This endpoint is deprecated and currently unavailable (brownout).
Outside the window, the endpoint works normally with deprecation headers.
Behavior after sunset
If gone_after_sunset is enabled (default), the endpoint permanently returns 410 Gone once the sunset date has passed.
Customizing the error response
Two Symfony events are dispatched before the default 410 Gone response is returned:
ApiSunsetEvent— dispatched when the sunset date has passedApiBrownoutEvent— dispatched during a brownout window
An event listener can call $event->setResponse() to replace the default 410 with a custom response. Both events expose the ApiDeprecated attribute and the deprecation headers that will be added to the response.
Example: returning a Problem Details response (RFC 9457)
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\JsonResponse; use Tiime\ApiDeprecationBundle\Event\ApiBrownoutEvent; use Tiime\ApiDeprecationBundle\Event\ApiSunsetEvent; #[AsEventListener] final class DeprecationProblemDetailsListener { public function __invoke(ApiSunsetEvent|ApiBrownoutEvent $event): void { $response = new JsonResponse([ 'type' => 'https://docs.example.com/errors/gone', 'title' => 'Gone', 'status' => 410, 'detail' => sprintf( 'This endpoint was deprecated on %s and is no longer available.', $event->attribute->since, ), ], 410, ['Content-Type' => 'application/problem+json']); $event->setResponse($response); } }
The $event->headers array contains the computed deprecation headers (Deprecation, Sunset, Link, Retry-After) — they are automatically added to whatever response is returned.
If no listener sets a response, the default 410 Gone with a plain-text body is returned (backward compatible).
Attribute parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
since |
string|null |
null |
Deprecation date (ISO 8601) |
sunset |
string|null |
null |
Removal date (ISO 8601) |
link |
string|null |
null |
URL to deprecation documentation (RFC 9745 §3) |
brownout |
string|null |
null |
Name of a brownout strategy defined in the configuration |
Requirements
- PHP >= 8.3
- Symfony 6.4 or 7.x or 8.x
Tests
docker compose run --rm php vendor/bin/phpunit
License
MIT