AJAXフォームはJSONを期待するのにHTMLが返る:応答を段階的に追う
200という応答はJSONである証拠にならない。ネットワークパネルを段階的にたどり、転送・表現・契約の失敗を切り分ける。
フォームは送信され、スピナーが出て、コンソールはJSONの位置0に予期しない記号 < があると告げる。ページ自体は問題なく読み込まれるため、まずパーサーやトンネルを疑いたくなる。だがどちらも通常は無関係だ。送信はページ読み込みとは別のリクエストであり、JSONではないものが返っている。ブラウザが実際に見せる順序で追おう。四つの可能性はそれぞれ別の修正を必要とする。
ステップ1:実際に起きた送信を捕まえる
送信の前にネットワークパネルを開き、ログをナビゲーションをまたいで保持する設定にし、使用を許されたアカウントで無害なテスト送信を一度だけ行う。選ぶのはフォーム自身が開始したリクエストで、ページの文書リクエストではない。
フィルターには注意が必要だ。XHRやFetchのフィルターは便利だが、フレームワークが今も隠しiframeや動的生成フォームで送信している場合、フィルターは唯一重要なリクエストを隠してしまう。まずフィルターなしで一度送信し、何が現れたかを見る。
選んだリクエストについて、解釈の前に六つを記録する。メソッド、クエリ文字列を含む完全なURL、ステータス、リダイレクトの連鎖、応答のContent-Type、本文の最初の数行だ。Chrome DevToolsのネットワークパネルのリファレンスは、それらがあるHeaders、Response、Initiatorの各ビューを扱っており、他のブラウザの同等パネルも同じ項目を表示する。
ステップ2:思っているリクエストかを確かめる
JSONのエラーの驚くほど多くは、実はルーティングのエラーだ。送信ハンドラーがpreventDefaultを呼ぶ前に例外を投げると、ブラウザはaction属性に書かれた宛先へネイティブのフォームPOSTを行い、サーバーは完全なHTMLページで応答する。ハンドラーは決して走らないので、JSONは一度も要求されていない。
パネルのURLと、JavaScriptが呼んでいるURLを比べる。別の場所で終わるリダイレクト、欠けた、あるいは余分な末尾スラッシュ、ハンドラーがGETを送るのに端点がPOSTを受けるメソッドの食い違い、移動したページから相対パスへ送るフォームを探す。リダイレクトの連鎖を最後まで追い、最初ではなく最終のURLを記録する。
次に開始元を確認する。リクエストがスクリプトからではなく文書ナビゲーションから来ているなら、問題はすでに見つかっている。応答パーサーを変えても助けにはならない。
ステップ3:結果を四つのカテゴリーに分ける
どの結果もこのどれか一つに属し、カテゴリーが次に見る場所を決める。
| What you see | Category | Where to look |
|---|---|---|
| Request never finishes, TLS or connection error | No usable response | Endpoint reachability, certificate, front-end timeout |
| Request completes, script gets an opaque failure | Access blocked | Cross-origin policy, credentials mode, missing response header |
| 200 with an HTML body or a login form | Wrong representation | Redirect, expired session, route fallback, error handler |
| Body begins with a notice then valid JSON | Wrong representation | Server-side warning output before serialisation |
| Valid JSON, framework rejects it | Contract mismatch | Envelope shape, field names, value types |
中央の行が誤診されやすいのは、ステータスが200だからだ。リダイレクトの後に到達するサインインページは、完全に正しいHTTP応答であり、非常に悪いJSON文書である。application/jsonというContent-Typeはそれを直さない。それはラベルにすぎず、誤ってラベル付けされた本文はやはり解析できない。アクセス遮断は、完了したリクエストに見える行だ。パネルはステータスを表示するが、クロスオリジンのポリシーが本文を渡さないので、呼び出すスクリプトにとってはタイムアウトと同じくらい使えない。
四番目の行は、古いPHPやフレームワークの構成で同じくらいよくある。符号化されたペイロードの前に出力された非推奨の警告は、どのJSONパーサーも受け付けない本文を作る。多くの場合、その警告が説明のすべてだ。
ステップ4:パーサーのエラーを文字どおり読む
エラーの文言は解析を壊した最初の文字を名指しする。これで多くの場合、推測なしにカテゴリーが特定できる。
| Error text (Chrome/V8) | What it means |
|---|---|
| Unexpected token < in JSON at position 0 | The body starts with markup: an HTML page or an error document |
| Unexpected end of JSON input | Empty body, or the response was truncated |
| Unexpected token o in JSON at position 1 | You parsed something already parsed, typically an object rather than text |
| Unexpected token u | The body starts with the bare word undefined, usually an empty response rendered as a string |
ステータス200で本文が空というのは独立した事例だ。端点は動いたが何も生まず、下流の何かがそれを成功として扱った。これは転送ではなくアプリケーションの契約の失敗であり、置き場所はプロキシではなく次のステップだ。FirefoxとSafariは同じ四つの失敗を別の言葉で表すので、正確な文字列ではなく文字や空の本文で照合する。
ステップ5:証拠が指す層を直す
ルーティングの問題ならリクエストを直す。actionを正し、ハンドラーが抜け落ちないようにし、端点とメソッドを合わせて再試験する。表現の問題なら本文を生んだものを直し、データを期待する場所でなぜエラー文書が返るのかを突き止める。契約の問題なら、実行している版に対してフレームワークが実際に期待する構造を読み、クライアントを緩めるのではなく応答をそれに合わせる。
アクセス遮断の応答には独自の手順が要る。リクエスト自体は正しく、本文がコードに届かないことがあるからだ。応答のAccess-Control-Allow-Originを、ブラウザが送った正確なOriginと比較する。ワイルドカードは資格情報付きのリクエストを満たさないことを忘れない。クライアント側のcredentials: 'include'とサーバー側のAccess-Control-Allow-Credentials: trueを対にし、呼び出しがno-corsモードを使っていなかったか確認する。そこでは不透明な本文が欠陥ではなく文書化された結果だ。
パーサーを寛容なものに置き換えるのは、避けるべき唯一の修正だ。応答をコードとして評価したり、解析できるまで文字を削ったりすると、契約の不一致を隠し、沈黙と引き換えに注入の面を増やす。同様に、ステータスは本文とは別に確認する必要がある。アプリケーション層のエラーオブジェクトを伴う200は多くのAPIで正常であり、ペイロードを読んで扱わなければならない。
他の人の助けが要るときは、生のエクスポートではなく最小限の記録を共有する。メソッド、URL、ステータス、リダイレクト後の最終URL、応答のContent-Type、本文の形、期待する契約だ。ネットワークのアーカイブにはCookie、トークン、クエリ文字列、フォームにあった個人データが含まれており、自動の匿名化はレビューではない。
- コードを変える前に、フィルターなしでリクエストを捕まえる。
- 開始元が自分のスクリプトで、最終URLが自分の端点であることを確かめる。
- パーサーに触る前に応答を分類する。
- ルーティング、アクセス、表現、契約を直し、パーサーを緩めることで済ませない。