🚀

Dartでhttpライブラリを使用した際の複数set-cookieの対応方法

2022/04/26に公開

http ライブラリから返却された複数の set-cookie

始めに

どうも、真也です。

今回は Dart言語HTTP通信[1] をする際のデファクトスタンダードである http[2] ライブラリが抱える Cookie に関する問題暫定的な解決策について記事を書いていきます。

https://pub.dev/packages/http

まず http ライブラリとはなにか

この http ライブラリはとても洗練されていて完成度が高く、非常に簡単に HTTP通信 を実装することができます。

例えば、Dart言語GET通信 をしたければ http を使用して以下の処理を記述するだけで動作します。

import 'package:http/http.dart' as http;

void main() async {
    final response = await http.get(Uri.parse('https://example.com'));
}

POST通信 をしたければ以下のような感じです。

import 'package:http/http.dart' as http;

void main() async {
  final response = await http.post(Uri.parse('https://example.com'), body: {
    'name': 'doodle',
    'color': 'blue',
  });
}

簡潔で素晴らしいですね。

なにが問題なのか?

しかし、とても残念な仕様もいくつかあります。その残念な仕様の一つが今回の記事の主題となる Cookie の扱われ方 です。

http ライブラリを使用して HTTP通信 をした際に、レスポンスヘッダーに set-cookie が一つだけ設定されて返却される場合は問題ないのですが、複数の set-cookie が設定される場合にはカンマ区切りで結合されて返却されます

具体的には以下のような場合です。

問題となる場面

例えば、以下のような複数の set-cookie が返却されてくる場面が問題になります。

set-cookie: AWSALB=TEST; Expires=Tue, 03 May 2022 02:03:35 GMT; Path=/
set-cookie: AWSALBCORS=TEST; set-cookie: Expires=Tue, 03 May 2022 02:03:35 GMT; Path=/; SameSite=None; Secure
set-cookie: jwt_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT; SameSite=lax; Secure
set-cookie: csrf_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT
set-cookie: csrf_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT

これら複数の set-cookiehttp ライブラリから返却された headers フィールドから使用しようとするとこのようになります。

import 'package:http/http.dart' as http;

void main() async {
  final response = await http.get(Uri.parse('https://example.com'));

  print(response.headers['set-cookie']);
}
AWSALB=TEST; Expires=Tue, 03 May 2022 02:03:35 GMT; Path=/,AWSALBCORS=TEST; set-cookie: Expires=Tue, 03 May 2022 02:03:35 GMT; Path=/; SameSite=None; Secure,jwt_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT; SameSite=lax; Secure,csrf_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT,csrf_token=TEST; Domain=.example.com; Max-Age=31536000; Path=/; expires=Wed, 26-Apr-2023 02:03:35 GMT,

もともと別々の set-cookie で設定されていた Cookie がカンマ区切りで結合されているのがわかりますね。このままの形式では使い物にならないのでどうにかして変換する必要があります。

暫定的な解決策

問題となっているカンマ区切りで結合された set-cookie を変換するのは一筋縄ではいきません。「カンマ区切りで結合されているのであればカンマで分割すればいいではないか」と考える方もいるかもしれませんが、Cookie の各フィールドにカンマが含まれている場合があるので有効な手段ではありません。

そこで、暫定的な解決策として現状としては以下の 2 つの選択肢があります。

  1. 正規表現で分割する
  2. 私が作ったライブラリを使用する

いずれも根本的な解決ではないのですが、動作確認ができている暫定的な解決策として順に紹介していきます。

解決策 1: 正規表現で分割

開発しているアプリやパッケージに依存性を増やしたくないという方にとっては正規表現で分割するのが最適な手段になるでしょう。

具体的には以下の実装で問題となっている結合された set-cookie を分割して使用できます。

import 'dart:io';

import 'package:http/http.dart' as http;

/// 結合されたset-cookieを分割する正規表現
final _regexSplitSetCookies = RegExp(',(?=[^ ])');

void main() async {
  final response = await http.get(Uri.parse('https://example.com',));

  final List<Cookie> cookies = [];

  final setCookie = _getSetCookie(response.headers);
  if (setCookie.isNotEmpty) {
    for (final cookie in setCookie.split(_regexSplitSetCookies)) {
      cookies.add(Cookie.fromSetCookieValue(cookie));
    }
  }

  // 結合されたset-cookieが個別のset-cookieに分割されているのを確認できます。
  print(cookies);
}

String _getSetCookie(final Map<String, dynamic> headers) {
  for (final key in headers.keys) {
    // システムによって返却される "set-cookie" のケースはバラバラです。
    if (key.toLowerCase() == 'set-cookie') {
      return headers[key] as String;
    }
  }

  return '';
}

解決策 2: ライブラリを使用

依存ライブラリを増やすことも選択肢の内に入る方は私が過去に作成したライブラリを使用できます。

https://pub.dev/packages/sweet_cookie_jar

ライブラリの中身で行われる処理は先に紹介した正規表現で分割する方法とまったく同じになります。

例えば、以下のように使用できます。

import 'package:http/http.dart' as http;
import 'package:sweet_cookie_jar/sweet_cookie_jar.dart';

void main() async {
  final response = await http.get(Uri.parse('https://example.com'));

  // やることはResponseオブジェクトを渡すだけです。
  final cookieJar = SweetCookieJar.from(response: response);

  print(cookieJar.find(name: 'csrf_token'));
}

Dart 開発チームにプルリクエストを送りました

この問題を解決するための最も良い方法は http ライブラリ自体の処理を修正してこの困難さを解決することです。

ここまでいくつかの暫定的な解決策を並べてきましたが、ライブラリの不出来な部分を個々人の実装で補うというのはスマートではありません。長く愛され使われるライブラリだからこそ、より開発者が扱いやすい機能を標準で提供すべきです。

そのため、私は先に紹介した SweetCookieJar の一部のアルゴリズムを http ライブラリに移植するプルリクエストを送り、現在 Dart開発チーム とマージに向けた調整と協議をしています。

https://github.com/dart-lang/http/pull/688

貢献者の募集

この記事とは別件になるのですが、私は Dart言語 製のジョブスケジューリングフレームワーク Batch.dart を開発しています。

Batch.dartオープンソースですのでどのような方でも開発に貢献することができます。開発リポジトリの公用語は英語にしていますが、日本人の方々も大歓迎ですのでお気軽に IssuePull Request を作成してください。

また、IssuePull Request はハードルが高いがそれでも何か貢献したいという方は、GitHub の開発リポジトリにスターを付けることや、Pub.devいいねを付けることを検討してください。これは Batch.dart の開発コミュニティを活性化するためにとても大きな意味があります。

https://github.com/batch-dart/batch.dart

https://pub.dev/packages/batch

Batch.dart に関しては過去に以下の記事を書きましたので参考にしてください。

https://zenn.dev/kato_shinya/articles/what-is-batch-dart

スポンサーの募集

国内外を問わずオープンソース開発をサポートしてくださるスポンサーを募集しています。少額からの寄付も可能ですので、以下のリンクから是非ご支援ください。

https://github.com/sponsors/myconsciousness

また、この記事にバッジを贈っていただくことでも支援は可能です。

脚注
  1. Hypertext Transfer Protocol ↩︎

  2. Pub.dev - http ↩︎

GitHubで編集を提案

Discussion