こんにちは、駆け出しエンジニアのtaiyoです。
1. GASのエンドポイントを叩くとCORSエラーが発生した
GAS(Google App Script)を使ってdoPostを含むAPIを作り、Next.jsでブラウザからGASのエンドポイントにfetchを使ってPOSTリクエストを送ったところ、次のような「CORSエラー」が発生しました。今回はこのエラーの意味と解決策がわかったので、共有したいと思います。
Access to fetch at 'https://script.google.com/macros/s/(デプロイID)/exec' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
2. CORSとはなにか?その仕組みを知ろう
エラーの解説に入る前に、そもそもCORSの仕組みがわかっていないと解決法を見てもよくわからないと思うので、まずはCORSの基本から抑えていきたいと思います。すでに知っている方は飛ばしていただいても構いません。
1. CORSの基本と目的
CORS(Cross-Origin Resource Sharing)とは、ブラウザが異なるオリジン(ドメイン、ポート、またはプロトコルが異なるリソース)に対してリクエストを送る際のセキュリティチェックを行い、リクエストが許可されているか確認する仕組みのことです。
オリジン(Origin)とは、URLのプロトコル、ドメイン名、ポート番号を組み合わせたもので、例えば以下のように区別されます。
-
https://example.comとhttps://api.example.comは別オリジン -
http://example.com:3000とhttp://example.com:4000も別オリジン
2. 「安全なリクエスト」と「安全でないリクエスト」
実は、ブラウザからのリクエストには「安全なリクエスト(safe request)」と「安全でない(non-safe request)」の2つがあります。
安全なリクエスト
-
メソッドが
GET、POST、またはHEAD -
ヘッダーが、以下の「単純ヘッダー」のみを含む:
AcceptAccept-LanguageContent-Language-
Content-Typeがapplication/x-www-form-urlencoded、multipart/form-data、またはtext/plainのいずれか
安全でないリクエスト
-
上記以外のリクエスト(例:
PUT、DELETEメソッドやapplication/jsonのContent-Typeヘッダーを含む場合など)
実は、CORS処理が発生するのは、この「安全でない(non-safe request)」のときだけです。ブラウザに「安全でない」と判断された場合、本来のリクエストの前に、次のような「プリフライトリクエスト」というものが走ります。
3. プリフライトリクエスト
プリフライトリクエストとは、実際のリクエストの前にブラウザがサーバーに確認するためOPTIONSリクエストです。
※ OPTIONSリクエストとは、HTTPのメソッドの一つで、ブラウザがサーバーに対してそのリソースにアクセスできるかどうかを確認するためのリクエストです。
プリフライトリクエストの流れは以下の通りです:
-
ブラウザが
OPTIONSリクエストを送信し、「このオリジンからのリクエストを受け入れるかどうか」を確認する。 -
サーバーが次のようなヘッダーを返し、指定のオリジン(またはワイルドカード
*ですべてのオリジン)が許可されている場合、リクエストが通ります。逆にサーバーが許可していない場合、ブラウザはエラーを表示し、リクエストを中断します。HTTP/1.1 200 OK Content-Type: application/json **Access-Control-Allow-Origin: http://localhost:3000** Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type Content-Length: 123 { "message": "リクエストは成功しました", "data": { "key": "value" } }
3. エラーの解説
さてさて、CORSやプリフライトリクエストの仕組みがわかったところで、次のエラーの解説に入りたいと思います。
Access to fetch at 'https://script.google.com/macros/s/(デプロイID)/exec' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
-
“Access to fetch at...”
このメッセージの最初の部分では、
localhost:3000からhttps://script.google.com/macros/s/.../exec(GASのエンドポイント)へのリクエストがブロックされたことが示されています。つまり、
localhost:3000のJavaScriptコードから異なるオリジン(GASのURL)へのリクエストが「CORSポリシーによりブロック」されました。 -
原因:プリフライトリクエストの確認が通過しなかった
Response to preflight request doesn't pass access control check
プリフライトリクエストをサーバーが受け取りましたが、「アクセスコントロールチェックに失敗した」という意味です。
ブラウザが送信したOPTIONSリクエストに対して、サーバー側(この場合GASのエンドポイント)がCORSを許可する応答を返せていないことが問題です。
-
サーバーからの応答に
Access-Control-Allow-Originヘッダーが含まれていないNo 'Access-Control-Allow-Origin' header is present on the requested resource
サーバーのレスポンスに、クロスオリジンリクエストを許可するための
Access-Control-Allow-Originヘッダーが含まれていないため、ブラウザがリクエストを拒否しています。このヘッダーがないと、ブラウザは「このリクエストは安全ではない」と判断し、JavaScriptでデータを取得できなくなります。
-
最後の部分の解釈
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
ブラウザは、
mode: 'no-cors'を設定してfetchリクエストを送信するよう提案していますが、この方法には制限があります。no-corsモードでは、CORSのエラーは回避できますが、レスポンスの内容が「opaque」(不透明)となり、データの中身をJavaScriptから確認することはできません。
4. エラーの原因と解決策
では、どうしてこのようなエラーが発生してしまうのでしょうか?
どうやら調べたところ、Google Apps Script(GAS)では、プリフライトリクエストに対する応答をうまく処理できないことがあるそうです。
つまり、プリフライトリクエストを受け取っても、「このリクエストを許可する」という応答を返さないため、ブラウザが「リクエストが安全でない」と判断し、実際のPOSTリクエストがブロックされます。
CORSエラーを解決するための方法の一つは、プリフライトリクエストが発生しないようにリクエストの形式を変更することです。
例えば私の場合、POSTリクエストのContent-Typeをapplication/jsonからtext/plainに設定することで、これは「安全なリクエスト」となり、ブラウザがプリフライトリクエストを送信しなくなります。これにより、Google Apps Scriptに直接リクエストを送ることができ、エラーを回避できます。
const response = await fetch('https://script.google.com/macros/s/(デプロイID)/exec', {
method: 'POST',
headers: {
'Content-Type': 'text/plain', // 'application/json'から変更
},
body: JSON.stringify(data),
});
また、もう一つの解決策はfetchをサーバーサイドから実行することです。なぜなら、CORS制限はブラウザレベルで適用されるセキュリティ機構のため、ブラウザを介さず直接サーバーからGASのエンドポイントにリクエストを送ることができれば、CORSエラーが発生しません。
5. まとめ
いかがだったでしょうか?GASはかゆいところに手が届くツールではありますが、内部構造がわからないため、仲良くなるにはもう少し時間が必要みたいです。これからもっとGASと仲良くなれるよう頑張ります。
この記事がエラーの解決の一助になれば幸いです。