お疲れ様です。
今日はRestAPIをたたいていて落とし穴にはまったのでそのまとめをしておきます。
(エラーが出ないのが厄介でした・・・)
2024/02/17:タイムアウトについてコメントをいただきましたので追記しました。
前提
Node.jsさえあればどこでも動くRestAPIサーバを以前作って使っています。
(手前味噌ですが、これかなり簡易でよい感じです。ファイルを使うのでDBとかも不要ですし)
(ただし、本当に簡単なことしかできません)
ぶっちゃけRestAPIサーバを自前で実装したらどうなんの?って思って書いただけです。
(本当はExpressとか使った方がいいんでしょうけどね。まぁそのあたりは自作のご愛嬌)
しようとしたこと
jsで、以下のようなコードを書きました。
ざっくりいうと、fetchでAPIに対してPOSTでデータを送信しようというものです。
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:3000/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
};
~略~
<Button type="submit">Submit</Button>
何が起きたか
ボタンを押しても何も反応しないんです。えぇ、全く無反応です。
涙目になりながらせめてエラーは出してくれよ・・・と思いました。
で、タイミングが悪いことに新しめのUIライブラリとかを使っていたので、そのせいかも・・・とか思っちゃってました。
調査してみる
とりあえず動いてないんで、前後とかにconsole.logとかしてみると、どうにもやはりawait fetchの部分が動いてない。
いろいろ試す
そういえば、ほかのコードではうまく動くんだけどな、とか思ってました。
動くコードは例えば以下
axios
.post(targetUrl, { ...userData })
どうしてなんだろうなぁと思ってました。いったい何がいけないんだろうなぁと思いました。
ChatGPT先生に相談だ
元のコードもChatGPT先生に書いてもらってたんですが、改めて、「FetchAPIでPOSTしたい」といってコードを書いてもらいました。
でもやっぱり同じ事象が起きたんで「何にもエラー出ないんだけど」って聞いたら以下を言われました
CORSポリシー: ブラウザのセキュリティにより、異なるオリジン間のリクエストが制限されている可能性があります。localhostでサーバーを実行している場合、サーバー側で適切なCORS設定を行っていることを確認してください。
ネットワークエラーの確認: ブラウザの開発者ツールのネットワークタブを確認して、リクエストが実際に送信されているかどうかと、レスポンスのステータスコードを確認してください。
サーバー側のログ確認: サーバー側でリクエストを受け取っているかどうか、エラーログがないかを確認してください。サーバー側でリクエストを正しく処理できていない可能性があります。
フォームデータの確認: handleSubmit関数が正しくトリガーされているか、及びformDataに正しい値が設定されているかをデバッグで確認してください。
HTTPSとHTTP: セキュリティの観点から、特に本番環境ではHTTPSを使用することが推奨されます。開発環境ではHTTPでのテストも一般的ですが、本番環境へのデプロイ時にはHTTPSへの切り替えを忘れないようにしてください。
サーバ側はCORSは対応していた("Access-Control-Allow-Origin": "*")ので、とりあえず二つ目のネットワークを確認してみました。
すると、、、OPTIONSメソッドしか送信されていないことがわかりました。
ありゃまーこりゃだめだということで、ChatGPTに聞いてみたところ、PREFLIGHTだということがわかりました。
とりあえず解決してみる
よくよく見てみると、「'Content-Type': 'application/json'」をヘッダーに指定すると、プリフライトが発行され、それをクライアント側は待つんでですね。
というわけで、簡易的な切り分けとして、上記のヘッダーを消したところあっけなくうまくいきました。
本格的な対応策
原因の切り分けはできたんですが、本来はサーバ側がきちんとプリフライトに対応すべき、OPTIONSメソッドに応答するべきなので、以下のコードを追記します。
// Handle PREFLIGHT (OPTIONS) request
if (req.method === "OPTIONS") {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*", // Allow requests from any origin
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", // Specify allowed methods
"Access-Control-Allow-Headers": "Content-Type, Authorization", // Specify allowed headers
"Access-Control-Max-Age": 86400, // Cache preflight response for 24 hours
});
res.end();
return;
}
そしたら、プリフライトのときにもうまく動きました。(ChatGPT先生に教えてもらいました)
教訓
というわけで解決したんですが以下を教訓にします。
ネットワークはみよう
原因調査ではまずはネットワークから確認することが大事だなと改めて思いました。
(どうしてもアプリ開発者はコードに問題があると思いがちですが、実際はコード以外の部分に欠陥があることも多くありますし、こうなるともうコードを読んでいても全く分からずドツボにはまります。)
ということで、しっかりとステップバイステップで一つずつ確認していきましょうね。
自分で実装しよう
今回はAPIサーバを自前で実装していてそのせいで発生したともいえます。(考慮不足)
逆に言うと、はまっておくと記憶には残ると思うので、車輪の再発明かもしれませんが、こういう部分を自分で作ってみるというのはすごく勉強になるなと思います。
何も出ないので気を付けよう
この事象は「エラーが出ない」ので泣きながら原因調査をすることになります。
こういった暗黙的にやられているようなものに関しては別の観点での調査が必要になると思いますので、自分の知識として蓄えておきたいと思います。
(にしても、プリフライトのところはエラー出てほしいよ・・・)
以上です。誰かの参考になればと思います。
追記:プリフライトでもエラーは出せる
@junerさんより情報提供いただきましたが、fetchのときに第二引数でタイムアウトを指定することでプリフライトでOPTIONSが返ってこない時にタイムアウトになりました。
const response = await fetch("http://localhost:3000/thread", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
signal: AbortSignal.timeout(5000),
});
これでタイムアウトでエラーが出てくれるようになりました。
(が、コメントで書いてもらっている通り、エラーが出るようになってから原因調査は必要にはなりますね)
※個人的にはデフォルトでタイムアウトしてほしいなという感じが・・・タイマー設計って重要なんですね。