WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

CORS 非対応の場合のエラーメッセージ

by WebSurfer 2024年4月2日 16:33

ブラウザの fetch API を使ってドメインが異なる Web API などに要求を出した時、Web API 側が CORS に対応していない場合のブラウザのエラーメッセージや Fiddler で見た時の要求・応答がどのようになるかを書きます。

検証に使ったのは、先の記事「Web API に CORS 実装 (CORE)」に書いた ASP.NET Core Web API アプリで、Web API の CORS を無効にして、ドメインが異なる MVC アプリから fetch API を使って Web API に要求を出ました。ブラウザは Windows 10 の Chrome 123.0.6312.86 と Edge 123.0.2420.65 です。

ブラウザのエラーメッセージは、ディベロッパーツールの Console タブを開くと見ることができ、Web API 側が CORS 非対応では以下のようになるはずです。赤枠が「単純リクエスト」の場合で、青枠が「プリフライトリクエスト」の場合です。(注: 「単純リクエスト」というのは古い CORS 仕様書の用語で、現在 CORS を定義している Fetch 仕様書 ではその用語を使用していないそうです)

ブラウザのエラーメッセージ

エラーメッセージを以下にコピペしておきます。文章中の 'https://localhost:44371/api/values' が Web API 側で 'https://localhost:44343' がブラウザ側です。

「単純リクエスト」の場合

Access to fetch at 'https://localhost:44371/api/values' from origin 'https://localhost:44343' has been blocked by CORS policy: 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 'https://localhost:44371/api/values' from origin 'https://localhost:44343' 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.

「プリフライトリクエスト」の場合は "Response to preflight request doesn't pass access control check:" という文が追加されています。その文の有無で「プリフライトリクエスト」か否かが分かるはずです。

蛇足ですが、エラーメッセージに含まれる "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 API の mode:"no-cors"」を見てください。

「単純リクエスト」と「プリフライトリクエスト」が必要になる場合の違いについて簡単に書いておきます。「単純リクエスト」になるのは、メソッドが GET, HEAD, POST のいずれかで、かつ、要求ヘッダが「CORS-safelisted request header (CORS セーフリストリクエストヘッダー)」の場合です。それ以外は「プリフライトリクエスト」が必要になります。(詳しくは、MDN のドキュメント「オリジン間リソース共有 (CORS)」を見てください)

例えば、JSON 文字列をコンテンツに含めて POST 送信する場合は要求ヘッダに Content-Type: application/json を含めると思いますが、その場合は「プリフライトリクエスト」が必要になります。


Fiddler で見た時の要求・応答は以下のようになります。CORS の要求側はブラウザの仕事で、プリフライトが必要か否かもブラウザが判断して、下の画像の赤枠で示した CORS に必要な要求を出してくれます。

「単純リクエスト」の場合

ブラウザは Origin を要求ヘッダに含めて送信しますが、応答ヘッダに Access-Control-Allow-Origin が含まれないということででエラーとなっています。

「単純リクエスト」の場合

「プリフライトリクエスト」の場合

OPTIONS メソッドを使って CORS に必要なヘッダを要求に含めて出しています。下の画像の赤枠部分を見てください。しかし、応答ヘッダに Access-Control-Allow-Origin が含まれないということでエラーとなっています。

「プリフライトリクエスト」の場合

上の「プリフライトリクエスト」の場合の画像で、応答が 405 Method Not Allowed、Allow: GET, POST となっています。これは ASP.NET Core Web API による応答と思われますが、なぜ Method Not Allowed となるのか、GET, POST でないとダメと言われるのかは調べ切れてません。想像ですが、CORS を有効にしないと OPTIONS メソッドが許可されないということではないかと思われます。


参考に、Web API 側が CORS に対応していて、ブラウザ側でデータの取得に成功する場合の要求・応答を Fiddler で見た画像も下に貼っておきます。

「単純リクエスト」の場合

単純リクエスト

「プリフライトリクエスト」の場合

Web API が CORS に対応しているので「プリフライトリクエスト」の応答ヘッダに Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin が含まれて返ってきます。

プリフライトリクエスト

ブラウザはそれを見て再度要求を出しデータを取得します。

データの取得

Tags: , , ,

JavaScript

fetch API の mode:"no-cors"

by WebSurfer 2023年11月28日 17:14

JavaScript の fetch() グローバル関数 のリクエストオプションに mode:"no-cors" を設定するとどうなるかという話を書きます。

mode:'no-cors' に設定した結果

