冒頭まとめ

先に結論を述べます。GCP の窓口は、検証に落ちた要求に対して 422 を返しません。Google が公開しているエラー区分の定義ファイルには17の区分があり、それぞれに対応する HTTP の状態コードが併記されていますが、422 は1か所も出てきません。

代わりに使われるのは 400 です。しかも、400 に対応する区分は1つではなく3つあります。引数が不正な場合、対象の現在の状態がその操作を許さない場合、そして値が許容範囲の外にある場合です。他のサービスが 400 と 422 で表現し分けている区別を、GCP は状態コードではなく区分名で表現している、と考えると分かりやすくなります。

したがって、GCP で入力の誤りを追うときに見るべきは、状態コードではなく応答に含まれる区分名です。ここを読まないと、400 が返ってきたという事実だけでは原因を1つに絞れません。

そして、実際に GCP の宛先から 422 を受け取った場合、それを作ったのは GCP の窓口ではありません。前段のプロキシか、GCP 上で動いている自作あるいは第三者のアプリケーションです。この場合、GCP の設定を調べても答えは出ません。

エラーの概要

区分と状態コードの対応は、定義ファイルにそのまま書かれています。検証に関わる3つを抜き出すと、次のようになります。

引数が不正な場合の区分は、対応する状態コードが 400 です。定義の説明では、系の状態に関係なく問題のある引数、たとえば形式の壊れた名前などを指す、とされています。

対象の状態が操作を許さない場合の区分も、対応は 400 です。空でないディレクトリ削除しようとした場合が例として挙げられており、系の状態が変われば成功しうる、という性質を持ちます。

値が許容範囲の外にある場合の区分も、対応は 400 です。読み取りの開始位置が終端を越えている場合などが該当します。

つまり、応答だけを見ると次の形になり、code の値は3つとも同じです。

{
  "error": {
    "code": 400,
    "message": "Request contains an invalid argument.",
    "status": "INVALID_ARGUMENT"
  }
}

区別できるのは status の値だけです。ここが INVALID_ARGUMENT なのか FAILED_PRECONDITION なのか OUT_OF_RANGE なのかで、次にやることが変わります。

まず最初に:statusの値を読む

第一に、状態コードが 400 であることを確認します。GCP で入力の誤りを疑う場面では、まずここに落ちてきます。

第二に、status の値を読みます。INVALID_ARGUMENT なら送った値そのものが不正で、何度送っても同じです。FAILED_PRECONDITION なら値は正しく、対象の現在の状態が問題です。状態を整えれば同じ要求が通ります。OUT_OF_RANGE なら、値の形式は正しいが範囲の外です。

第三に、message を読みます。どの項目が問題かは、多くの場合ここに書かれています。区分名で大枠を掴み、文言で具体を掴む、という二段構えになります。

第四に、それでも 422 が返ってきている場合は、応答の形を見ます。上記のような区分名を含む構造になっていなければ、作ったのは GCP の窓口ではありません。

よくある原因と解決手順

原因1:422を探しているが、実際に返っているのは400

最も多い形です。他のサービスの経験から 422 を探してしまい、見つからずに手が止まります。

対処は、探す対象を切り替えることです。GCP における検証の失敗は 400 なので、そちらの調べ方に移ってください(GCP の 400 の記事)。

確認は、応答をそのまま表示すれば済みます。

# 応答の全体を確認する(code と status の両方を見る)
curl -sS -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{"invalid": "payload"}' \
  https://<対象のサービス>.googleapis.com/v1/<対象の資源> | python3 -m json.tool

原因2:値ではなく、対象の状態が問題

statusFAILED_PRECONDITION の場合です。送った値に誤りはありません。対象が今その操作を受け付けられる状態にない、という意味です。

Before(値を疑って何度も送り直す):

gcloud <サービス> <操作> --arg=value
# → 何度実行しても同じ 400 が返る

After(対象の現在の状態を先に確認する):

gcloud <サービス> describe <対象> --format="yaml(state, status)"

対象が作成中や削除中であれば、完了を待てば通ります。依存する別の資源が未作成であれば、そちらを先に用意します。この区分は、待つか順序を変えるかで解決する種類のものです。値をいじっても変わりません。

原因3:値が範囲の外にある

statusOUT_OF_RANGE の場合です。形式としては正しいが、許される範囲を超えています。一覧の取得で開始位置を指定する場合や、大きさや個数に上限がある場合に現れます。

