Content-Type: 'application/json'のときのCORSエラー

## 経緯

React + Web API（C#）でアプリを開発しています。
長いことnugetパッケージのアップデートを放置()していたので、更新していました。
ある程度直ってきたので、Reactとの動作確認を行ったところ、GETメソッドは問題なく通信できるのに、POST（+ Preflight）はCORSエラーになってしまい、機能しなくなっていました。

## 出てきたエラー

```console: コンソール
Access to fetch at '' from origin 'https://localhost:3000' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
```

```:NetworkタブのStatusホバーで出てきたメッセージ
Cross-Origin Resource Sharing error: PrefilghtMissingAllowOriginHeader
```

## 前提条件

- **Web API (C#)**:
  フレームワークは.Net Framework 4.8
  `Microsoft.AspNet.WebApi.Cors`はv5.2.4からv5.3.0にアップデート
  CORSの設定は以下の3ヶ所

  ```cs: App_Start/WebApiConfig.cs
  using System.Web.Http;

  namespace SampleApp
  {
    public static class WebApiConfig
    {
      public static void Register(HttpConfiguration config)
      {
        // 省略
        config.EnableCors();
      }
    }
  }
  ```

  ```conf: Web.config
  <system.webServer>
    <!-- 省略 -->
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS, PUT, PATCH, DELETE" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
  ```

  ```cs: 各ApiController（共通）
  using System.Web.Http;
  using System.Web.Http.Cors;

  namespace SampleApp.Controllers
  {
    [EnableCors(origins: "*", headers: "*", methods: "*", SupportsCredentials = true)]
    public class DefaultController : ApiController
    {
      [HttpGet]
      public IHttpActionResult Get()
      {
        return Ok();
      }
    }
  }
  ```

- **React**:
  パッケージの`react`のバージョンはv16.2.0
  通信はJSの`fetch`を使用。基本の形は以下の通り
  
  ```js
  // GET, DELETE など
  fetch (url, {
    mode: 'cors', method: method, credentials: 'include',
  })
 
  // POST, PUT, PATCH など
  fetch (url, {
    mode: 'cors', method: method, credentials: 'include',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: body,
  })
  ```

## 原因

**Request HeaderのContent-Typeに'application/json'を指定しているのに、Access-Control-Allow-HeadersにContent-Typeを"明示的に"追加していない**ことが原因でした。

詳しいことは以下の記事に書いてあります。

https://blog.foresta.me/posts/http_preflight_request/

今回のケースに則して要点をまとめると、

セーフリクエストヘッダーであるContent-Typeを、Access-Control-Allow-Headersに追加する必要はない**が**、Content-Typeが'application/json'の場合はセーフリクエストヘッダーでは**なくなる**ため、追加しなければいけない。

です。

## 修正

今回は`Web.config`と各ApiControllerの修正でよさそうです。
※ローカルだけがcross-originとなり、それ以外はsame-originとなるため、ローカルだけ解消したことを確認しています。

```diff_c_sharp:Web.config
  <system.webServer>
    <!-- 省略 -->
    <httpProtocol>
      <customHeaders>
+       <add name="Access-Control-Allow-Origin" value="https://localhost:3000" />
+       <add name="Access-Control-Allow-Headers" value="Content-Type" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS, PUT, PATCH, DELETE" />
+       <add name="Access-Control-Allow-Credentials" value="true" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
```

```diff_c_sharp: 各ApiController（共通）
  using System.Web.Http;
  using System.Web.Http.Cors;

  namespace SampleApp.Controllers
  {
-    [EnableCors(origins: "*", headers: "*", methods: "*", SupportsCredentials = true)]
     public class DefaultController : ApiController
     {
        [HttpGet]
        public IHttpActionResult Get()
        {
           return Ok();
        }
     }
  }
```

## 蛇足1: EnableCorsAttributeは使えなかった

`EnableCorsAttribute`を使う方も試しましたが、解決できませんでした。

```diff_c_sharp:App_Start/WebApiConfig.cs
  using System.Web.Http;
+ using System.Web.Http.Cors;

  namespace SampleApp
  {
    public static class WebApiConfig
    {
      public static void Register(HttpConfiguration config)
      {
-       config.EnableCors();
+       var cors = new EnableCorsAttribute(
+                    "https://localhost:3000", 
+                    "Content-Type", 
+                    "GET, POST, OPTIONS, PUT, PATCH, DELETE");
+       cors.SupportsCredentials = true;
+       config.EnableCors(cors);
      }
    }
  }
```

`Web.config`と一緒に設定すると`Cross-Origin Resource Sharing error: MultipleAllowOriginValues`のCORSエラーになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/6b26b913-d061-b039-9da3-33887abbb09b.png)

ただ、`Web.config`の重複している設定を消してもPOSTでエラーになるのは変わらないので、使わないことにしました。

## 蛇足2: Credentialsがtrueの場合、Originに"*"を設定しても無効になる

公式のドキュメントを見ていると、以下のような記述が見つかりました。

https://learn.microsoft.com/ja-jp/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#pass-credentials-in-cross-origin-requests

> CORS 仕様では、SupportsCredentials が true である場合、origins を "*" に設定しても無効であることが示されています。

すべて、"*"でした、、！
静かに直しておきます。

## 呟き

コンソールのエラー読むと、ちゃんと書いてありますね、、
エラーはちゃんと読もう！（自戒）

## 発生した疑問

時間があったら調べて解決したい疑問。

1. `Access-Control-Allow-`系の値は、大文字小文字の区別があるのか
1. ローカルでの開発の際、`Access-Control-Allow-Origin`に`"https://localhost:3000"`としか書かずに、`https://localhost:3001`からアクセスしたら弾かれるのか
   →CORS的には弾きそうだけど、弾かれなかったのが不思議
1. `Access-Control-Allow-Headers`などに`*, Content-Type`みたいな指定はできるのか
   →ChatGPTには無理だと言われた
1. `EnableCorsAttribute`を使う方はなぜ上手くいかなかったのか
   →どこかで`[EnableCors]`をまとめて指定できるのが`EnableCorsAttribute`だと見た気がするけど、`Web.config`と両立できないなら使えないのは困る
1. 各APIコントローラーにある`EnableCors`の指定は必要なのか
   →今回のソースのような指定なら、`Web.config`の設定だけで十分にも見える

## 参考

https://blog.foresta.me/posts/http_preflight_request/

https://learn.microsoft.com/ja-jp/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api

https://learn.microsoft.com/ja-jp/iis/extensions/cors-module/cors-module-configuration-reference

https://developer.mozilla.org/ja/docs/Glossary/Preflight_request

https://zenn.dev/luvmini511/articles/d8b2322e95ff40