WebSurfer's Home

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

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

ASP.NET Core MVC で Bootstrap5 Modal の利用

by WebSurfer 2023年11月11日 10:46

Visual Studio 2022 を使って作成する ASP.NET Core MVC アプリで、データベースのテーブル一覧を表示し、その中の項目を選択すると、その詳細を Bootstrap5 の Modal を利用して表示する方法を書きます。

Bootstrap Modal に詳細を表示

この記事ではターゲットフレームワークを .NET 7.0 としています。.NET 5.0 以前をターゲットフレームワークとして作成したプロジェクトでは Bootstrap のバージョンが異なるので注意してください。

この記事のベースとしたアプリは、Microsoft のチュートリアル「CRUD 機能を実装する」です。そのアプリの Student テーブルのレコード一覧を表示する Index ページと各レコードの詳細を表示する Details ページを使います。

チュートリアルでは、Index ページに一覧表示されている各項目の右横のアクションリンク Details をクリックすると、別ページ Details に遷移し、遷移した Details ページ上で選択した項目の詳細を表示するようになっています。チュートリアルの「Details ビューに登録を追加する」セクションの下の方の画像を見てください。

それを、別ページ Details に遷移しないで、この記事の上の画像のように Index ページ上で Bootstrap Modal 内に詳細を表示するようにしてみます。

Bootstrap については、Visual Studio 2022 のテンプレートで、ターゲットフレームワーク .NET 7.0 として作成した ASP.NET Core MVC アプリのプロジェクトに Bootstrap.js, Boorstrap.css v5.1.0 が含まれています。Bootstrap Modal を使うために必要なものもそれらの中に含まれているので何も追加する必要はありません。

レイアウトページ _Layout.cshtml にはプロジェクトの Bootstrap.js, Boorstrap.css を参照する link タグ、script タグが含まれていますので、スキャフォールディングの際 _Layout.cshtml を使用するように設定すれば、それらが参照されるようになります。

Bootstrap5 Modal の詳しい説明は Bootstrap のドキュメント「Modal (モーダル)」にあります。普通の html + css + javascript のページに Bootstrap5 Modal を組み込むなら、そのドキュメントから十分な情報が得られると思います。

ASP.NET Core MVC アプリに、Bootstrap5 Modal を組み込んで、Index ページにのアクションリンクをクリックして詳細データを取得し、Index ページ上で Modal に表示するにはどうしたらいいかというのががこの記事のポイントで、それを以下に述べます。

まずコントローラーのアクションメソッドですが、Index はチュートリアルのものと全く同じ、Details も最後の一行で部分ビューを返すように変更する以外はチュートリアルのものと同じです。

コードは以下の通りとなります。この記事ではアクションメソッドの名前を Index3, Details3 としています。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;

namespace MvcNet7App.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

        // Student のレコード一覧を取得しビューに渡す
        public async Task<IActionResult> Index3()
        {
            return _context.Students != null ?
                   View(await _context.Students.ToListAsync()) :
                   Problem("Entity set 'SchoolContext.Students'  is null.");
        }

        // 詳細を表示する部分ビュー用のアクションメソッド
        public async Task<IActionResult> Details3(int? id)
        {
            if (id == null || _context.Students == null)
            {
                return NotFound();
            }

            //var student = await _context.Students
            //    .FirstOrDefaultAsync(m => m.ID == id);
            var student = await _context.Students
                                        .Include(s => s.Enrollments)
                                            .ThenInclude(e => e.Course)
                                        .AsNoTracking()
                                        .FirstOrDefaultAsync(m => m.ID == id);

            if (student == null)
            {
                return NotFound();
            }

            // 部分ビューを返すように変更
            return PartialView(student);
        }
    }
}

次に、上の Index アクションメソッドに対応するビューですが、スキャフォールディングで自動生成されたコードに (1) Modal 部分を実装、(2) Details アクションリンクを JavaScript のメソッドを呼び出すよう変更、(3) fetch API でサーバーに要求を出し、応答として返される部分ビューを Modal 内の div 要素に書き込んだ後 Modal を表示する JavaScript のメソッドを追加します。

