WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

CORS 非対応の場合のエラーメッセージ(その2)

by WebSurfer 4. June 2024 12:47

先の記事「CORS 非対応の場合のエラーメッセージ」の続きで、fetch メソッドで指定した URL が間違っていた場合どうなるかを調べたので、備忘録として書いておきます。

URL が間違っているとサーバーからは HTTP 404 Not Found 応答が返ってきますが、それがどのように表示されるかが書いておきたかったことです。

ちなみに、404 応答というのは、(1) URL のホスト名は正しく名前解決されブラウザからの要求は Web サーバーに届いた、(2) 要求を受け取った Web サーバーは URL に指定されたリソースを探したが見つからなかった、(3) Web サーバーは見つからなかったという応答を返した・・・ということです。

検証に使ったのは、先の記事「Web API に CORS 実装 (CORE)」に書いた ASP.NET Core Web API アプリで、Web API の CORS を無効にして、ドメインが異なる MVC アプリから fetch API を使って Web API に要求を出します。その際、正しい URL は https://localhost:44371/api/values とすべきところを、values ⇒ valuesx としました。

実は、CORS を無効にしたら 404 応答は返ってこないと思い違いをしていたのですが、そうではなかったです。バックエンドが CORS 非対応でもフロントエンドから要求は出て、URL が間違っているため指定されたリソースが見つからない場合は 404 応答が返ってきます。

以下に、「CORS 非対応 URL 誤」「CORS 非対応 URL 正」「CORS 有効 URL 誤」の 3 つのケースで、Chrome のディベロッパーツールの Console に表示されるエラーメッセージと、Fiddler で要求・応答をキャプチャした画像を貼っておきます。

(1) CORS 非対応 URL 誤

CORS 非対応でもブラウザから要求は出ます。URL が間違っているので Web サーバーは 404 応答を返します。それを受けたブラウザでは、まず応答ヘッダに Access-Control-Allow-Origin が無いということで CORS 関係のエラーメッセージを出しています。次に、404 応答なのでそのエラーメッセージ、最後に Failed to fetch (fetch 失敗) というエラーメッセージを出しています。

CORS 非対応 URL 誤

Fiddler の画像です。404 応答が返ってきています。バックエンドが CORS 非対応なので応答ヘッダには Access-Control-Allow-Origin は含まれていません。

CORS 非対応 URL 誤


(2) CORS 非対応 URL 正

上の (1) のケースから URL を正しいものにした場合の結果です。URL は正しいので当然 404 応答ではなく 200 応答が返ってきます。JSON 文字列も正しく応答コンテンツに含まれて返ってきます。しかし、応答ヘッダに Access-Control-Allow-Origin が無いので fetch メソッドでのデータの取得に失敗しています。

CORS 非対応 URL 正

Fiddler の画像です。URL は正しいので 200 応答が返ってきています。応答の TextView (コンテンツ) を見ると期待通り JSON 文字列が返ってきています。しかし、バックエンドが CORS 非対応なので応答ヘッダには Access-Control-Allow-Origin は含まれていません。

CORS 非対応 URL 正


(3) CORS 有効 URL 誤

上の (1) のケースからバックエンドの CORS 対応を有効にした結果です。URL は間違ったままなので 404 応答が返ってきています。上の (1), (2) と違って fetch 失敗とはみなされないようです。

CORS 有効 URL 誤

CORS を有効にしたので応答ヘッダには Access-Control-Allow-Origin が含まれています。

CORS 有効 URL 誤

Chrome のディベロッパーツールの Source タブの画面です。上の (1), (2) のケースでは fetch(url) の実行で例外がスローされて処理が終わってしまいますが、(3) のケースでは if(response.ok) 以降に処理が進みます。

CORS 有効 URL 誤

Tags: , , , ,

JavaScript

Razor Pages アプリでファイルアップロード

by WebSurfer 6. May 2024 13:10

ASP.NET Core Razor Pages アプリでファイルをアップロードする方法について書きます。先の記事「ASP.NET Core MVC でファイルアップロード」の Razor Pages 版です。MVC ⇒ Razor Pages にしたこと以外で先の記事と違うのは、ターゲットプラットフォームを .NET 8.0 にしたことと、jQuery ajax に代えて fetch API を使ったことです。

Razor Pages でファイルアップロード

普通に form を submit して POST 送信する場合と、fetch API を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は fetch API を使ってアップロードした結果です。

先の記事にも書きましたが、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。

この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。

