blob: 1cf469660d2004efdc1f8c1d754554e7649d07ee [file] [log] [blame]
// Copyright (c) 2014, 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.
library http_base;
import 'dart:async';
/// These headers should be ignored by [Client]s when making requests and when
/// receiving headers from a HTTP server.
const List<String> _TRANSPORT_HEADERS =
const ['connection', 'upgrade', 'keep-alive', 'transfer-encoding'];
/// These headers cannot be folded into one header value via ',' joining.
const List<String> _COOKIE_HEADERS = const ['set-cookie', 'cookie'];
/// Representation of a set of HTTP headers.
abstract class Headers {
/// Returns the names of all header fields.
Iterable<String> get names;
/// Returns `true` if a header field of the specified [name] exist.
bool contains(String name);
/// Returns the value for the header field named [name].
///
/// The HTTP standard supports multiple values for each header field name.
/// Header fields with multiple values can be represented as a
/// comma-separated list. If a header has multiple values the returned string
/// is the comma-separated list of all these values.
///
/// For header field-names which do not allow combining multiple values with
/// comma, this index operator will throw `ArgumentError`.
/// This is currently the case for the 'Cookie' and 'Set-Cookie' headers. Use
/// `getMultiple` method to iterate over the header values for these.
String operator [](String name);
/// Returns the values for the header field named [name].
///
/// The order in which the values for the field name appear is the same
/// as the order in which they are to be send or was received.
///
/// If there are no header values named [name] `null` will be returned.
Iterable<String> getMultiple(String name);
}
/// Representation of a HTTP request.
abstract class Request {
/// Request method.
String get method;
/// Request url.
Uri get url;
/// Request headers.
Headers get headers;
/// Request body.
Stream<List<int>> read();
}
/// Representation of a HTTP response.
abstract class Response {
/// Response status code.
int get statusCode;
/// Response headers.
Headers get headers;
/// Response body.
Stream<List<int>> read();
}
/// Function for performing an HTTP request.
///
/// The [RequestHandler] may use any transport mechanism it wants
/// (e.g. HTTP/1.1, HTTP/2.0, SPDY) to perform the HTTP request.
///
/// [RequestHandler]s are composable. E.g. A [RequestHandler] may add an
/// 'Authorization' header to [request] and forward to another [RequestHandler].
///
/// A [RequestHandler] may ignore connection specific headers in [request] and
/// may not present them in the [Response] object.
///
/// Connection specific headers:
/// 'Connection', 'Upgrade', 'Keep-Alive', 'Transfer-Encoding'
typedef Future<Response> RequestHandler(Request request);
/// An implementation of [Headers].
class HeadersImpl implements Headers {
static const HeadersImpl Empty = const HeadersImpl.empty();
final Map<String, List<String>> _m;
/// Constructs a [HeadersImpl] with no headers.
const HeadersImpl.empty() : _m = const {};
/// Constructs a new [HeaderImpl] initialized with [map].
///
/// [map] must contain only String keys and either String or
/// Iterable<String> values.
HeadersImpl(Map map) : _m = {} {
_addDiff(map);
}
/// Makes a copy of this [HeadersImpl] and replaces all headers in present in
/// [differenceMap].
///
/// [differenceMap] must contain only String keys and either String or
/// Iterable<String> values.
HeadersImpl replace(Map differenceMap) {
var headers = new HeadersImpl({});
_m.forEach((String key, List<String> value) {
headers._m[key] = value;
});
headers._addDiff(differenceMap);
return headers;
}
void _addDiff(Map diff) {
diff.forEach((String key, value) {
key = key.toLowerCase();
if (value == null) {
_m.remove(key);
} else if (value is String) {
var values = new List(1);
values[0] = value;
_m[key] = values;
} else {
_m[key] = value.toList();
}
});
}
Iterable<String> get names => _m.keys;
bool contains(String name) => _m.containsKey(name.toLowerCase());
String operator [](String name) {
name = name.toLowerCase();
if (_COOKIE_HEADERS.contains(name)) {
throw new ArgumentError('Cannot use Headers[] with $name header.');
}
var values = _m[name];
if (values == null) return null;
if (values.length == 1) return values.first;
return values.join(',');
}
Iterable<String> getMultiple(String name) {
name = name.toLowerCase();
var values = _m[name];
if (values == null) return values;
if (_COOKIE_HEADERS.contains(name)) {
return values;
} else {
return values.expand((e) => e.split(',')).map((e) => e.trim());
}
}
}
/// Internal helper class to reduce code duplication between [RequestImpl]
/// and [ResponseImpl].
class _Message {
final Headers headers;
final Stream<List<int>> _body;
bool _bodyRead = false;
_Message(Headers headers_, body)
: headers = headers_ != null ? headers_ : HeadersImpl.Empty,
_body = body != null ? body : (new StreamController()..close()).stream;
/// Returns the [Stream] of bytes of this message.
///
/// The body of a message can only be read once.
Stream<List<int>> read() {
if (_bodyRead) {
throw new StateError('The response stream has already been listened to.');
}
_bodyRead = true;
return _body;
}
}
/// An immutable implementation of [Request].
///
/// The request can be modified with the copy-on-write `replace` method.
class RequestImpl extends _Message implements Request {
final String method;
final Uri url;
RequestImpl(this.method, this.url, {Headers headers, Stream<List<int>> body})
: super(headers, body);
/// Makes a copy of this [RequestImpl] by overriding `method`, `url`,
/// `headers` and `body` if they are not null.
///
/// In case no [body] was supplied, the current `body` will be used and is
/// therefore no longer available to users. This is a transfer of the owner
/// of the body stream to the returned object.
RequestImpl replace(
{String method, Uri url, Headers headers, Stream<List<int>> body}) {
if (method == null) method = this.method;
if (url == null) url = this.url;
if (headers == null) headers = this.headers;
if (body == null) body = read();
return new RequestImpl(method, url, headers: headers, body: body);
}
}
/// An immutable implementation of [Response].
///
/// The response can be modified with the copy-on-write `replace` method.
class ResponseImpl extends _Message implements Response {
final int statusCode;
ResponseImpl(this.statusCode, {Headers headers, Stream<List<int>> body})
: super(headers, body);
/// Returns a copy of this [ResponseImpl] by overriding `statusCode`,
/// `headers` and `body` if they are not null.
///
/// In case no [body] was supplied, the current `body` will be used and is
/// therefore no longer available to users. This is a transfer of the owner
/// of the body stream to the returned object.
ResponseImpl replace(
{int statusCode, Headers headers, Stream<List<int>> body}) {
if (statusCode == null) statusCode = this.statusCode;
if (headers == null) headers = this.headers;
if (body == null) body = read();
return new ResponseImpl(statusCode, headers: headers, body: body);
}
}