コードは以下の通りです。Modal 部分は Bootstrap 5 のドキュメント「Modal (モーダル)」の「Static backdrop」セクションのサンプルコードをコピペし、若干修正をして利用しています。詳細を書き込む div 要素に id="details" を追加したこと、デフォルトの "medium" サイズでは幅が足らないので CSS に modal-lg を追加したこと、Understood ボタンは削除したことが主な違いです。

@model IEnumerable<MvcNet7App.Models.Student>

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

<h1>Index3</h1>

<!-- (1) Modal 部分を実装 -->
<div class="modal fade"
     id="staticBackdrop"
     data-bs-backdrop="static"
     data-bs-keyboard="false" tabindex="-1"
     aria-labelledby="staticBackdropLabel"
     aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"
                    id="staticBackdropLabel">
                    Student Details
                </h5>
                <button type="button"
                        class="btn-close"
                        data-bs-dismiss="modal"
                        aria-label="Close">
                </button>
            </div>
            <div class="modal-body" id="details">
                ...
            </div>
            <div class="modal-footer">
                <button type="button"
                        class="btn btn-primary"
                        data-bs-dismiss="modal">
                    Close
                </button>
            </div>
        </div>
    </div>
</div>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.LastName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.FirstMidName)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @* (2) クリックで JavaScript の showDetails
                    を呼び出す。引数には Student の ID を渡す *@ 
                    <a href="javascript:showDetails(@item.ID)">
                        Details
                    </a>
                </td>
            </tr>
        }
    </tbody>
</table>

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

        // (3) fetch API でサーバーに要求を出し、応答として返される
        // 部分ビューを Modal 内の div 要素に書き込んだ、後 Modal を
        // 表示する

        // Modal を表示するためのヘルパーメソッド
        const showBootstrapModal = () => {
            // staticBackdrop は Modal の div 要素の id
            let divModal = document.getElementById('staticBackdrop');
            let myModal = new bootstrap.Modal(divModal);
            myModal.show();
        }

        const showDetails = async (id) => {
            // 部分ビュー用アクションメソッドの url
            // id は Student テーブルのレコードの ID
            const url = "/Students/Details3/" + id;

            // Student 詳細を表示する Modal 内の div 要素
            const resultDiv = document.getElementById('details');

            const response = await fetch(url);
            if (response.ok) {
                // 部分ビューのテキストを取得
                const text = await response.text();

                // テキストを Modal 内の div 要素に書き込み
                resultDiv.innerHTML = text;

                // Modal を表示
                showBootstrapModal();
            }            
        }

    </script>
}

最後に、Details アクションメソッドに対応する部分ビューですが、以下の通りとなっています。チュートリアルで自動生成されたコードから部分ビューに不要な部分を削除しただけです。

@model MvcNet7App.Models.Student

<div>
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>

        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>

Tags: , , ,

CORE

JavaScript の関数内での this

by WebSurfer 2023年11月6日 17:41

JavaScript の関数内での this の使い方を備忘録として残しておきます。

MDN ドキュメント「this」に JavaScript の this についていろいろ書いてあります。関数内での this については「関数コンテキスト」のセクションに書いてあるように "関数の呼び出し方によって異なります" ということです。ただし、アロー関数の場合はそれは当てはまらないようです。

というわけで、関数内で this を使う場合は、常に「その this は何なのか?」ということを考えてコードを書く必要がありそうです。

どういうことか具体例を以下に書きます。

下の画像は、Visual Studio 2022 のテンプレートを使って作成した React + Web API のプロジェクトを実行し、React 側から fetch API を使って Web API から天気予報データを取得してブラウザに表示したものです。

天気予報デ���タを取得しブラウザに表示

