GASのCORSエラーを解消する

こんにちは、駆け出しエンジニアの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](https://script.google.com/macros/s/(%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4ID)/exec)' from origin '[http://localhost:3000](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`
- **ヘッダー**が、以下の「単純ヘッダー」のみを含む:
    - `Accept`
    - `Accept-Language`
    - `Content-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のメソッドの一つで、ブラウザが**サーバーに対してそのリソースにアクセスできるかどうかを確認するためのリクエスト**です。

プリフライトリクエストの流れは以下の通りです：

1. **ブラウザが`OPTIONS`リクエスト**を送信し、「このオリジンからのリクエストを受け入れるかどうか」を確認する。
2. サーバーが次のようなヘッダーを返し、指定のオリジン（またはワイルドカード`*`ですべてのオリジン）が許可されている場合、リクエストが通ります。逆にサーバーが許可していない場合、ブラウザはエラーを表示し、リクエストを中断します。
    
    ```tsx
    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](https://script.google.com/macros/s/(%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4ID)/exec)' from origin '[http://localhost:3000](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.
> 

1. **“Access to fetch at...”**
    
    このメッセージの最初の部分では、`localhost:3000`から`https://script.google.com/macros/s/.../exec`（GASのエンドポイント）へのリクエストがブロックされたことが示されています。
    
    つまり、`localhost:3000`のJavaScriptコードから異なるオリジン（GASのURL）へのリクエストが「CORSポリシーによりブロック」されました。
    
2. **原因：プリフライトリクエストの確認が通過しなかった**
    
    > Response to preflight request doesn't pass access control check
    > 
    
    プリフライトリクエストをサーバーが受け取りましたが、「アクセスコントロールチェックに失敗した」という意味です。
    
    ブラウザが送信したOPTIONSリクエストに対して、サーバー側（この場合GASのエンドポイント）がCORSを許可する応答を返せていないことが問題です。
    
3. **サーバーからの応答に`Access-Control-Allow-Origin`ヘッダーが含まれていない**
    
    > No 'Access-Control-Allow-Origin' header is present on the requested resource
    > 
    
    サーバーのレスポンスに、クロスオリジンリクエストを許可するための`Access-Control-Allow-Origin`ヘッダーが含まれていないため、ブラウザがリクエストを拒否しています。
    
    このヘッダーがないと、ブラウザは「このリクエストは安全ではない」と判断し、JavaScriptでデータを取得できなくなります。
    
4.  **最後の部分の解釈**
    
    > 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に直接リクエストを送ることができ、エラーを回避できます。

```tsx
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と仲良くなれるよう頑張ります。

この記事がエラーの解決の一助になれば幸いです。