リクエストの構造

メソッド、URL、ヘッダー、ボディ、そしてサーバーが実際に受け取るもの。

前のレッスンでは、ブラウザに URL を入力してリクエストを送りました。残りはすべて ブラウザが埋めてくれました。このレッスンでは、リクエストを分解して各部分が何なのかを確かめます。これから書くテストは どれも、これらの部分のいくつかを意図して設定し、サーバーの反応を確かめるものだからです。

HTTP リクエストは 4 つの部分からなります。

  • メソッド:どんな種類の操作をしたいか
  • URL:どのサーバーの、どの対象に対して操作するか
  • ヘッダー:リクエストについての追加情報
  • ボディ(任意):送るデータ

通信路の上のリクエストの姿

HTTP/1.1 はテキストのプロトコルです。暗号化される前のリクエストは、数行のプレーンテキストにすぎません。次は curl が https://api.testingapis.com/api/echo?color=red に送るリクエストです。curl を -v(詳細表示)付きで実行すれば 自分でも見られます。> で始まる行が送信された内容です。

生のリクエスト
GET /api/echo?color=red HTTP/1.1
Host: api.testingapis.com
User-Agent: curl/8.18.0
Accept: */*

1 行目はリクエスト行で、メソッド、クエリ文字列付きのパス、プロトコルのバージョンが並びます。その後の各行はヘッダーで、 名前と値がコロンで区切られています。空行でヘッダーが終わり、ボディがあればその空行の後に続きます。新しい HTTP(HTTP/2 と HTTP/3)は同じ情報をバイナリ形式で送りますが、部分の構成は同じで、どのツールも今でもこのテキストの形で見せてくれます。

サーバーが受け取ったものを見る

リクエストを知るいちばん簡単な方法は、受け取ったものを教えてくれるサーバーに送ってみることです。/api/echo はまさにそれをします。どのメソッドでも受け付け、リクエストの説明を返します。次を送ってみてください。

GET/api/echo?color=red&size=10

curl から送ったときの返事を、興味深いフィールドだけに絞ったものが次です。このページから送ると、ヘッダーはブラウザのものに なります。

JSON
{
  "method": "GET",
  "url": "http://api.testingapis.com/api/echo?color=red&size=10",
  "path": "/api/echo",
  "queryString": "?color=red&size=10",
  "query": { "color": "red", "size": "10" },
  "headers": {
    "Accept": "*/*",
    "Host": "api.testingapis.com",
    "User-Agent": "curl/8.18.0",
    "Accept-Encoding": "gzip, br",
    "Cf-Ipcountry": "IN",
    "X-Forwarded-For": "203.0.113.7, 172.71.198.178, 10.30.107.46",
    "X-Forwarded-Proto": "https"
  },
  "contentType": null,
  "contentLength": 0,
  "body": null
}

ここで 2 つのことをよく見てください。サーバーは URL を https:// ではなく http:// として見ています。そして、あなたが送っていない Cf-Ipcountry や X-Forwarded-For のようなヘッダーを受け取っています。リクエストはアプリケーションに届く前に CDN とロードバランサーを通り、そのそれぞれが 何かを書き換えたり付け加えたりしたのです。X-Forwarded-For の最初のアドレスはあなたのものです(上のものは例です)。本番のシステムではこれがふつうで、エコーのエンドポイントが役立つ 理由でもあります。送ったつもりのものではなく、実際に届いたものを見せてくれるからです。

メソッド

メソッドは動詞です。GET はデータを求め、POST は何かを作るためにデータを送ります。PUT、PATCH、DELETE は変更や削除をします。ブラウザのアドレスバーは GET しか送れないので、それ以外には別のツールが必要です。 メソッドには HTTP メソッド という専用のレッスンがあるので、今は、同じ URL でもメソッドによって違うことができる、とだけ覚えておけば十分です。

URL

この URL を部分に分けてみましょう。

URL
https://api.testingapis.com/api/Products?category=books&limit=5
部分値意味
スキームhttpsどう接続するか。https は暗号化された接続の中の HTTP。
ホストapi.testingapis.comどのサーバーか。Host ヘッダーとしても送られる。
パス/api/Productsそのサーバー上のどのリソースか。
クエリ文字列category=books&limit=5? の後に & で区切って付ける追加のパラメーター。

