REST原則に従った非同期APIの設計

# はじめに

ある程度時間のかかる重い処理をするAPIを同期的に処理をしてしまうと、そのリクエストがスレッドを占有してしまい、他の軽いAPIに影響を与えてしまうことがあります。そこで、先日、RESTに従った非同期なAPIを作りたくて調べていた時に、皆それぞれ独自に設計をしていて、どう設計するのが良いのか迷ったため、その時に考察したことをまとめます。

# 同期的に処理をする場合

まず、RESTに従った同期APIの設計について考えます。ここではユーザを作成する場合を例にしました。同期APIについては、ほとんどの場合、以下のような設計になっていました。

はじめに、`/users` に対して、`POST`リクエストを送ります。レスポンスにはステータスコード`201 Created`に、HTTPヘッダーの`Location`には作られたリソースへ参照するためのパスが入ります。

```
$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 201 Created
< Location: /users/<user_id>
<
```

Locationヘッダーのパスにアクセスすると、作られたユーザの情報が取得できます。

```
$ curl -v -X GET http://api.example.com/users/<user_id>
< HTTP/1.1 200 OK
<
{
    "id": <user_id>,
    "name": "yuto425",
    "gender": "male"
}
```

# 非同期に処理をする場合

## はじめのリクエスト

同期的に処理をする場合は、多くの場合上記のような設計がされているようでしたが、非同期の場合はどのようにするのが良いでしょうか。調べていくと、以下のようにステータスコード`202 Accepted`と、HTTPヘッダーのLocationにバックグラウンド処理のステータスを参照するためのパスを入れるケースが多く見受けられました。ステータスコード`202 Accepted`はリクエストを受け取ったが処理はされていないということを表します。

```
$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
< Location: /tasks/<task_id>
<
```

例えば、以下など

* [REST and long-running jobs](https://farazdagi.com/2014/rest-and-long-running-jobs/)

ステータスコード `202 Accepted`については問題ないかと思いますが、`Location`ヘッダーを使うことが正しいのか疑問に思うところがあったので、調べていくと以下のDelmo氏の回答が参考になりました。ここでは「ステータスコード `202 Accepted`に`Location`ヘッダーを使うのはRFCに準拠していないため、含めるべきではない。」と書いてあります。ここで `202 Accepted`に`Location`をつけると言うことは、この`Location`ヘッダーはRFCに準拠したヘッダーではなく、独自ヘッダーと言うことになります。

* [Is the use of Location header in HTTP 202 response RFC-compliant?](https://stackoverflow.com/questions/26199228/is-the-use-of-location-header-in-http-202-response-rfc-compliant)

他の方法としては、レスポンスのBodyや独自ヘッダーにIDを含める方法がありました。

レスポンスBodyにIDを含める方法

* [Oracle Cloud Infrastructure Documentation - Asynchronous Work Requests](https://docs.cloud.oracle.com/iaas/Content/API/Concepts/workrequests.htm)

```
$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
<
{
    "task_id": <task_id>
}
```

独自ヘッダーにIDを含める方法

* [Google Cloud Speech-To-Text Documentation](https://cloud.google.com/speech-to-text/docs/async-recognize#speech-async-recognize-gcs-protocol)

```
$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
< X-Task-Id: <task_id>
<
```

`Location` ヘッダーを用いる方法はRFCに準拠した`201 Created`とは違い、独自ヘッダーであるにも関わらず、そのように見えないので、誤解を招く恐れがあるので、個人的には使わない方が良いかと思いました。後の2つに関してはどちらでも良いかと思いますが、後述にレスポンスBodyに情報を入れるため、ここは揃えるために今回はレスポンスBodyにIDを入れる方法を選択しました。

## 処理待ち中のリクエスト

ユーザ情報作成のステータスを確認するためのリクエストを、先ほど取得したタスクのIDを元に送ります。結論から言うと以下のように設計しました。上からユーザ作成が処理中、処理に失敗した場合、処理が完了した場合のレスポンスです。`result`の中にそれぞれで必要な情報を含みます。

```
$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "IN_PROGRESS",
    "progress_percentage": 30,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at":  null,
    "result": null,
}
```
```
$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "FAILED",
    "progress_percentage": null,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at":  null,
    "result": {
        "message": "error message is here"
    }
}
```
```
$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "SUCCEEDED"
    "progress_percentage": 100,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at": 2017-10-13T17:23:30.000Z,
    "result": {
        "user_id": <user_id>
    }
}
```

以下のサイトではユーザ作成が完了した場合、処理中や処理失敗時と同様に`200 OK`を返すのではなく、`303 See Other` を使って、作られたユーザにリダイレクトをする方法が取られていました。ここにおいては、`GET /tasks/<task_id>`に対してリクエストをした場合に返すべきは、そのtaskがどうなっているかであって、その結果生まれた副産物を返すのはRESTの原則としてどうなのかと思い、今回は却下しました。

* [REST and long-running jobs](https://farazdagi.com/2014/rest-and-long-running-jobs/)

## 最後のリクエスト

最後は受け取った`GET /users/<user_id>`に対してリクエストを送って、作られたリソースを取得するだけです。

```
$ curl -v -X GET http://api.example.com/users/<user_id>
< HTTP/1.1 200 OK
<
{
    "id": <user_id>,
    "name": "yuto425",
    "gender": "male"
}
```

# まとめ

調べていると色々な方法があったのですが、一番しっくりきた方法を今回は選びました。もっと良い方法があれば、是非教えていただきたいです。

# 参考

* [REST and long-running jobs](https://farazdagi.com/2014/rest-and-long-running-jobs/)
* [Is the use of Location header in HTTP 202 response RFC-compliant?](https://stackoverflow.com/questions/26199228/is-the-use-of-location-header-in-http-202-response-rfc-compliant)
* [Oracle Cloud Infrastructure Documentation - Asynchronous Work Requests](https://docs.cloud.oracle.com/iaas/Content/API/Concepts/workrequests.htm)
* [Google Cloud Speech-To-Text Documentation](https://cloud.google.com/speech-to-text/docs/async-recognize#speech-async-recognize-gcs-protocol)
* [HTTP response status codes
](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
* [REST API Tutorial - HTTP Status 202 (Accepted)](https://restfulapi.net/http-status-202-accepted/)
* [MDN Web Docs - Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)