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

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

JavaScript の async / await

by WebSurfer 2022年11月25日 18:50

JavaScript の async / await の使い方について思い違いをしていました。また同じミスを犯さないように、どういうことだったかを以下に書いておきます。

例として、GET 要求を受けると 2 秒後に以下の JSON 文字列を返す Web API から fetch を使って応答を取得するサンプルを書きました。

{"id":5,"name":"スライムマン"}

JavaScript のコードは以下のようにし、ボタンクリックなどのイベントで asyncTest を実行します。

const fetchHero5 = async () => {
    const response = await fetch("/api/values/5")
    const data = await response.json();
    console.log(data.name);
};

const writeLog = () => {
    console.log("writeLog の実行");
}

// 同期的に順番に実行される
function asyncTest()
{
    fetchHero5();
    writeLog();
}

fetchHero5 の中で await で待機しているので、asyncTest の中の fetchHero5 の呼び出しでまず Web API からの応答の "スライムマン" がコンソールに書き込まれ、その後 writeLog が呼び出されて "writeLog の実行" が書き込まれると思っていたのですが、そうではなかったです。

結果は以下の通りで、思っていたのと逆の順序でした。

コンソール書き込み

asyncTest は同期関数でその中の fetchHero5() と writeLog() は同期的に実行されます。つまり、同期的に順番にまず fetchHero5 が呼ばれ、fetchHero5 の完了後 writeLog が呼ばれるという動きになります。

ここで、fetchHero5 は async が付与された非同期関数であるということが、自分が思っていた結果と違ったことになった原因のようです。

現代の JavaScript チュートリアル の記事 Async/await に "async は関数が promise を返すことを保証し、非 promise をその中にラップします" という説明があります。

ということは、fetchHero5 が呼ばれると即 Promise が返され、その時点で fetchHero5 は完了とみなされて、次の writeLog が呼ばれてコンソールに "writeLog の実行" と書き込んだということのようです。

その後、Web API からの応答の処理は fetchHero5 から返された Promise が行うようです。Promise オブジェクトが fetch で Web API を呼んで 2 秒後に応答が返ってきてから "スライムマン" と書き込んだということだと思います。

fetchHero5() で "スライムマン" と書き込んだ後で、writeLog() で "writeLog の実行" と書き込む(上の画像の順序を反対にする)には、asyncTest に async を付与してを非同期関数にし、その中の fetchHero5 に await を付与してそこで待機するようにします。

すなわち以下のようなコードにします。

async function asyncTest()
{
    await fetchHero5();
    writeLog();
}

結果は以下の通りとなります。

コンソール書き込み

上のコード例で、一番下位の関数 fetch, json は Promise を返します。その上位の関数 fetchHero5 も async が付与されているので Promise を返します。そういう形で Promise が下位から上位に伝搬して行くので、上のコード例でのように最上位の関数 asyncTest を非同期にして、そこで await を使って fetchHero5 の完了を待機すると期待したようになるということのようです。

.NET アプリの Microsoft のドキュメント「非同期プログラミングのベスト プラクティス」には "上から下に (または下から上に) 非同期コードが他の非同期コードを呼び出す、または呼び出される場合に最もうまく機能する" ということがに書かれています。

それは JavaScript でも同じことなのかもしれません。

(自分用のメモ: 検証に使ったのは VS2022 の MvcCore6App2 プロジェクトの Home/Api)

Tags: , , ,

JavaScript

About this blog

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

Calendar

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

View posts in large calendar