FormatException when the cookie value in Set-Cookie header contains a space.The following is from a Flutter app, but the underlying code throwing the error is in the Dart SDK.
I/flutter (24821): FormatException: Invalid character in cookie value, code unit: '32' (at character 16)
I/flutter (24821): cookievaluewith spaceinit
I/flutter (24821): ^
https://github.com/dart-lang/sdk/blob/master/sdk/lib/_http/http_headers.dart
for (int i = start; i < end; i++) {
int codeUnit = newValue.codeUnits[i];
if (!(codeUnit == 0x21 ||
(codeUnit >= 0x23 && codeUnit <= 0x2B) ||
(codeUnit >= 0x2D && codeUnit <= 0x3A) ||
(codeUnit >= 0x3C && codeUnit <= 0x5B) ||
(codeUnit >= 0x5D && codeUnit <= 0x7E))) {
throw new FormatException(
"Invalid character in cookie value, code unit: '$codeUnit'",
newValue,
i);
}
}
Spaces in cookie values (or names) do violate RFC 6265. No argument there.
However most clients comply with RFC 2965 instead (or even 2109), which is more lenient. Then they add additional leniency. As a Client, we sometimes have to deal with malformed responses, therefore leniency is good. Chrome and Firefox don't crash, they just accept the cookie with spaces (or other illegal characters) and deal with it. But with Dart, the error is thrown so low in the stack that any upstream Dart/Flutter library (Dio, http) can't catch the error, at least not with access to the rest of the Response.
Http libraries in other languages are more accommodating with the characters they accept, and also by avoiding throwing an error if possible, in favor of returning NULL or something else:
val cookieValue = setCookie.trimSubstring(pairEqualsSign + 1, cookiePairEnd)
if (cookieValue.indexOfControlOrNonAscii() != -1) return null
and indexOfControlOrNonAscii contains:
indexOfControlOrNonAscii: if (c <= '\u001f' || c >= '\u007f')
(space is char \u0020 or 0x20, thus not rejected by okhttp)
source-1
source-2
# These quoting routines conform to the RFC2109 specification, which in
# turn references the character definitions from RFC2068. They provide
# a two-way quoting algorithm. Any non-text character is translated
# into a 4 character sequence: a forward-slash followed by the
# three-digit octal equivalent of the character. Any '\' or '"' is
# quoted with a preceding '\' slash.
# Because of the way browsers really handle cookies (as opposed to what
# the RFC says) we also encode "," and ";".
#
# These are taken from RFC2068 and RFC2109.
# _LegalChars is the list of chars which don't require "'s
# _Translator hash-table for fast quoting
#
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
) spaces inside cookie valuesif (!(codeUnit == 0x21 || to if (!(codeUnit == 0x21 || codeUnit == 0x20 ||a;b c => abcSimilar issues have been raised before, and either partially fixed or not fixed:
https://github.com/flutterchina/dio/issues/785
https://github.com/flutterchina/dio/issues/412
https://github.com/flutterchina/dio/issues/54
Dart has attempted to build a library that adheres to recent IETF specs.
@sortie wrote in 2019:
Dart implements a strict version of RFC 6265 (HTTP State Management Mechanism) which standardizes the cookie format.
Unfortunately, some web servers out in the wild aren't fully compliant (or compliant to an older spec), which leads to unwanted and unnecessary failures in real-world use. To their credit, the Dart devs have recognized the need to "bend the rules" on occasion to improve real-world compatibility. The most notable example is with case-sensitive header names:
Header fields of HttpHeaders are case-insensitive according to specification. Implementation class _HttpHeaders will convert all header fields into lowercases by default. This is expected behavior. However, some servers do rely on cased header fields.
From RFC-2616 (1999):
Each header field consists of a name followed by a colon (":") and the field value. Field names are case-insensitive.
And case-insensitivity is confirmed in RFC-7230:
Each header field consists of a case-insensitive field name
However, despite the clarity of the specs, the Dart developer community allowed for preservation of header casing.
I believe that invalid cookie values, which are also out of the client's control, deserve similar treatment. Perhaps a strictCookieRules flag (on by default) would, just like preserveHeaderValues, allow the developer more control when communicating with a non-compliant server. Thank you for your consideration.
To follow up on this and offer a temporary workaround:
By default, validating cookies in Dart is handled by http_headers.dart, the implementation is the _Cookie class. The class is defined by the abstract Cookie class in http.dart. Because _Cookie is private, it cannot be extended except within its own module -- and of course, if you do extend or re-implement, any code that relies on default Cookie needs to be updated to point to your custom extension or implementation.
Side note: Some packages facilitate replacing parts of the default stack with a custom implementation. Dio, for example, makes it easy to use your own HttpClient simply by swapping out the DefaultHttpClientAdapter with your own. (the hard part, of course, is creating a custom HttpClient. Huge credit to @shaxxx who did exactly that with the "alt_http" module, without his example I could not have figured out how all of this work and create a solution)
In the case of cookies, at least with Dio, it is not the HttpClient that validates the cookies, but the cookie manager. Seeing that this is a single small file, creating an alternative implementation will be far easier than creating (and maintaining) a new HttpClient. Specifically, pointing the method Cookie.fromSetCookieValue to a custom implementation of Cookie will allow us to change how cookies are validated.
CookieManager class can be extended, rather than entirely re-implemented:
class MyCookieManager extends CookieManager {
MyCookieManager(CookieJar cookieJar) : super(cookieJar);
@override
Future onResponse(Response response) async => _saveCookies(response);
@override
Future onError(DioError err) async => _saveCookies(err.response);
_saveCookies(Response response) {
if (response != null && response.headers != null) {
List<String> cookies = response.headers[HttpHeaders.setCookieHeader];
if (cookies != null) {
cookieJar.saveFromResponse(
response.request.uri,
// we're changing this ↙ line only... to use a custom class for cookie parsing
cookies.map((str) => MyCookie.fromSetCookieValue(str)).toList(),
);
}
}
}
}
And for MyCookie, as noted earlier it can't be extended, we must re-implement. Fortunately we can just copy the code of _Cookie from http_headers, with the changes you want to implement:
class MyCookie implements Cookie {
... copied code ...
// make your changes wherever appropriate in the class
if (!(codeUnit == 0x21 || codeUnit == 0x20 || // [allows spaces in cookie values]
... copied code ...
}
And because that copied code relies on a date parser from http_date.dart, we need to implement that too:
DateTime _parseCookieDate(String date) {
... copied code ... // suggest not making any changes
}
You can do this all in a single file, or use part and part of to combine 3 physical files into one logical file to Dart.
The last step is to change your Dio interceptor from the default to our custom version:
Dio()..interceptors.add(MyCookieManager(cookieJar))
Hopefully this will help anyone else finding themselves communicating with a non-compliant web server. I stated earlier that @shaxxx's example were hugely helpful, perhaps someone will find this post useful as well. This example is based on Dio, but it should be possible to adapt to any package that leverages the built-in Dart SDK HttpClient or associated classes (like Cookie).
Most helpful comment
To follow up on this and offer a temporary workaround:
By default, validating cookies in Dart is handled by http_headers.dart, the implementation is the
_Cookieclass. The class is defined by the abstractCookieclass in http.dart. Because_Cookieis private, it cannot be extended except within its own module -- and of course, if you do extend or re-implement, any code that relies on default Cookie needs to be updated to point to your custom extension or implementation.Side note: Some packages facilitate replacing parts of the default stack with a custom implementation. Dio, for example, makes it easy to use your own
HttpClientsimply by swapping out the DefaultHttpClientAdapter with your own. (the hard part, of course, is creating a custom HttpClient. Huge credit to @shaxxx who did exactly that with the "alt_http" module, without his example I could not have figured out how all of this work and create a solution)In the case of cookies, at least with Dio, it is not the HttpClient that validates the cookies, but the cookie manager. Seeing that this is a single small file, creating an alternative implementation will be far easier than creating (and maintaining) a new
HttpClient. Specifically, pointing the methodCookie.fromSetCookieValueto a custom implementation of Cookie will allow us to change how cookies are validated.CookieManagerclass can be extended, rather than entirely re-implemented:And for MyCookie, as noted earlier it can't be extended, we must re-implement. Fortunately we can just copy the code of _Cookie from http_headers, with the changes you want to implement:
And because that copied code relies on a date parser from http_date.dart, we need to implement that too:
You can do this all in a single file, or use
partandpart ofto combine 3 physical files into one logical file to Dart.The last step is to change your Dio interceptor from the default to our custom version:
Hopefully this will help anyone else finding themselves communicating with a non-compliant web server. I stated earlier that @shaxxx's example were hugely helpful, perhaps someone will find this post useful as well. This example is based on Dio, but it should be possible to adapt to any package that leverages the built-in Dart SDK HttpClient or associated classes (like Cookie).