対処は、上限を文書で確認し、それに合わせることです。一覧の取得であれば、開始位置ではなく続きを示す値を使う方式に切り替えると、範囲の管理を自分で行わずに済みます。

原因4:422を作っているのは前段か、自分のアプリケーション

応答の形が GCP の区分を含んでいない場合です。この場合、考えられる出どころは2つあります。

1つ目は、前段に置いた中継役です。要求を検査して弾く仕組みを入れている場合、その仕組みが 422 を返すことがあります。応答の中身を見れば、GCP の形式でないことが分かります。

2つ目は、自分または第三者が作ったアプリケーションです。GCP 上で動かしているだけで、応答を作っているのは GCP ではありません。この場合、状態コードの意味はそのアプリケーションの実装次第です。

# 応答の本文をそのまま見て、どの形式かを判断する
curl -sS -i https://<対象の宛先>/<パス> | head -30

GCP の形式であれば error の下に codemessagestatus が並びます。並んでいなければ、調べる相手は GCP ではありません。

補足:似ているが別のもの

同じ 400 でも、区分によって対処が違うのは前述のとおりです。加えて、既に同じものが存在する場合は 409、対象が見つからない場合は 404 になります(GCP の 404 の記事)。権限が足りない場合は 403 で、要求の内容の問題ではありません(GCP の 403 の記事)。

要求の頻度が上限を超えた場合は 429 です(GCP の 429 の記事)。範囲の超過と頻度の超過は別物なので、区分名で見分けてください。

なお、502 も同じく区分の定義に存在しません。GCP で 502 を受け取った場合も、作ったのは窓口ではなく前段です(GCP の 502 の記事)。定義に無い状態コードが返ってきたら前段を見る、という判断は共通して使えます。

切り分けの順序

  1. 返っている状態コードを確認する。400 なら GCP の窓口が検証で弾いている。
  2. status の値を読む。ここで大枠が決まる。
  3. INVALID_ARGUMENT なら送った値そのものを直す。何度送っても結果は変わらない。
  4. FAILED_PRECONDITION なら対象の状態を確認する。待つか、順序を変えるかで解決する。
  5. OUT_OF_RANGE なら上限を文書で確認し、範囲に収める。
  6. 422 が返っている場合は、応答の形を見る。区分名が無ければ、作ったのは GCP ではない。
  7. 前段に検査の仕組みを置いているなら、そちらの設定を確認する。

確認コマンド集

# 1. 応答の全体を整形して見る(code と status を同時に確認する)
curl -sS -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d @request.json \
  https://<対象のサービス>.googleapis.com/v1/<対象の資源> | python3 -m json.tool

# 2. status の値だけを取り出す
curl -sS ... | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['error']['status'], '|', d['error']['message'])"

# 3. 対象の現在の状態を確認する(FAILED_PRECONDITION のとき)
gcloud <サービス> describe <対象> --format="yaml(state, status)"

# 4. コマンドが実際に送っている内容を確認する
gcloud <サービス> <操作> --log-http 2>&1 | grep -A20 "== body start =="

# 5. 応答の形式から、作った相手を判断する
curl -sS -i https://<対象の宛先>/<パス> | head -30

Editor’s Note

この記事は、他のエラーの記事とは性質が違います。「そのエラーは起きない」という結論から始まるためです。しかし、これは調べ物として無駄ではありません。むしろ、探す対象が違うと分かるまでの時間が、いちばん失われやすい時間です。

定義ファイルを眺めて分かるのは、GCP が状態コードを絞り込んで使っているということです。17の区分に対して、使われている状態コードは12種類しかありません。500 に対応する区分は3つ、400 に対応する区分も3つ、409 に対応する区分は2つあります。つまり、状態コードだけでは区分に戻せない設計になっています。

この設計は、状態コードで細かく表現する流儀とは逆向きです。422 を使う流儀は、400 では粗すぎるという判断から生まれました。GCP は粗さを状態コードの側に残し、細かさを区分名の側に置きました。どちらが優れているかという話ではなく、読み方が違うというだけです。

したがって、GCP を扱うときに身に付けるべき習慣は1つです。状態コードを見たら、必ず status も見る。この2つを組で読むようになれば、422 が無いことは不便ではなくなります。


免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。