blob: 0527461dbb9f0e22927012745b7b29cdb2ddc18d [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'base_client.dart';
import 'base_request.dart';
import 'client.dart';
import 'response.dart';
import 'streamed_response.dart';
/// The base class for HTTP responses.
///
/// Subclasses of [BaseResponse] are usually not constructed manually; instead,
/// they're returned by [BaseClient.send] or other HTTP client methods.
abstract class BaseResponse {
/// The (frozen) request that triggered this response.
final BaseRequest? request;
/// The HTTP status code for this response.
final int statusCode;
/// The reason phrase associated with the status code.
final String? reasonPhrase;
/// The size of the response body, in bytes.
///
/// If the size of the request is not known in advance, this is `null`.
final int? contentLength;
// TODO(nweiz): automatically parse cookies from headers
/// The HTTP headers returned by the server.
///
/// The header names are converted to lowercase and stored with their
/// associated header values.
///
/// If the server returns multiple headers with the same name then the header
/// values will be associated with a single key and seperated by commas and
/// possibly whitespace. For example:
/// ```dart
/// // HTTP/1.1 200 OK
/// // Fruit: Apple
/// // Fruit: Banana
/// // Fruit: Grape
/// final values = response.headers['fruit']!.split(RegExp(r'\s*,\s*'));
/// // values = ['Apple', 'Banana', 'Grape']
/// ```
///
/// To retrieve the header values as a `List<String>`, use
/// [HeadersWithSplitValues.headersSplitValues].
///
/// If a header value contains whitespace then that whitespace may be replaced
/// by a single space. Leading and trailing whitespace in header values are
/// always removed.
final Map<String, String> headers;
final bool isRedirect;
/// Whether the server requested that a persistent connection be maintained.
final bool persistentConnection;
BaseResponse(this.statusCode,
{this.contentLength,
this.request,
this.headers = const {},
this.isRedirect = false,
this.persistentConnection = true,
this.reasonPhrase}) {
if (statusCode < 100) {
throw ArgumentError('Invalid status code $statusCode.');
} else if (contentLength != null && contentLength! < 0) {
throw ArgumentError('Invalid content length $contentLength.');
}
}
}
/// A [BaseResponse] with a [url] field.
///
/// [Client] methods that return a [BaseResponse] subclass, such as [Response]
/// or [StreamedResponse], **may** return a [BaseResponseWithUrl].
///
/// For example:
///
/// ```dart
/// final client = Client();
/// final response = client.get(Uri.https('example.com', '/'));
/// Uri? finalUri;
/// if (response case BaseResponseWithUrl(:final url)) {
/// finalUri = url;
/// }
/// // Do something with `finalUri`.
/// client.close();
/// ```
///
/// [url] will be added to [BaseResponse] when `package:http` version 2 is
/// released and this mixin will be deprecated.
abstract interface class BaseResponseWithUrl implements BaseResponse {
/// The [Uri] of the response returned by the server.
///
/// If no redirects were followed, [url] will be the same as the requested
/// [Uri].
///
/// If redirects were followed, [url] will be the [Uri] of the last redirect
/// that was followed.
abstract final Uri url;
}
/// "token" as defined in RFC 2616, 2.2
/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`"
'abcdefghijklmnopqrstuvwxyz|~';
/// Splits comma-seperated header values.
var _headerSplitter = RegExp(r'[ \t]*,[ \t]*');
/// Splits comma-seperated "Set-Cookie" header values.
///
/// Set-Cookie strings can contain commas. In particular, the following
/// productions defined in RFC-6265, section 4.1.1:
/// - <sane-cookie-date> e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT"
/// - <path-value> e.g. "Path=somepath,"
/// - <extension-av> e.g. "AnyString,Really,"
///
/// Some values are ambiguous e.g.
/// "Set-Cookie: lang=en; Path=/foo/"
/// "Set-Cookie: SID=x23"
/// and:
/// "Set-Cookie: lang=en; Path=/foo/,SID=x23"
/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23"
///
/// The idea behind this regex is that ",<valid token>=" is more likely to
/// start a new <cookie-pair> then be part of <path-value> or <extension-av>.
///
/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)');
extension HeadersWithSplitValues on BaseResponse {
/// The HTTP headers returned by the server.
///
/// The header names are converted to lowercase and stored with their
/// associated header values.
///
/// Cookies can be parsed using the dart:io `Cookie` class:
///
/// ```dart
/// import "dart:io";
/// import "package:http/http.dart";
///
/// void main() async {
/// final response = await Client().get(Uri.https('example.com', '/'));
/// final cookies = [
/// for (var value i
/// in response.headersSplitValues['set-cookie'] ?? <String>[])
/// Cookie.fromSetCookieValue(value)
/// ];
Map<String, List<String>> get headersSplitValues {
var headersWithFieldLists = <String, List<String>>{};
headers.forEach((key, value) {
if (!value.contains(',')) {
headersWithFieldLists[key] = [value];
} else {
if (key == 'set-cookie') {
headersWithFieldLists[key] = value.split(_setCookieSplitter);
} else {
headersWithFieldLists[key] = value.split(_headerSplitter);
}
}
});
return headersWithFieldLists;
}
}