blob: 7945526871d3e5526bad5d768d19645c8af79ffa [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.
import 'dart:convert';
import 'package:http_parser/http_parser.dart';
import 'message.dart';
import 'util.dart';
/// The response returned by a [Handler].
class Response extends Message {
/// The HTTP status code of the response.
final int statusCode;
/// The date and time after which the response's data should be considered
/// stale.
///
/// This is parsed from the Expires header in [headers]. If [headers] doesn't
/// have an Expires header, this will be `null`.
DateTime get expires {
if (_expiresCache != null) return _expiresCache;
if (!headers.containsKey('expires')) return null;
_expiresCache = parseHttpDate(headers['expires']);
return _expiresCache;
}
DateTime _expiresCache;
/// The date and time the source of the response's data was last modified.
///
/// This is parsed from the Last-Modified header in [headers]. If [headers]
/// doesn't have a Last-Modified header, this will be `null`.
DateTime get lastModified {
if (_lastModifiedCache != null) return _lastModifiedCache;
if (!headers.containsKey('last-modified')) return null;
_lastModifiedCache = parseHttpDate(headers['last-modified']);
return _lastModifiedCache;
}
DateTime _lastModifiedCache;
/// Constructs a 200 OK response.
///
/// This indicates that the request has succeeded.
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.ok(body, {Map<String, String> headers, Encoding encoding,
Map<String, Object> context})
: this(200, body: body, headers: headers, encoding: encoding,
context: context);
/// Constructs a 301 Moved Permanently response.
///
/// This indicates that the requested resource has moved permanently to a new
/// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's
/// automatically set as the Location header in [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.movedPermanently(location, {body, Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: this._redirect(301, location, body, headers, encoding,
context: context);
/// Constructs a 302 Found response.
///
/// This indicates that the requested resource has moved temporarily to a new
/// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's
/// automatically set as the Location header in [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.found(location, {body, Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: this._redirect(302, location, body, headers, encoding,
context: context);
/// Constructs a 303 See Other response.
///
/// This indicates that the response to the request should be retrieved using
/// a GET request to a new URI. [location] is that URI; it can be either a
/// [String] or a [Uri]. It's automatically set as the Location header in
/// [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.seeOther(location, {body, Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: this._redirect(303, location, body, headers, encoding,
context: context);
/// Constructs a helper constructor for redirect responses.
Response._redirect(int statusCode, location, body,
Map<String, String> headers, Encoding encoding,
{ Map<String, Object> context })
: this(statusCode,
body: body,
encoding: encoding,
headers: addHeader(
headers, 'location', _locationToString(location)),
context: context);
/// Constructs a 304 Not Modified response.
///
/// This is used to respond to a conditional GET request that provided
/// information used to determine whether the requested resource has changed
/// since the last request. It indicates that the resource has not changed and
/// the old value should be used.
Response.notModified({Map<String, String> headers,
Map<String, Object> context})
: this(304, headers: addHeader(
headers, 'date', formatHttpDate(new DateTime.now())),
context: context);
/// Constructs a 403 Forbidden response.
///
/// This indicates that the server is refusing to fulfill the request.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.forbidden(body, {Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: this(403,
headers: body == null ? _adjustErrorHeaders(headers) : headers,
body: body == null ? 'Forbidden' : body,
context: context);
/// Constructs a 404 Not Found response.
///
/// This indicates that the server didn't find any resource matching the
/// requested URI.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.notFound(body, {Map<String, String> headers, Encoding encoding,
Map<String, Object> context})
: this(404,
headers: body == null ? _adjustErrorHeaders(headers) : headers,
body: body == null ? 'Not Found' : body,
context: context);
/// Constructs a 500 Internal Server Error response.
///
/// This indicates that the server had an internal error that prevented it
/// from fulfilling the request.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.internalServerError({body, Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: this(500,
headers: body == null ? _adjustErrorHeaders(headers) : headers,
body: body == null ? 'Internal Server Error' : body,
context: context);
/// Constructs an HTTP response with the given [statusCode].
///
/// [statusCode] must be greater than or equal to 100.
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body.
/// If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response(this.statusCode, {body, Map<String, String> headers,
Encoding encoding, Map<String, Object> context})
: super(body, encoding: encoding, headers: headers, context: context) {
if (statusCode < 100) {
throw new ArgumentError("Invalid status code: $statusCode.");
}
}
/// Creates a new [Response] by copying existing values and applying specified
/// changes.
///
/// New key-value pairs in [context] and [headers] will be added to the copied
/// [Response].
///
/// If [context] or [headers] includes a key that already exists, the
/// key-value pair will replace the corresponding entry in the copied
/// [Response].
///
/// All other context and header values from the [Response] will be included
/// in the copied [Response] unchanged.
///
/// [body] is the request body. It may be either a [String] or a
/// [Stream<List<int>>].
Response change(
{Map<String, String> headers, Map<String, Object> context, body}) {
headers = updateMap(this.headers, headers);
context = updateMap(this.context, context);
if (body == null) body = getBody(this);
return new Response(this.statusCode, body: body, headers: headers,
context: context);
}
}
/// Adds content-type information to [headers].
///
/// Returns a new map without modifying [headers]. This is used to add
/// content-type information when creating a 500 response with a default body.
Map<String, String> _adjustErrorHeaders(Map<String, String> headers) {
if (headers == null || headers['content-type'] == null) {
return addHeader(headers, 'content-type', 'text/plain');
}
var contentType = new MediaType.parse(headers['content-type'])
.change(mimeType: 'text/plain');
return addHeader(headers, 'content-type', contentType.toString());
}
/// Converts [location], which may be a [String] or a [Uri], to a [String].
///
/// Throws an [ArgumentError] if [location] isn't a [String] or a [Uri].
String _locationToString(location) {
if (location is String) return location;
if (location is Uri) return location.toString();
throw new ArgumentError('Response location must be a String or Uri, was '
'"$location".');
}