セキュリティの話はちょっと置いといて、この記事では単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。

  1. cshtml のコードでは form 要素の enctype 属性に "multipart/form-data" を設定する。
  2. cshtml.cs の OnPost メソッドが受け取るモデルの、アップロードされたファイルがバインドされるプロパティは IFormFile 型とする。  
  3. 上に述べたプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. ブラウザによってはクライアント PC でのフルパスがファイル名として送信されることがあるので、Path.GetFileName を使ってパスを除いたファイル名のみを取得する。  
  5. ワーカープ���セスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です。Kestrel の場合も権限が必要なのは同じだと思いますが詳しいことは未調査で分かりません)
  6. IIS も Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。

fetch API を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。

  1. fetch API を使用して送信するフォームデータを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。FormData オブジェクトには CSRF 防止のための隠しフィールドのトークンも含まれます。同時にクッキーの CSRF 防止のためのトークンも送信されるので、サーバー側で検証が可能になります。
  2. fetch API を使う場合、デフォルトでは要求ヘッダに X-Requested-With: XMLHttpRequest は含まれない。サーバー側での判定のためなどにそのヘッダが必要な場合、クライアントスクリプトにそのヘッダを追加するコードを書く必要があります。

サンプルコードを以下に載せておきます。この記事の一番上の画像は下のサンプルコードの実行結果です。

Model

namespace RazorPages1.Models
{
    public class UploadModels
    {
        public string? CustomField { get; set; }
        public IFormFile? PostedFile { get; set; }
    }
}

cshtml

@page
@model RazorPages1.Pages.File.UploadModel

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

<style type="text/css">
    /*Bootstrap5 には form-group は無いので 4 と同じものを追加*/
    .form-group {
        margin-bottom: 1rem;
    }
</style>

<h1>Upload</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div class="form-group">
                <div class="col-md-10">
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのプロパティ名と同じにする
                    こと。大文字小文字の区別はない*@
                    <input type="file" name="postedfile" />
                </div>
            </div>
            <div class="form-group">
                <div class="col-md-10">
                    <input type="submit" value="Submit Form"
                           class="btn btn-primary" />
                    <div>@Model.Message</div>
                </div>
            </div>
        </form>

        <div class="form-group">
            <div class="col-md-10">
                <input type="button" id="ajaxUpload"
                    value="Use Fetch API" class="btn btn-primary" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        document.getElementById("ajaxUpload")
            .addEventListener('click', async () => {
                const fd = new FormData(document.querySelector("form"));
                const result = document.getElementById("result");

                // 追加データを以下のようにして送信できる。フォーム
                // データの一番最後に追加されて送信される
                fd.append("CustomField", "This is some extra data");

                const param = {
                    method: "POST",
                    body: fd,
                    // jQuery ajax と違って X-Requested-With ヘッダは
                    // 自動的には送られないので以下の設定で対応
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                }

                const response = await fetch("/file/upload", param);
                if (response.ok) {
                    const data = await response.text();
                    result.innerText = data;
                } else {
                    result.innerText = "response.ok が false";
                }
            });

        //]]>
    </script>
}

cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPages1.Models;

namespace RazorPages1.Pages.File
{
    public class UploadModel : PageModel
    {
        // Core では Server.MapPath が使えないことの対応
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadModel(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }
        
        public void OnGet()
        {
        }

        public string? Message { get; set; }

        [BindProperty]
        public UploadModels Model { get; set; } = default!;

        public async Task<IActionResult> OnPostAsync()
        {
            string result;
            IFormFile? postedFile = Model.PostedFile;
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザによっ
                // ては postedFile.FileName はクライアント側でのフル
                // パスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string rootPath = _hostingEnvironment.ContentRootPath;

                // アプリケーションルートの UploadedFiles フォルダに
                // ファイルを保存する
                string filePath = $"{rootPath}\\UploadedFiles\\{filename}";
                using (var fs = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(fs);
                }

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

            // クライアントスクリプトによるアップロードか否かを判定。
            // fetch API を使う場合は X-Requested-With: XMLHttpRequest
            // ヘッダを送るようクライアントスクリプト側でコーディング
            // が必要
            if (Request.Headers.XRequestedWith == "XMLHttpRequest")
            {
                return Content(result);
            }
            else
            {
                Message = result;
                return Page();
            }
        }
    }
}

Tags: , , ,

Upload Download

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

by WebSurfer 2. April 2024 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

About this blog

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

Calendar

<<  July 2024  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar