tiime/api-deprecation-bundle

Symfony bundle for managing API endpoint deprecation with Deprecation, Sunset and Link HTTP headers, including brownout support

Maintainers

Package info

github.com/Tiime-Software/ApiDeprecationBundle

Type:symfony-bundle

pkg:composer/tiime/api-deprecation-bundle

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 1


README

logo Tiime
API Deprecation Bundle

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 passed
  • ApiBrownoutEvent — 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