Visual Studio が自動生成した React 側の JavaScript のコードは以下の通りです。React の componentDidMount のタイミングでコードの下の方にある populateWeatherData メソッドが呼び出されると、fetch API を使って Web API から天気予報データを取得し、取得したデータを this.setState メソッドを使って state に設定しています。

import React, { Component } from 'react';

export class FetchData extends Component {
    static displayName = FetchData.name;

    constructor(props) {
        super(props);
        this.state = { forecasts: [], loading: true };
    }

    componentDidMount() {
        this.populateWeatherData();
    }

    static renderForecastsTable(forecasts) {
        return (
            <table className="table table-striped" aria-labelledby="tableLabel">
                <thead>
                    <tr>
                        <th>Date</th>
                        <th>Temp. (C)</th>
                        <th>Temp. (F)</th>
                        <th>Summary</th>
                    </tr>
                </thead>
                <tbody>
                    {forecasts.map(forecast =>
                        <tr key={forecast.date}>
                            <td>{forecast.date}</td>
                            <td>{forecast.temperatureC}</td>
                            <td>{forecast.temperatureF}</td>
                            <td>{forecast.summary}</td>
                        </tr>
                    )}
                </tbody>
            </table>
        );
    }

    render() {
        let contents = this.state.loading
            ? <p><em>Loading...</em></p>
            : FetchData.renderForecastsTable(this.state.forecasts);

        return (
            <div>
                <h1 id="tableLabel">Weather forecast</h1>
                <p>This component demonstrates fetching data from the server.</p>
                {contents}
            </div>
        );
    }

    async populateWeatherData() {
        const response = await fetch('weatherforecast');
        const data = await response.json();
        this.setState({ forecasts: data, loading: false });
    }
}

上のコードの一番最後の行の this.setState メソッドの this が、関数の書き方によってどう変わってくるかを以下に書きます。

上のコード例で populateWeatherData メソッドは FetchData クラスのメソッドなので、MDN のドキュメント this の Class context のセクションに "the this value is the object that the method was accessed on." と書いてあるように、this には FetchData オブジェクトが設定されます。

this は FetchData オブジェクト

結果、Web API から取得したデータは this.setSate メソッドによって FetchData オブジェクトの state に設定され、この記事の一番上の画像のように天気予報データがブラウザに表示されます。

では、以下のように async/await は使わないで then() の中でコールバックを呼ぶように変更し、コールバックを function () { ... } という形にして、その function の中で this を使うとどうなるでしょう?

this は undefined

上の画像の通り this は undefined となります。結果、Web API から取得したデータの state への設定は失敗しますので、初期画面で Loading... と表示された状態で止まってしまいます。

MDN のドキュメント「関数コンテキスト」に書いてありますが、(1) strict モードでは、実行コンテキストに入るときに this 値が設定されていないと、undefined のままになり、(2) strict モードでない場合は、this は既定でグローバルオブジェクトすなわち window となるそうです。then() の中で非同期に実行される場合は「実行コンテキストに入るときに this 値が設定されていない」ということになるようで、上の画像の例では (1) という結果になっています。

次に、上の画像の例のコールバック function () { ... } を以下のようにアロー関数に代えて、その中で this を使うとどうなるでしょうか?

this は FetchData オブジェクト

上の画像の通り this は FetchData オブジェクトとなります。結果、Web API から取得したデータは FetchData オブジェクトの state に設定され、期待通り天気予報データがブラウザに表示されます。

MDN のドキュメント「アロー関数」によると "アロー関数では、this はそれを囲む構文上のコンテキストの this の値が設定されます" ということで、this には FetchData オブジェクトが設定されたということのようです。

アロー関数というのは従来の function() { ... } の簡潔な代替構文に過ぎないと思っていたのですが、MDN のドキュメント「アロー関数式」に書いてあるように意図的な使用上の制限があるそうです。その中の一つが this で、アロー関数自身では this の結び付けを行わず、包含する構文上のコンテキストの this の値を保持するのだそうです。

Tags: , , ,

JavaScript

About this blog

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

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar