😊

SvelteKitでCookieはどう生きるか

2023/09/25に公開

SvelteKit には、クライアントとサーバ両方の側面がありますが、今開発中のシステムは、SvelteKit をフロントとして、バックエンドに API サーバを置いています。
SvelteKit は、http://localhost:5137 にあり、API サーバは、 http://localhost:8000 にあります。ちなみに、本番環境では、SvelteKit は、https://example.com にあり、API サーバは、 https://api.example.com のような関係にあります。

SvelteKit のクライアントから SvelteKit のサーバに POST リクエストするときは、Cookie は自動で送られます。ただ、SvelteKit のサーバから API サーバに POST リクエストするときは、そもそもブラウザは関係ないので、Cookie が自動送信されたりしません。そのため、本来であれば、Cookie の送受信は、SvelteKit のサーバからクライアントにレスポンスする際に、手動で設定する必要があるかと思います。しかし、SvelteKit にはどうもその辺を自動でやってくれる機能があるようで、有難い気もしますが、逆に混乱する気もします。ということで、今混乱しているので、この投稿を書いております。混乱中なのでとんでもない勘違いを書いている可能性もありんす。

やっていること

今やっていることは下記です。
呼び方として、SvelteKit のクライアントは A, SvelteKit のサーバは B, API サーバは C と呼びます。

  • A<form method="post" action="?/hoge">みたいな感じで POST アクセスを飛ばしています。これは B に飛びます。
  • B に飛んできた内容を、ごにょごにょして、C に POST リクエストしています。

B に飛んできたときに、実行されるのは下記です。

+page.server.ts
import type { Actions } from './$types';
import { emailAction, codeAction } from 'services/authService';

export const actions = {
	hoge: async ({ fetch, request }) => {
    const data = await request.formData();
    const apple = data.get('apple');
    const url = "https://api.example.com/hoge";
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({ apple })
      });
      if (response.ok) {
        return { success: true, apple };
      } else {
        const message = (await response.json()).message;
        return { error: true, message };
      }
    } catch (e) {
      const message = '通信エラーが発生しました';
      return { error: true, message };
    }
	},
} satisfies Actions;

AB は同じサーバですし、A はブラウザなので当然 Cookie は自動で B に飛びます。はて、B から C への POST リクエストはどうでしょうか?
そもそも B はブラウザではないので、本来自動で Cookie は飛びません。よって、手動で設定しなければ Cookie 飛ばないはずです。
結果は、ローカル環境では飛ばないけど、本番環境では飛んでました。これはここに書いてある、「特別なバージョンの fetch」だからだと思っております。よく考えたら、上記コードも引数のオブジェクトから fetch を取り出して、それを使っております。これはもはや、特別な fetch に違いないですね。

特別な fetch 関数の自動引き渡し条件

なぜ本番環境でだけ飛ぶのか?という点については、ここに関係しそうなことが書いてあります。「提供されている fetch 関数」というのは、上記の「特別なバージョンの fetch」と同じという認識です。ここの NOTE に、「Cookie は、ターゲットホストが Sveltekit アプリケーションと同じか、より明確・詳細(specific)なサブドメインである場合にのみ引き渡されます。」と書いてあります。本番環境は、送信先(ターゲットホスト)が、api.example.com であり、example.comのサブドメインなので自動引き渡しされたのかなと思いました。ローカルは、localhost は同じですが、port が異なりますので、自動引き渡しされないのかなと思いました。

より詳細なルールがここに書いてありました。src/hooks.server.ts の中で、handleFetch 関数というのが使えますが、これは、特別な fetch の実行内容(リクエスト内容)を変更・置換するためのもののようです。それはさておき、特別なバージョンの fetch の Cookie 自動引き渡しのルールについて、下記が書いてありました。

  • 同一オリジン(same-origin)リクエストの場合、SvelteKit の fetch 実装は、credentials オプションを "omit" にしない限り、 cookie と authorization ヘッダーを転送します。
  • クロスオリジン(cross-origin)リクエストの場合、リクエスト URL がアプリのサブドメインに属するときは cookie はリクエストに含まれます。例えば、あなたのアプリが my-domain.com にあり、あなたの API が api.my-domain.com にある場合、cookie はリクエストに含まれることになります。
  • もしあなたのアプリと API が兄弟関係にあるサブドメイン (例えば www.my-domain.comapi.my-domain.com) の場合は、my-domain.com のような共通の親ドメインに属する cookie は含まれません、なぜなら SvelteKit にはその cookie がどのドメインに属するか判断する方法がないからです。こういったケースでは、handleFetch を使って手動で cookie を含める必要があります

なるほどー。なるほどー。同一オリジンなら自動引き渡し、クロスオリジンでも親子関係なら自動引き渡しということです。ですので、port が違うのはクロスオリジンであり、ドメイン以外が異なる時点で自動引き渡しされないってことじゃないかなーと思いました。(そうではありませんでした)

localhost と 127.0.0.1

今なぜか、ブラウザの A の URL が、http://localhost:5173 ではなく、http://127.0.0.1:5173 になっていることに気づきました。そして、localhostの場合は、5173ポートから8000ポートに送信しても、自動引き渡しされていました。127.0.0.1だと引き渡しされませんでした。

試しに、B から C に送るときに C の宛先をhttp://127.0.0.1:8000にしたら、今度はAの URL がhttp://localhost:5173だと引き渡しされませんでした。つまり、127.0.0.1という IP アドレス形式であってもACが一致していれば、ポートが違っても自動引き渡しされるようでした。

まとめ

今回は、この前実験した結果認識していたことと、異なることが起きたので、混乱しておりました。結局、URL を localhost にしていたのが、127.0.0.1 にしてしまっていることに気づかなかっただけでした。ただ、ポートが異なる場合でも特別な fetch はドメインが同じか、親子関係であれば、明確に拒否しない限り、自動引き渡しするのだということをしっかり認識できました。よかったです。

Discussion