Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkgs/shelf/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## 1.4.3-wip

* Require `sdk: ^3.9.0`.
* Join multiple `Cookie` request-header values with `'; '` instead of `','`
in the single-value `headers` map, following the cookie-string grammar of
[RFC 6265 section 5.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.4).

## 1.4.2

Expand Down
2 changes: 1 addition & 1 deletion pkgs/shelf/lib/src/headers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final _emptyHeaders = Headers._empty();
class Headers extends UnmodifiableMapView<String, List<String>> {
late final Map<String, String> singleValues = UnmodifiableMapView(
CaseInsensitiveMap.from(
map((key, value) => MapEntry(key, joinHeaderValues(value)!)),
map((key, value) => MapEntry(key, joinHeaderValues(value, name: key)!)),
),
);

Expand Down
16 changes: 12 additions & 4 deletions pkgs/shelf/lib/src/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ Map<String, Object> removeHeader(Map<String, Object>? headers, String name) {
String? findHeader(Map<String, List<String>?>? headers, String name) {
if (headers == null) return null;
if (headers is ShelfUnmodifiableMap) {
return joinHeaderValues(headers[name]);
return joinHeaderValues(headers[name], name: name);
}

for (var key in headers.keys) {
if (equalsIgnoreAsciiCase(key, name)) {
return joinHeaderValues(headers[key]);
return joinHeaderValues(headers[key], name: name);
}
}
return null;
Expand Down Expand Up @@ -138,9 +138,17 @@ List<String> expandHeaderValue(Object v) {

/// Multiple header values are joined with commas.
/// See https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-p1-messaging-21#page-22
String? joinHeaderValues(List<String>? values) {
///
/// The `Cookie` header is the one exception: the cookie-string grammar of
/// RFC 6265 separates cookie-pairs with `; ` and does not allow commas, so
/// when [name] is `cookie` multiple values are recombined with `; ` as
/// specified in https://datatracker.ietf.org/doc/html/rfc9113#section-8.2.3
String? joinHeaderValues(List<String>? values, {String? name}) {
if (values == null) return null;
if (values.isEmpty) return '';
if (values.length == 1) return values.single;
return values.join(',');
final separator = name != null && equalsIgnoreAsciiCase(name, 'cookie')
? '; '
: ',';
return values.join(separator);
}
16 changes: 16 additions & 0 deletions pkgs/shelf/test/message_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ void main() {
'content-length': ['0'],
});
});

test('multiple cookie header values are joined with "; "', () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 must literal semicolons in cookie-value be replaced with %3B

Is that escaping done at another level?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — that escaping (when it happens at all) is done by whoever writes the cookie, not at this layer. RFC 6265 doesn't actually mandate %3B or any specific escape: the cookie-octet grammar in §4.1.1 excludes ; entirely, and its guidance for servers that want to store such data is that they "SHOULD encode that data, for example, using Base64" at Set-Cookie time. The user agent sends the encoded form back untouched, so in a conforming request ; only ever appears between cookie-pairs (cookie-string = cookie-pair *( ";" SP cookie-pair ), section 4.2.1), which is what keeps the RFC 9113 section 8.2.3 recombination this PR performs unambiguous. shelf keeps treating the values as opaque strings and adds or removes no encoding, same as before this change. A raw ; inside a value would already be indistinguishable from a pair boundary within a single Cookie line, so the join doesn't create a new case.

final message = _createMessage(
headers: {
'Cookie': ['session=123', 'theme=dark'],
},
);
expect(
message.headers,
containsPair('cookie', 'session=123; theme=dark'),
);
expect(
message.headersAll,
containsPair('cookie', ['session=123', 'theme=dark']),
);
});
});

group('context', () {
Expand Down
Loading