はじめに

Apigee でプロキシを作っていたところ、エラー時のレスポンスにポリシー名が
そのまま出ていることに気づきどこまで出るのか気になったので確認してみました。

Apigee の既定のエラー応答とは

Apigee では、ポリシーの実行に失敗すると、特に何も設定していなくても以下のような JSON が返されます。

{“fault”:{“faultstring”:”Failed to resolve API Key variable request.queryparam.apikey”,”detail”:{“errorcode”:”steps.oauth.v2.FailedToResolveAPIKey”}}}

https://cloud.google.com/apigee/docs/api-platform/fundamentals/fault-handling?hl=ja

公式ドキュメントの例でも、API キーをどの変数から読もうとしたか(request.queryparam.apikey)が faultstring にそのまま入っています。
このように、エラーの理由がそのままクライアントに返るようです。

既定のままだと何が出るか

検証用に、エラーを既定のまま返す /v1/fault-raw と、置き換える /v1/fault-masked の 2 つのエンドポイントを用意しました。
パスごとに必ず失敗するポリシーを 1 つ置き、同じエラーを両方で起こして比べます。

まずは /v1/fault-raw で、既定のレスポンスを確認しました。
存在しないホスト(fault-exposure-check.invalid)を呼び出す ServiceCallout を実行すると、以下のレスポンスが返りました。

$ curl -s https://api.example.com/v1/fault-raw/sc-unknown-host
{"fault":{"faultstring":"Execution of ServiceCallout SC-UnknownHost failed. Reason: Unable to resolve host fault-exposure-check.invalid","detail":{"errorcode":"steps.servicecallout.ExecutionFailed"}}}

ServiceCallout のポリシー名(SC-UnknownHost)と、呼び出し先のホスト名がそのまま返っています。

次に、throw new Error("internal-detail: db=orders-primary") とだけ書いた JavaScript ポリシーを実行しました。

$ curl -s https://api.example.com/v1/fault-raw/js-error
{"fault":{"faultstring":"Execution of JS-Throw failed with error: Exception thrown from JavaScript : Error: internal-detail: db=orders-primary (throw_js#1)","detail":{"errorcode":"steps.javascript.ScriptExecutionFailed"}}}

ポリシー名とスクリプト名に加えて、throw した例外のメッセージまでそのまま返ることが確認できました。

最後に、<FaultResponse> を書いていない RaiseFault を実行しました。

$ curl -s https://api.example.com/v1/fault-raw/rf-bare
{"fault":{"faultstring":"Raising fault. Fault name : RF-Bare","detail":{"errorcode":"steps.raisefault.RaiseFault"}}}

エラーを返すためのポリシーでも、<FaultResponse> を書かないと自身のポリシー名(RF-Bare)が返ります。

FaultRule と RaiseFault とは

置き換えには、FaultRule と RaiseFault を組み合わせて使います。

  • FaultRule: エラーが起きたときに実行する処理をまとめたものです。<Condition> で対象のエラーを絞り込みます。どの FaultRule にも一致しなかったときは、DefaultFaultRule が実行されます。
  • RaiseFault: 指定したステータスコードと本文でエラーを返すポリシーです。返したい内容は <FaultResponse> に書きます。

FaultRule で「どのエラーのときに」を決め、RaiseFault で「何を返すか」を決める、という役割分担です。

RaiseFault で置き換える

ここからは /v1/fault-masked 側の設定です。
API キー関連のエラーは 401、それ以外は 500 の汎用メッセージに置き換えます。

<!-- proxies/masked.xml(抜粋) -->
<FaultRules>
  <FaultRule name="AuthFaultRule">
    <Step>
      <Name>ML-LogError</Name>
    </Step>
    <Step>
      <Name>RF-Mask401</Name>
    </Step>
    <Condition>(fault.name = "FailedToResolveAPIKey") OR (fault.name = "InvalidApiKey")</Condition>
  </FaultRule>
</FaultRules>
<DefaultFaultRule name="DefaultFaultRule">
  <Step>
    <Name>ML-LogError</Name>
  </Step>
  <Step>
    <Name>RF-Mask500</Name>
  </Step>
</DefaultFaultRule>
<!-- policies/RF-Mask500.xml -->
<RaiseFault name="RF-Mask500">
  <FaultResponse>
    <Set>
      <StatusCode>500</StatusCode>
      <ReasonPhrase>Internal Server Error</ReasonPhrase>
      <Payload contentType="application/json">{"error":"An unexpected error occurred. Please try again later."}</Payload>
    </Set>
  </FaultResponse>
</RaiseFault>

Step の先頭にある ML-LogError は、置き換える前のエラー内容(fault.name や error.message)を Cloud Logging に書き出す MessageLogging ポリシーです。
レスポンスから消すと原因を調べられなくなるため、RaiseFault の前に置いてログに残しています。

