Fix multi-header handling in shelf_proxy (#519)
diff --git a/pkgs/shelf_proxy/CHANGELOG.md b/pkgs/shelf_proxy/CHANGELOG.md
index 1736364..37e518f 100644
--- a/pkgs/shelf_proxy/CHANGELOG.md
+++ b/pkgs/shelf_proxy/CHANGELOG.md
@@ -1,5 +1,8 @@
-## 1.0.5-wip
+## 1.0.5
+* Correctly handle multiple `Cookie` headers in requests and `Set-Cookie`
+ headers in responses.
+* Require `package:http` `^1.2.0`.
* Require Dart `^3.3.0`.
## 1.0.4
diff --git a/pkgs/shelf_proxy/lib/shelf_proxy.dart b/pkgs/shelf_proxy/lib/shelf_proxy.dart
index 7bd3c86..12f97f6 100644
--- a/pkgs/shelf_proxy/lib/shelf_proxy.dart
+++ b/pkgs/shelf_proxy/lib/shelf_proxy.dart
@@ -41,9 +41,16 @@
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
final requestUrl = uri.resolve(serverRequest.url.toString());
final clientRequest = http.StreamedRequest(serverRequest.method, requestUrl)
- ..followRedirects = false
- ..headers.addAll(serverRequest.headers)
- ..headers['Host'] = uri.authority;
+ ..followRedirects = false;
+
+ serverRequest.headersAll.forEach((name, values) {
+ // The Cookie header is special: multiple cookies are joined with a
+ // semicolon and a space, while other headers use a comma.
+ // See https://datatracker.ietf.org/doc/html/rfc6265#section-5.4
+ clientRequest.headers[name] =
+ values.join(name.toLowerCase() == 'cookie' ? '; ' : ', ');
+ });
+ clientRequest.headers['Host'] = uri.authority;
// Add a Via header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
@@ -57,42 +64,47 @@
.whenComplete(clientRequest.sink.close)
.ignore();
final clientResponse = await nonNullClient.send(clientRequest);
+ final headers = clientResponse.headers;
+ final headersAll =
+ Map<String, List<String>>.from(clientResponse.headersSplitValues);
+
// Add a Via header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
- _addHeader(clientResponse.headers, 'via', '1.1 $proxyName');
+ headersAll.update('via', (values) => [...values, '1.1 $proxyName'],
+ ifAbsent: () => ['1.1 $proxyName']);
// Remove the transfer-encoding since the body has already been decoded by
// [client].
- clientResponse.headers.remove('transfer-encoding');
+ headersAll.remove('transfer-encoding');
// If the original response was gzipped, it will be decoded by [client]
// and we'll have no way of knowing its actual content-length.
- if (clientResponse.headers['content-encoding'] == 'gzip') {
- clientResponse.headers.remove('content-encoding');
- clientResponse.headers.remove('content-length');
+ if (headersAll['content-encoding']?.contains('gzip') ?? false) {
+ headersAll.remove('content-encoding');
+ headersAll.remove('content-length');
// Add a Warning header. See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
- _addHeader(
- clientResponse.headers, 'warning', '214 $proxyName "GZIP decoded"');
+ headersAll.update(
+ 'warning', (values) => [...values, '214 $proxyName "GZIP decoded"'],
+ ifAbsent: () => ['214 $proxyName "GZIP decoded"']);
}
// Make sure the Location header is pointing to the proxy server rather
// than the destination server, if possible.
- if (clientResponse.isRedirect &&
- clientResponse.headers.containsKey('location')) {
- final location =
- requestUrl.resolve(clientResponse.headers['location']!).toString();
+ if (clientResponse.isRedirect && headers.containsKey('location')) {
+ final location = requestUrl.resolve(headers['location']!).toString();
if (p.url.isWithin(uri.toString(), location)) {
- clientResponse.headers['location'] =
- '/${p.url.relative(location, from: uri.toString())}';
+ headersAll['location'] = [
+ '/${p.url.relative(location, from: uri.toString())}'
+ ];
} else {
- clientResponse.headers['location'] = location;
+ headersAll['location'] = [location];
}
}
return Response(clientResponse.statusCode,
- body: clientResponse.stream, headers: clientResponse.headers);
+ body: clientResponse.stream, headers: headersAll);
};
}
diff --git a/pkgs/shelf_proxy/pubspec.yaml b/pkgs/shelf_proxy/pubspec.yaml
index 2e4125f..3fa2457 100644
--- a/pkgs/shelf_proxy/pubspec.yaml
+++ b/pkgs/shelf_proxy/pubspec.yaml
@@ -1,5 +1,5 @@
name: shelf_proxy
-version: 1.0.5-wip
+version: 1.0.5
description: A shelf handler for proxying HTTP requests to another server.
repository: https://github.com/dart-lang/shelf/tree/master/pkgs/shelf_proxy
issue_tracker: https://github.com/dart-lang/shelf/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ashelf_proxy
@@ -12,7 +12,7 @@
sdk: ^3.3.0
dependencies:
- http: '>=0.13.0 <2.0.0'
+ http: ^1.2.0
path: ^1.8.0
shelf: ^1.0.0
diff --git a/pkgs/shelf_proxy/test/shelf_proxy_test.dart b/pkgs/shelf_proxy/test/shelf_proxy_test.dart
index 822d262..5f3ceaa 100644
--- a/pkgs/shelf_proxy/test/shelf_proxy_test.dart
+++ b/pkgs/shelf_proxy/test/shelf_proxy_test.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
@@ -38,6 +39,21 @@
await get(headers: {'foo': 'bar', 'accept': '*/*'});
});
+ test('joins multiple request cookies with a semicolon', () async {
+ final client = MockClient((request) {
+ expect(request.headers,
+ containsPair('cookie', 'cookie1=value1; cookie2=value2'));
+ return Future.value(http.Response(':)', 200));
+ });
+
+ final handler = proxyHandler('http://dartlang.org', client: client);
+ final request =
+ shelf.Request('GET', Uri.parse('http://localhost/'), headers: {
+ 'cookie': ['cookie1=value1', 'cookie2=value2']
+ });
+ await handler(request);
+ });
+
test('forwards request body', () async {
await createProxy((request) {
expect(request.readAsString(), completion(equals('hello, server')));
@@ -64,6 +80,28 @@
expect(response.headers, containsPair('accept', '*/*'));
});
+ test('preserves multiple Set-Cookie headers in response, including dates',
+ () async {
+ await createProxy((request) {
+ return shelf.Response.ok(':)', headers: {
+ 'set-cookie': [
+ 'cookie1=value1; Expires=Mon, 20 Apr 2026 17:22:59 GMT',
+ 'cookie2=value2'
+ ],
+ });
+ });
+
+ final client = HttpClient();
+ final request = await client.getUrl(proxyUri);
+ final response = await request.close();
+
+ final setCookies = response.headers['set-cookie'];
+ expect(setCookies, [
+ 'cookie1=value1; Expires=Mon, 20 Apr 2026 17:22:59 GMT',
+ 'cookie2=value2'
+ ]);
+ });
+
test('forwards response body', () async {
await createProxy((request) => shelf.Response.ok('hello, client'));