| 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) |
composer require tiime/api-deprecation-bundle# 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 minutesAdd 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
#[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"
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 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.
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: 30The 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
{
// ...
}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.
If gone_after_sunset is enabled (default), the endpoint permanently returns 410 Gone once the sunset date has passed.
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.
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).
| 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 |
- PHP >= 8.3
- Symfony 6.4 or 7.x or 8.x
docker compose run --rm php vendor/bin/phpunitMIT