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

Web API に CORS 実装 (CORE)

by WebSurfer 2023年1月21日 22:50

ASP.NET Core Web API に CORS を実装してみました。下の画像は検証のため別プロジェクトの MVC アプリから fetch API を使って Web API に要求を出してデータを取得し表示したものです。

Web API に CORS 実装

開発環境の IIS Express で動かしているのでホストは同じ localhost ですが、ポートが異なるので要求はクロスドメインになります。上の画像では fetch API のメソッドが PUT なので、ブラウザがまずプリフライトリクエストを出し、その応答を見て要求を出してデータを取得しています。

ASP.NET Core の場合はフレームワーク組み込みの CORS 用のミドルウェアが用意されていて、それを Microsoft のドキュメント「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」に従って有効にします。

(ちなみに、.NET Framework 版の ASP.NET Web アプリでは、先の記事「クロスドメインの WCF サービス」で書いたように自力で実装していました)

具体的には、Visual Studio 2022 のテンプレートでフレームワーク .NET 6.0 で作った ASP.NET Web API アプリであれば、上に紹介した Microsoft のドキュメントの「名前付きポリシーとミドルウェアを使用した CORS」のセクションに従って Program.cs に以下のコードを追加するだけで CORS は有効になります。

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://localhost:44343")
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
});

// ・・・中略・・・

app.UseCors(MyAllowSpecificOrigins);

「名前付き」にすると、[EnableCors("{Policy String}")] 属性をコントローラーに付与しないと CORS は有効にならないと思っていたがそうではなかったです。コントローラーには何も付与しなくても上の設定だけで CORS は有効になります。

WithOrigins メソッドに設定した https://localhost:44343 は CORS でのアクセスを許可する要求元です。すべての要求元を許可する AllowAnyOrigin メソッドもあります。詳しくは Microsoft のドキュメント「CorsPolicyBuilder クラス」を見てください。

AllowAnyHeader メソッド、AllowAnyMethod メソッドはプリフライトが必要になる場合は必要です(Any ではなく特定の Header, Method を指定することもできます)。それらが無いとプリフライトの応答ヘッダに、

Access-Control-Allow-Headers
Access-Control-Allow-Methods

・・・が含まれないのでプリフライトの後の要求がブラウザから出ず失敗します。

上の画像のプリフライトリクエストの要求・応答ヘッダを Fiddler でキャプチャした画像を下に貼っておきます。

プリフライトリクエスト

要求側はブラウザの仕事で開発者は何もする必要はありません。プリフライトが必要か否かもブラウザが判断して CORS に必要な要求を出してくれます。

Response Headers の Security の項目は、要求を受けてサーバー側のミドルウェア(上のコード参照)が設定したものです。

参考に検証に使ったコードを下に載せておきます。Windows 10 22H2 の Edge 109.0.1518.61, Chrome 109.0.5414.75, Firefox 109.0, Opera 94.0.4606.65 で確認しました。

Web API