上の画像がその結果です。先の記事「Web API に CORS 実装 (CORE)」で紹介した CORS を実装した Web API に対し、リクエストオプションを mode:"no-cors" に設定した fetch 関数を使って GET 要求を出し、fetch 関数の戻り値の Response オブジェクトを Chrome 119.0.6045.160 のディベロッパーツールで開いて表示したものです。

Fetch Living Standard というドキュメントが fetch の仕様書ですが、このドキュメントの mode の説明に mode:"no-cors" に設定した場合どういう結果になるかが書いてあります。以下の通りです。

"Restricts requests to using CORS-safelisted methods and CORS-safelisted request-headers. Upon success, fetch will return an opaque filtered response."

上の前半は、HTTP 要求メソッドが GET, POST, HEAD に、要求ヘッダが CORS セーフリストリクエストヘッダーに制限されると言っています (古い CORS 仕様書で言うシンプルなリクエストの条件を満たすものと同じ)。後半は、成功すると opaque filtered response を返すと言っています。

opaque filtered response とは何かというと、上に紹介した fetch の仕様書に以下のように書いてあります。

"An opaque filtered response is a filtered response whose type is "opaque", URL list is « », status is 0, status message is the empty byte sequence, header list is « », body is null, and body info is a new response body info."

この記事の一番上の画像に表示した response の中身を見てください。上の説明通り、type:"opaque", url:"", status:0 となっているのが分かるでしょうか。

Fiddler を使って要求・応答をキャプチャすると以下のようになっています。要求は出ており、応答に含まれる JSON 文字列(画像の赤枠部分)は期待通りです。

Fiddler で要求・応答をキャプチャ

しかしながら、response は opaque filtered されているので、response.json() で Uncaught (in promise) SyntaxEffor: Unexpected end of input というエラーになります。下の画像を見てください。

SyntaxEffor: Unexpected end of input

要するに、fetch のリクエストオプションを mode:"no-cors" とした場合、たとえサーバー側で CORS 対応がしてあったとしても、ブラウザ側では CORS 対応がされず、Web API からの情報(上の例では Web API から返された JSON 文字列)は取得できないという結果になりました。

というわけで、mode:"no-cors" とする理由は、自分が考え付くケースに限っての話ですが、どう考えてもなさそうです。fetch の仕様書にはデフォルトは mode:"no-cors" と書いてありますが、それは "highly discouraged from using it for new features. It is rather unsafe" だそうです。実際、Chrome, Edge, Firefox など主要なブラウザの実装では mode:"cors" がデフォルトになっていますし。

(MDN のドキュメント Request.mode の no-cors の説明に "ドメイン間でデータが漏れることによって生じるセキュリティとプライバシーの問題を防ぐ" と書いてあります。セキュリティとプライバシーの問題を防いだ結果、必要な情報も取れなくなるのでは意味がないと思うのですが・・・)


以上の例はシンプルなリクエストになる場合ですが、プリフライトリクエストが行われる場合はどうなるかも書いておきます。下の画像を見てください。fetch が返すのはやはり opaque filtered response で、前のケースと同じ結果になります。

プリフライトリクエストが行われる場合

ただし、上に述べたように、mode:"no-cors" に設定した場合、HTTP 要求メソッドとヘッダが CORS 仕様書で言うシンプルなリクエストの条件に制限される点が前のケースとは異なります。

下の Fiddler で要求・応答をキャプチャした画像の青枠部分を見てください。プリフライトリクエストで使われる OPTIONS 要求ではなくて、いきなり POST 要求が出ています。また、fetch のパラメータで Content-Type を application/json と指定したにもかかわらず text/plain となっています。

Fiddler で要求・応答をキャプチャ

サーバーからの応答が 415 Unsupported Media Type になっていますが、これは要求ヘッダの Content-Type が text/plain に書き換えられてしまったからです (ASP.NET Core Web API では Content-Type:text/plain はサポートされません)。結果、415 エラーとなってエラーメッセージ(赤枠部分)が返されています。

fetch が返すのは opaque filtered response なので response.json() の結果は、上のケースと同様に、Uncaught (in promise) SyntaxEffor: Unexpected end of input というエラーになります。下の画像を見てください。

SyntaxEffor: Unexpected end of input


ちなみに、mode:"no-cors" を削除(デフォルトの mode:"cors" となる)した場合はどうなるかも書いておきます。下の画像を見てください。CORS の操作には成功し、API からのデータも期待通り取得できています。

mode:'no-cors' を削除

fetch のリクエストオプションから mode を削除しても(ブラウザの実装ではデフォルトが mode:"cors" になるはず)期待通りに動くことは、自分の環境の Windows 10 の Chrome 119.0.6045.160, Edge 119.0.2151.93, Firefox 120.0, Opera 105.0.4970.21 で確認しました。


最後に、fetch の要求先が同一ドメインで mode:"no-cors" に設定するとどうなるかも書いておきます。 Chrome 119.0.6045.160 で試した結果ですが、fetch が返すのはクロスドメインの時のような opaque filtered response ではなくて、以下のようになりました。

同一ドメインの場合

type:"basic" というのは同一ドメインのレスポンスを意味します (opaque filtered response になる場合は type:"opaque" となります)。どうも、mode:"no-cors" は無視されて、CORS に関係ない通常のやり取りがされるように見えます。

Tags: , , ,

JavaScript

input type="file" を生成してアップロード

by WebSurfer 2023年4月2日 20:46

JavaScript を使って、ボタンクリックでファイル選択ダイアログを表示し、ユーザーがダイアログに表示されたファイルを選択したら即アップロードする方法を書きます。

ファイルアップロード

ファイルのアップロードでよくあるパターンは、<form> 要素の中に <input type="file"> 要素と <input type="submit"> 要素を静的に配置しておいて、(1) <input type="file"> のファイル選択ボタンをクリックしてファイル選択ダイアログを表示、(2) ユーザーがダイアログからファイルを選択、(3) <input type="submit"> をクリックして選択されたファイルを POST 送信する・・・というものだと思います。

その (1) から (3) のステップを短縮し、<input type="button"> ボタンをクリックしたらファイル選択ダイアログを表示し、ユーザーがファイルを選択したら File API を利用して File オブジェクトを取得し、それを fetch API を使って即 Web API にアップロードしてみます。

クライアント側のコードは以下の通りです。<input type="button"> ボタンだけを静的に配置しておき、それ以外は JavaScript で処置しています。説明はコード内のコメントに書きましたのでそれを見てください。

コードは ASP.NET Core MVC の View をものをそのままコピー&ペーストしましたが、その中の html と JavaScript のコードはどのような Web アプリにも使えるはずです。

@{
    ViewData["Title"] = "UploadFile";
}

<h1>UploadFile</h1>

<button type="button" class="btn btn-primary" id="btn">
    ファイル選択してアップロード
</button>

<div id="result"></div>

@section Scripts {
   <script type="text/javascript">
        window.addEventListener('DOMContentLoaded', () => {

            // Promise を返す非同期メソッド
            const showOpenFileDialog = async () => {
                return new Promise(resolve => {
                    // input type="file" 要素を動的に生成、
                    const input = document.createElement('input');
                    input.type = 'file';

                    // .jpg ファイルのみ選択できるよう設定してみた
                    input.accept = '.jpg';

                    // ユーザーがファイルを選択すると change イベント
                    // が発生するのでそれにリスナをアタッチし、File
                    // オブジェクトを取得。それを Promise の resolve
                    // コールバックに設定
                    input.addEventListener('change', 
                            e => resolve(e.target.files[0]));

                    // クリックしてファイル選択ダイアログを開く
                    input.click();
                });
            };

            // ボタンクリックで上のメソッドを呼び出し File オブジェ
            // クトを取得。それを fetch API を使って Web API に送信
            document.getElementById("btn")
                    .addEventListener('click', async () => {
                const file = await showOpenFileDialog();
                const formData = new FormData();
                formData.append("postedFile", file);
                const param = {
                    method: "POST",
                    body: formData
                }
                const response = await fetch("/api/Upload", param);
                const data = await response.text();
                document.getElementById("result").innerText = data;
            });
        });
   </script>
}

サーバー側で、アップロードされてきたファイルを受けて保存する ASP.NET Core Web API のコードも参考までに以下に載せておきます。

using Microsoft.AspNetCore.Mvc;

namespace MvcCore6App3.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        // .NET 6.0 で作ったプロジェクトは「Null 許容」オプションが「有効化」
        // に設定してあるので、引数も null 許容にしておかないと、クライアント
        // でファイルを選択しないで送信した場合、HTTP 400 Bad Request エラー
        // になって "The postedFile field is required."というエラーメッセージ
        // が返ってくる
        [HttpPost]
        public async Task<string> Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                string filePath = $"{contentRootPath}\\UploadedFiles\\" +
                    $"{filename}{DateTime.Now.ToString("yyyyMMddHHmmss")}.jpg";

                // フォルダ UploadedFile に画像ファイルを保存
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

                result = $"{filename} ({postedFile.ContentType}) - " +
                    $"{postedFile.Length} bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return result;
        }
    }
}

Tags: , , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar