Laravel で Access-Control-Allow-Origin ヘッダーを付与しても CORS エラーが解消しない

よくよく考えれば当たり前のことだったんですが、若干ハマってしまったので・・・
同様にハマった人の助けになれば幸いです。

# 普通に CORS 対応をすすめる
## Access-Control-Allow-Origin などをレスポンスヘッダーに含ませる Middleware の作成
いろいろな人が記事をまとめてくれているので、ググればすぐ見つかると思います。

各ヘッダーの意味はこちらを参照。
[Access-Control-Allow-Origin](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
[Access-Control-Allow-Methods](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)
[Access-Control-Allow-Headers](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)

```php:App\Http\Middleware\Cors
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    public function handle($request, Closure $next)
    {
        return $next($request)
            ->header('Access-Control-Allow-Origin', '*')
            ->header('Access-Control-Allow-Methods', 'GET, POST')
            ->header('Access-Control-Allow-Headers', 'Accept, X-Requested-With, Origin, Content-Type');
    }
}
```

api Middleware グループに追加します。
これで api 全体に適用されるようになります。（もちろんルーティング定義の際に api Middleware グループを指定している場合に限ります）

```php:App\Http\Kernel
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    :

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\Cors::class, // ★追加
        ],
    ];

    :

}

```

大抵はこれで十分なんですが、 プリフライトリクエストが発生する場面ではこれだけでは不十分でした。
プリフライトリクエストについてはこちら
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Simple_requests
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Preflighted_requests

# プリフライトリクエストの場合は api Middleware が使用されない

どうやらプリフライトリクエストについてのルーティング定義は自動的に Laravel が作成するようです。
その場合は、Middleware の適用などが行われません。

## つまり

こんなルーティング定義を用意しておきます。

```php
Route::get('sample', function () { echo 'sample api'; });
```

URL や Middleware の適用はこんな状態です。
http://domain/api/sample にアクセスすると api Middleware グループが適用されます。

```sh
$ php artisan route:list | grep 'sample'
|        | GET|HEAD  | api/sample                               | front.api.                            | Closure                                                                      | api                                                                            |
```

この状態で CORS となるようにブラウザの Console 上でこの API を実行してみます。

```js
fetch('https://dummy.jp/api/sample', {method: 'GET', headers: {'X-Requested-With': 'XMLHttpRequest'}})
.then(response => console.log(response))
.catch(response => console.error(response))

// -->
// Access to fetch at 'https://dummy.jp/api/sample' from origin 'https://developer.mozilla.org' 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.
```

先程追加した Cors Middleware で設定しているはずの Access-Control-Allow-Origin が効いていません。

ブラウザの開発者ツールからこのときにリクエストの情報を見てみると

1. プリフライトリクエストとして OPTIONS のリクエストが最初に飛ぶ
2. その時のレスポンスヘッダーに Access-Control-Allow-Origin は含まれていない

である事がわかります。

### OPTIONS に対応するルーティング定義は用意していない

php artisan route:list の結果からわかるように OPTIONS に対するルーティング定義は用意していません。
Laravel は対応していない HTTP リクエストメソッドに対しては 405 エラーを返しますが、そのエラーは発生していません。
このことから OPTIONS の場合は Laravel がプリフライトリクエストだからいい感じにレスポンスしてくれてると思われます。
ソースコードを grep してみたんですが、 OPTIONS ではいろいろな箇所がヒットして特定しきれなかったので諦めました。

### Laravel がいい感じにレスポンスしてくれるときは Middleware が効かない

URL 自体はルーティング定義にある api/sample なので、 api Middleware グループを適用してほしいところですが、そこまではやってくれないようです。
試しに App\Http\Middleware\Cors に全然関係ない echo や exit を埋め込んでも動作に変化はありませんでした。

なので、レスポンスヘッダーに Access-Control-Allow-Origin が含まれることはありません。

# 対応方法

もうちょっとスマートな方法もあるきもしますが、ひとまず以下の方法で問題は解消できました。

1. すべてのルーティングに Cors Middleware を適用する
2. Cors Middleware 内で API Middleware グループを適用しようとしている URL であれば Access-Control-Allow-Origin をレスポンスヘッダーに含める

としました。

## すべてのルーティングに Cors Middleware を適用する

```php:App\Http\Kernel
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    :

    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\Cors::class, // ★追加
    ];

    :

}

```

## Cors Middleware 内で API Middleware グループを適用しようとしている URL であれば Access-Control-Allow-Origin をレスポンスヘッダーに含める

適用されている Middleware グループを取得できれば一番良かったんですが、どうにもうまく取れなかったので URL の一部から API であるかを判定するようにしています。
判定方法は実際の実装に合わせて柔軟に変更してください。

```php:App\Http\Middleware\Cors
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    public function handle($request, Closure $next)
    {
        // すべてのレスポンスに CORS 用のヘッダーを追加する必要はないので URL から判断する
        $paths = explode('/', \Request::getPathInfo());
        if ($paths[1] === 'api') {
            return $next($request)
                ->header('Access-Control-Allow-Origin', '*')
                ->header('Access-Control-Allow-Methods', 'GET, POST')
                ->header('Access-Control-Allow-Headers', 'Accept, X-Requested-With, Origin, Content-Type');
        }
        return $next($request);
    }
}
```

## 再度 CORS となるようにブラウザの Console 上でこの API を実行してみる

今度はエラーが発生せず、正常に通信できるようになります。