URL にはポート(ホストの後の :8080)やフラグメント(末尾の #section)が付くこともあります。ポートは テスト環境やローカル環境でよく見かけます。フラグメントはサーバーには届かず、ブラウザが持ったままです。

パスパラメーターとクエリパラメーター

値はパスでもクエリ文字列でも運べます。/api/Products/1 の 1 はパスパラメーターで、特定の 1 つの商品を指します。ドキュメントではパスを /api/Products/{id} と書きます。/api/Products?limit=5 の limit はクエリパラメーターです。クエリパラメーターはたいてい結果の絞り込み、並べ替え、ページ分けに使われ、たいてい省略できます。 この使い分けは慣習でありルールではないので、API ごとにドキュメントを確認してください。

パスは大文字と小文字を区別することが多く、クエリパラメーターの名前もたいてい区別します。これはサーバーのフレームワーク 次第なので、新しい API では手早く試してみましょう。/api/Products の代わりに /api/products を送って、どうなるかを見ます。

パーセントエンコーディング

URL の中で役割を持つ文字があります。? はクエリ文字列の始まり、& はパラメーターの区切り、# はフラグメントの始まり、/ はパスの区切りで、スペースはそもそも使えません。これらをデータとして送るには、% とそのバイトの 16 進数の値に置き換えます。スペースは %20 に、& は %26 になります。これをパーセントエンコーディング、または URL エンコーディングと呼びます。

「red shoes」を検索し、値がそのまま a&b という文字列のタグで絞り込みたいとします。エンコードすればうまくいきます。

GET/api/echo?q=red%20shoes&tag=a%26b
一部を抜き出したレスポンス
{
  "queryString": "?q=red%20shoes&tag=a%26b",
  "query": { "q": "red shoes", "tag": "a&b" }
}

今度はアンパサンドをエンコードせずにタグを送ってみます。

GET/api/echo?tag=a&b
一部を抜き出したレスポンス
{
  "queryString": "?tag=a&b",
  "query": { "tag": "a", "b": "" }
}

サーバーには 2 つのパラメーターが見えています。値が a の tag と、b という名前の空のパラメーターです。エラーも警告もありません。リクエストは「成功」し、データは間違っています。文字列を つなげて URL を組み立てるクライアントで本当によくある種類のバグで、テストのときに検索欄へアンパサンド、プラス記号、 スラッシュ、英語以外の文字を入れてみるとよい理由でもあります。手で変換したいときは、このサイトのURL エンコーダーが使えます。

ヘッダー

ヘッダーはリクエストを説明する名前と値の組です。名前は大文字と小文字を区別しないので、content-type と Content-Type は同じ意味です。ほとんどのクライアントはいくつかのヘッダーを自動で付けます。自分で設定したり確認したりすることが多いのは 次のものです。

ヘッダー意味例
Content-Type送るボディの形式。application/json
Accept受け取ってもよい形式。application/json
User-Agentリクエストを送ったクライアント。curl/8.18.0
Authorizationトークンなどの認証情報。Bearer abc123
Hostサーバー上のどのサイトか。クライアントが URL から埋める。api.testingapis.com

Accept は、API が複数の形式で応えられるときに意味を持ちます。/api/formats/negotiateサインインして表示 は同じユーザーを、既定では JSON で、求めれば XML で返します。

GET/api/formats/negotiate
このリクエストはサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成

ヘッダーなしで受け取る JSON と XML を比べてみてください。どちらも同じ 7 つのフィールドを、それぞれの形式なりの書き方で 持っています。roles のリストは繰り返される role 要素になり、null の manager は xsi:nil の付いた空の要素になります。API が複数の形式を提供しているなら、それぞれをテストし、内容が一致すると思い込まないでください。 このレッスンを最初に書いたとき、このユーザーの XML 版には 3 つのフィールドが欠けていました。

Authorization は 認証と認可 でしっかり扱います。ひとつだけ今のうちに知っておいてください。本物のトークンをスクリーンショット、バグ報告、共有コレクション に貼り付けてはいけません。トークンはパスワードです。

ボディ

GET リクエストには通常ボディがありません。POST、PUT、PATCH はサーバーにデータを送るので、たいていボディがあります。 ボディの形式は自由で、Content-Type ヘッダーがサーバーに読み方を伝えます。今日のほとんどの API ではボディは JSON です。次は curl で小さな JSON オブジェクトをエコーのエンドポイントに送ります。

Terminal
curl -X POST https://api.testingapis.com/api/echo \
  -H "Content-Type: application/json" \
  -d '{"title":"Dune","year":1965}'
一部を抜き出したレスポンス
{
  "method": "POST",
  "path": "/api/echo",
  "query": {},
  "contentType": "application/json",
  "contentLength": 28,
  "body": "{\"title\":\"Dune\",\"year\":1965}",
  "json": { "title": "Dune", "year": 1965 }
}

サーバーはヘッダーを読んで application/json を確認し、28 バイトをオブジェクトとして解析しました。同じリクエストを ここから送ることもできます。

POST/api/echo
{"title":"Dune","year":1965}

Content-Type とボディが食い違うとき

今度は同じようなボディを curl で、ヘッダーを付けずに送ってみます。

Terminal
curl -X POST https://api.testingapis.com/api/echo \
  -d '{"title":"Dune"}'
一部を抜き出したレスポンス
{
  "contentType": "application/x-www-form-urlencoded",
  "contentLength": 16,
  "body": "{\"title\":\"Dune\"}",
  "json": null,
  "form": { "{\"title\":\"Dune\"}": "" }
}

curl の -d オプションの既定値は、HTML フォームが使う形式 application/x-www-form-urlencoded です。サーバーはヘッダーを信じてボディをフォームとして読もうとし、JSON のテキスト全体を名前にしたフォームフィールドが 1 つできあがりました。エラーは返ってきません。実際のサーバーにも同じように振る舞うものは多く、415 Unsupported Media Type を返すものもあります。Content-Type がない、または間違っているのは、リクエストが「正しく見える」のに失敗するいちばんよくある原因の 1 つです。サーバーがデータを 無視するときに最初に確認すべきところです。

ヘッダーが正しくても JSON が壊れていれば、よいサーバーはそう言います。末尾にカンマがあるボディ {"title":"Dune",} に対するエコーのレスポンスの一部が次です。

一部を抜き出したレスポンス
"json": null,
"jsonError": "The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. LineNumber: 0 | BytePositionInLine: 16."

演習

/api/echo を使って、それぞれの問いに答えてください。推測せず、毎回レスポンスを見ること。

  • ブラウザのアドレスバーから /api/echo?name=Zoë&city=São Paulo を送ってください。送る前にブラウザは URL の何を変えましたか。サーバーはそれを何にデコードしましたか。
  • /api/echo?id=1&id=2 を送ってください。2 回出てくるパラメーターを、サーバーはどう表しますか。フレームワークによって扱いが違うので、 テストするどの API にとってもよい問いです。
  • JSON のボディと Content-Type: text/plain で POST を送ってください。サーバーはそれでも JSON を解析しましたか。
  • ブラウザと curl の両方から /api/echo/user-agent を試してください。どちらを使ったかを API が気にするのはなぜでしょうか。

練習とその先へ