using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers
{
    public class Hero
    {
        public int Id { get; set; }
        public string? Name { get; set; }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
        };

        // GET: api/values (Read...すべてのレコードを取得)
        [HttpGet]
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        [HttpGet("{id}")]
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values (Create...レコード追加)
        [HttpPost]
        public List<Hero> Post([FromBody] Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        [HttpPut("{id}")]
        public List<Hero> Put(int id, [FromBody] Hero postedHero)
        {
            heroes[id - 1].Name = postedHero.Name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        [HttpDelete("{id}")]
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

クライアント側 (MVC の View)

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

<h1>ApiCors</h1>

<input type="button" value="READ ALL" onclick="apiHeroesGet();" />
<input type="button" value="READ 5" onclick="apiHeroesGet5();" />
<input type="button" value="UPDATE 5" onclick="apiHeroesPut5();" />
<input type="button" value="DELETE 5" onclick="apiHeroesDelete5();" />
<input type="button" value="CREATE 6" onclick="apiHeroesPost();" />

<ul id="heroes"></ul>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[
        const url = "https://localhost:44371/api/values"; // IIS Express
        //const url = "https://localhost:7216/api/values";    //Kestrel
        const elem = document.querySelector("#heroes");

        const apiHeroesGet = async () => {
            const response = await fetch(url);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend", 
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesGet5 = async () => {
            const response = await fetch(url + "/5");
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}</li>`);
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesPut5 = async () => {
            const params = {
                method: "PUT",
                body: '{"Id":5,"Name":"Updated Hero"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url + "/5", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesDelete5 = async () => {
            const params = {
                method: "DELETE"
            }
            const response = await fetch(url + "/5", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesPost = async () => {
            const params = {
                method: "POST",
                body: '{"Id":6,"Name":"Created Hero"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url, params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++)
                {
                    elem.insertAdjacentHTML("beforeend", 
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        //]]>
    </script>
}

本題の CORS の実装とは直接関係ないことですが、fetch メソッドの引数の要求 URL の書き方に注意すべきことがあったのでそれを書いておきます。

PUT と DELETE については、Web API 側のアクションメソッドの引数に int id が含まれているので、要求 URL に api/values/5 というように id を渡す必要があります。そうしないと、プリフライトリクエストは成功しますが、それを受けてブラウザが要求に行った際に応答が 405 Method Not Allowed、Allow: GET, POST となって失敗します。

何故それで Method Not Allowed となるのか、GET, POST でないとダメと言われるのかは分かりませんが・・・

Tags: ,

Web API

クロスドメインの WCF サービス

by WebSurfer 2016年12月27日 21:58

先の記事「WCF と jQuery AJAX」で書いた ASP.NET がホストする WCF サービスを、クロスドメインで利用する方法について書きます。

プリフライトリクエスト

Tsuyoshi Matsuzaki さん Blog の記事「JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意」では以下の 3 つの方法が紹介されています。それぞれの概要が説明されていて分かりやすいので一読されるといいと思います。

  1. Cross Document Messaging (XDM)
  2. Cross-Origin Resource Sharing (CORS)
  3. JSONP

ここでは 2 番目の Cross-Origin Resource Sharing (CORS) という方法を使います。上の画像は CORS でのプリフライトリクエストとそれに対する応答を Fiddler で見たものです。

(最初は jQuery.ajax を利用した JSONP が一番簡単かと思ったのですが、jQuery.ajax では応答のスクリプトにコールバック jQuery1830...26( ... ) で囲んだ JSON 文字列を期待しているのに、WCF サービスは JSON 文字列しか返さないので使えませんでした)

CORS は HTML 5 の仕様なので依然として使えないブラウザがありますが、対応ブラウザが増えるにつれ今後の本命になるだろうということです。(ちなみに、IE9 は対応してませんでした。上に紹介した MSDN Blog の記事によると独自の XDomainRequest オブジェクトを使用するという回避方法はあるそうですが)

まず、この記事を書くに当たって自分が参考にさせていただいた記事へのリンクを張っておきます。私の説明では分からない場合はそちらを読まれるといいと思います。手抜きでスミマセン。(汗)

  1. オリジン間リソース共有 (CORS)
  2. CORS(Cross-Origin Resource Sharing)について整理してみた

ブラウザが CORS をサポートしていれば、クライアント側の処理はブラウザが自動的に行いますので、開発者はクライアント側の対応は何もする必要がないです。

サーバー側すなわち ASP.NET Web アプリで CORS に必要な応答ヘッダ返せるようにするだけで対応できます。基本的には Global.asax ファイルに以下のコードを記述してやれば可能になります。(厳格なセキュリティを要求せず無条件で応答するというような場合はもっと簡単になります)

protected void Application_BeginRequest(object sender, 
                                        EventArgs e)
{
  CrossOriginResourceSharingSetting();
}

private void CrossOriginResourceSharingSetting()
{
  HttpRequest request = HttpContext.Current.Request;
  HttpResponse response = HttpContext.Current.Response;

  // プリフライトリクエストの場合
  if (request.HttpMethod == "OPTIONS")
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った
    // 方がいいかもしれないが省略。要求ヘッダの origin フィ
    // ールドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {
      string method = 
       request.Headers["Access-Control-Request-Method"];
      string header = 
        request.Headers["Access-Control-Request-Headers"];

      // 要求ヘッダに Access-Control-Request-Method および
      // Access-Control-Request-Headers がなければ何もしない
      if (!string.IsNullOrEmpty(method) && 
          !string.IsNullOrEmpty(header))
      {
        // method が GET または POST 以外は対応しない
        // header もチェックした方がいいかもしれない・・・
        if (method.Contains("GET") || method.Contains("POST"))
        {                            
          response.AddHeader("Access-Control-Allow-Origin", 
                             origin);                            
          response.AddHeader("Access-Control-Allow-Methods", 
                             method);                            
          response.AddHeader("Access-Control-Allow-Headers", 
                             header);

          // Access-Control-Max-Age ヘッダも任意で追加できる。
          // 指定した時間(秒)プリフライトの結果をキャッシュで
          // きるので、実際に運用に移す際は追加する方がよさそう
          //response.AddHeader("Access-Control-Max-Age", "600");

          response.End();
        }
      }
    }                
  }
  else
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った方
    // がいいかもしれないが省略。要求ヘッダの origin フィール
    // ドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {                    
      response.AddHeader("Access-Control-Allow-Origin",
                         origin);
    }
  }            
}

上記のコードが何をしているかと言うと、要求が「プリフライトリクエスト」になる場合と「シンプルなリクエスト」になる場合とに処置を分けて、CORS に必要な応答ヘッダを設定しているだけです。詳しくはコード中のコメントに書きましたのでそれを見てください。

CORS をサポートしているブラウザがクロスドメインでどのような処置を行うかは、要求が「シンプルなリクエスト」になるか否かによって異なってきます。要求が「シンプルなリクエスト」になる条件は、上に紹介した記事「HTTP アクセス制御 (CORS)」を読んでください。

例えば、先の記事「WCF と jQuery AJAX」で言うと「(3) WCF AJAX サービスへのアクセス」のコードで getAllCars メソッドを使う方が「シンプルなリクエスト」になります。

getCarsByDoors メソッドを使う方は要求ヘッダに contentType: "application/json; charset=utf-8" が含まれるので「シンプルなリクエスト」にはならず、「プリフライトリクエスト」が事前に行われます。

(1) シンプルなリクエスト

要求が「シンプルなリクエスト」になる場合、CORS をサポートしているブラウザからは要求ヘッダに origin: http://<要求元のドメイン> を追加する以外には普通にサーバーに要求を出します。(ただし、IE9 では要求さえ出ませんので注意)

サーバー側では要求ヘッダに含まれる origin フィールドを見て、 要求元のドメインがクロスドメインアクセスを許可されているか判定し、受け入れの判断をすることができます。

ただし、上記コードは origin フィールドの内容の検証はしていません。origin フィールドの内容(要求元のドメイン)をそのまま応答ヘッダの Access-Control-Allow-Origin に設定して返しています。それだけでも、* を設定するよりはよさそうです。

ブラウザは応答ヘッダの Access-Control-Allow-Origin: http://<求元のドメイン> を見てクロスドメインアクセスに成功したと判断し、応答に含まれる JSON 文字列などのコンテンツを処置します。

(2) プリフライトリクエスト

要求が「シンプルなリクエスト」にならない場合、CORS をサポートしているブラウザからは実際のリクエストを送信しても安全かを確かめるための「プリフライトリクエスト」が事前に行われます。

上の画像はその要求と応答を Fiddler で見たものです。

左下のウィンドウはブラウザがクロスドメインに対する要求でかつ「シンプルなリクエスト」にならないと判定して自動的にプリフライトリクエストを要求した要求ヘッダの内容です。

赤枠で囲った部分を見てください。HTTP メソッドの OPTIONS、要求ヘッダに含まれる origin, Access-Control-Request-Method, Access-Control-Request-Headers に注目です。

サーバーがクロスドメインからのリクエストを受け、それがプリフライトリクエストであるかどうかを判断するにはそれらの情報をチェックします。

そして、プリフライトリクエストであると判断した場合はは上の画像の右下のウィンドウの赤枠で囲ったような応答ヘッダを返します。ここでは、Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers は要求ヘッダで受けたものと同じ内容に設定しています。

ブラウザはそれらの情報を見て実際のリクエストを送信しても安全かを確認し、サーバーに要求を出して(上の画像で #3 がそれ)応答を取得し、コンテンツに含まれる JSON 文字列などの情報を処置します。

Tags: , , ,

AJAX

About this blog

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

Calendar

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

View posts in large calendar