※ ログを書き出すには、roles/logging.logWriter を持つサービスアカウントを指定してデプロイする必要があります。

置き換え後のレスポンスは以下のようになりました。

$ curl -s https://api.example.com/v1/fault-masked/missing-key
{"error":"Invalid or missing API key."}

$ curl -s https://api.example.com/v1/fault-masked/js-error
{"error":"An unexpected error occurred. Please try again later."}

ProxyEndpoint で起きたエラーは、すべて汎用メッセージに置き換わることが確認できました。

注意点

<Condition> を Step 側に書くと DefaultFaultRule が実行されない

FaultRule を実行するかどうかは、FaultRule 直下の <Condition> だけで決まります。
Step 側に書いた <Condition> は判定に使われないため、FaultRule がすべてのエラーに一致し、DefaultFaultRule が一度も実行されなくなります。

明示的な条件が指定されていない FaultRule の実行条件は、暗黙的に true と評価されます。 要素の子として接続されている 要素は、FaultRule の実行条件が true と false のいずれに評価されるかの決定には使用されません。

https://cloud.google.com/apigee/docs/api-platform/fundamentals/fault-handling?hl=ja

<Condition> は FaultRule の直下に置く必要があります。

ターゲット側のエラーは ProxyEndpoint の FaultRule では置き換わらない

TargetEndpoint で起きたエラー(ターゲットのホストが存在しない等)は、ProxyEndpoint にだけ FaultRule を書いても置き換わらず、既定のレスポンスがそのまま返ります。

$ curl -s https://api.example.com/v1/fault-masked/target-nomask/down
{"fault":{"faultstring":"Unable to resolve host fault-exposure-check.invalid","detail":{"errorcode":"protocol.http.NoResolvedHost","reason":"TARGET_CONNECT_HOST_NOT_REACHABLE"}}}

FaultRule が でのみ定義されている場合、そのエラーは処理されません。

https://cloud.google.com/apigee/docs/api-platform/fundamentals/fault-handling?hl=ja

対応方法は、TargetEndpoint にも DefaultFaultRule を置くか、ProxyEndpoint の DefaultFaultRule に <AlwaysEnforce>true</AlwaysEnforce> を付けるかのどちらかです。
前者はターゲットを増やすたびに同じ設定が必要になるため、今回は後者で置き換わることを確認しました。

<!-- proxies/masked.xml(抜粋) -->
<DefaultFaultRule name="DefaultFaultRule">
  <Step>
    <Name>ML-LogError</Name>
  </Step>
  <Step>
    <Name>RF-Mask500</Name>
  </Step>
  <AlwaysEnforce>true</AlwaysEnforce>
</DefaultFaultRule>
$ curl -s https://api.example.com/v1/fault-masked/target-nomask/down
{"error":"An unexpected error occurred. Please try again later."}

AlwaysEnforce でターゲット側のエラーまで処理される点は、公式ドキュメントには明記されていないようでした。

AlwaysEnforce を付けると、FaultRule の後にも DefaultFaultRule が実行される

AlwaysEnforce を付けると、他の FaultRule が実行された後にも DefaultFaultRule が実行されます。
ただし、先に実行された FaultRule が RaiseFault を呼んでいる場合は実行されません。

DefaultFaultRule 以外の FaultRule が RaiseFault ポリシーを呼び出す場合は、 タグの 要素が true でも、DefaultFaultRule は実行されません。

https://cloud.google.com/apigee/docs/api-platform/fundamentals/fault-handling?hl=ja

AssignMessage などでレスポンスを組み立てている FaultRule では、DefaultFaultRule の RaiseFault に error.status.code >= 500 のような条件を付けておかないと、レスポンスが上書きされるおそれがあります。

StatusCode を固定すると 503 が 500 になる

RaiseFault の <StatusCode> に固定値を書くと、元のステータスコードは残りません。
ターゲットのホストが存在しない場合、既定のままなら 503 が返りますが、置き換えたあとは 500 になります。

$ curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/v1/fault-raw/target/down
503
$ curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/v1/fault-masked/target/down
500

503 を残したい場合は、503 用の RaiseFault を別に用意し、error.status.code = 503 の条件で分けるのがよさそうです。

まとめ

Apigee の既定のエラー応答には、思っていた以上に内部の情報が含まれていました。
Apigee を使い始めたら、まずエラー応答を置き換えた上で、設定するときは以下を確認しておくと良さそうです。

  • FaultRule の <Condition> は Step 側ではなく FaultRule の直下に置く
  • ターゲット側のエラーは、TargetEndpoint に DefaultFaultRule を置くか、AlwaysEnforce を付けて処理する
  • 置き換える前の内容は MessageLogging でログに残す(サービスアカウントの権限も確認する)
  • 503 などのステータスコードをどこまで残すか決めておく

参考