Pull to refresh

Использование Accept Header для версионирования API

Reading time 4 min
Views 17K
Original author: Willem Jan
Я исследовал различные варианты дя версионирования REST API. Большинство источников, которые я нашел, говорят практически одно и тоже. Для версионирования любого ресурса в интернете вы не должны изменять URL-адрес. Веб не версионный, и изменение URLа говорит клиенту, что есть больше чем 1 ресурс. Но на самом-то деле не существует нескольких ресурсов, это просто разные представления одного и того же. Конечно, бывают случаи, когда необходимо изменить URL, например, когда измененяется функциональность. В данном конкретном случае причиной изменения служит тот факт, что это больше не один и тот же ресурс.

Но с другой стороны, и, возможно, даже более важной, вы всегда должны стараться убедиться, что изменения будут обратно совместимы. Нужно очень хорошо проектировать архитектуру, чтобы использующие ваше API клиенты не меняли свой код. Хорошо продуманное API может спасти вас от большой, очень большой головной боли.

Использование Accept Header


Конечно, всегда есть случаи, когда ломать обратную совместимость необходимы для того, чтобы двигаться вперед. В этом случае версионирование становится важным. Метод, который я нашел, представляется наиболее логичным, он предлагает запрашивать определенные версия API с помощью заголовка Accept. В этом посте я покажу возможную реализацию с помощью маршрутизации в Symfony.

GET /jobs HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.api+json;version=2

vnd. часть продиктована требованиями rfc4288-3.2, и используется для указания типов данных, предоставляемых поставщиком. В vnd.* тип данных может быть предоставлен в IANA и зарегистрирован в качестве официального типа. В теории, вы могли бы просто использовать здесь application/json и добавить параметр версии, но так как json стандарт не допускает подобных вольностей, это будет не совсем корректное использование.

Отличным примером немного другой реализации является Github API. Они просто решили использовать последнюю версию API, если клиент указыват конкретную. Но есть хороший аргумент в пользу того, чтобы всегда требовать указания версии; когда вы по умолчанию отдаете последнюю версию апи, то в случае его обновления все те, кто по каким-то причинам не предусмотрел вероятности того, что API будет изменено, пострадают от появившейся несовместимости.

Но как?


Итак, теперь мы перейдем к реализации. Предположим, вам нужно перенаправлять запросы различных контроллеров на основе заданного в заголовке Accept. Чтобы добиться этого, вам нужно распарсить Accept и создать правила маршрутизации, которые могут использовать эту информацию.

Специально для этого я создал песочницу с очень простым API, который показывает как вы можете направлять запросы на основе запрошенной версии. Первая версия (1) данного API просто возвращает статический список заданий:

class ApiController extends Controller
{
    public function getJobsAction()
    {
        $jobs = $this->get('wjzijderveld_api.job_manager')->getJobs();

        return new Response(json_encode($jobs));
    }

    public function getJobAction($id)
    {
        $job = $this->get('wjzijderveld_api.job_manager')->getJob($id);
        return new Response(json_encode($job));
    }
}

В версии 2 я отсортировал эти задания в алфавитном порядке в зависимости от названия.

class Api2Controller extends ApiController
{
    public function getJobsAction()
    {
        $jobs = $this->get('wjzijderveld_api.job_manager')->getJobs();

        usort($jobs, function ($a, $b) {
            return strcmp($a['title'], $b['title']);
        });

        return new Response(json_encode($jobs));
    }
}

Для простоты я создал BaseBundle с кастомным роутингом. Чтобы иметь возможность переключаться на нужный контроллер, мы должны определить запрошенную версию. Для этого я расширил стандартные классы Router и RequestContext, новые классы я определил в parameters.yml.

router.class: Wjzijderveld\BaseBundle\Router\ApiRouter
router.options.matcher_class: Wjzijderveld\BaseBundle\Router\VersionMatcher
router.request_context.class: Wjzijderveld\BaseBundle\Router\RequestContext


В своем роутере я получаю заголовок Accept и выставляю версию в RequestContext. Спасибо Symfony, за то что он такой торт и имеет реализацию AcceptHeader:

public function matchRequest(Request $request)
{
    $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept'))->get($this->acceptHeader);
    // .. 
    if (null === ($version = $acceptHeader->getAttribute('version'))) {
        return $this->match($request->getPathInfo());
    }

    $this->getContext()->setApiVersion($version);

    return $this->match($request->getPathInfo());
}

С версии 2.5 вы можете использовать язык выражений для создания новых условий роутинга.

# Version 1
api1_get_jobs:
    path: "/jobs"
    defaults:
        _controller: "WjzijderveldApiBundle:Api:getJobs"
    condition: "context.getApiVersion() === '1'"

# Version 2
api2_get_jobs:
     path: "/jobs"
     defaults:
         _controller: "WjzijderveldApiBundle:Api2:getJobs"
     condition: "context.getApiVersion() === '2'"

Итоги


Управлять версиями вашего API может быть довольно трудно, особенно если вы хотите сделать правильно! Это был просто мой взгляд на часть большой проблемы под названием «Создание API», а каково Ваше мнение?

Использованные ресурсы


urthen.github.io: Part two: How to architect a version-less API
pivotallabs.com: API Versioning
troyhunt.com: Your API versioning is wrong, which is why I decided to do it 3 different wrong ways
Tags:
Hubs:
+16
Comments 20
Comments Comments 20

Articles