// 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. /// /// {@template shelf_response_body_and_encoding_param} /// [body] is the response body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. /// /// If the body is a [String], [encoding] is used to encode it to a /// [Stream>]. It defaults to UTF-8. If it's a [String], a /// [List], or `null`, the Content-Length header is set automatically /// unless a Transfer-Encoding header is set. Otherwise, it's a /// [Stream>] and no Transfer-Encoding header is set, the adapter /// will set the Transfer-Encoding header to "chunked" and apply the chunked /// encoding to the body. /// /// 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". /// [headers] must contain values that are either `String` or `List`. /// An empty list will cause the header to be omitted. /// {@endtemplate} Response.ok( Object? body, { Map */ Object>? headers, Encoding? encoding, Map? 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]. /// /// {@macro shelf_response_body_and_encoding_param} Response.movedPermanently( Object location, { Object? body, Map */ Object>? headers, Encoding? encoding, Map? 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]. /// /// {@macro shelf_response_body_and_encoding_param} Response.found( Object location, { Object? body, Map */ Object>? headers, Encoding? encoding, Map? 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]. /// /// {@macro shelf_response_body_and_encoding_param} Response.seeOther( Object location, { Object? body, Map */ Object>? headers, Encoding? encoding, Map? context, }) : this._redirect(303, location, body, headers, encoding, context: context); /// Constructs a helper constructor for redirect responses. Response._redirect( int statusCode, Object location, Object? body, Map */ Object>? headers, Encoding? encoding, { Map? 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. /// /// [headers] must contain values that are either `String` or `List`. /// An empty list will cause the header to be omitted. /// /// If [headers] contains a value for `content-length` it will be removed. Response.notModified({ Map */ Object>? headers, Map? context, }) : this( 304, headers: removeHeader( addHeader(headers, 'date', formatHttpDate(DateTime.now())), 'content-length'), context: context, ); /// Constructs a 400 Bad Request response. /// /// This indicates that the server has received a malformed request. /// /// {@macro shelf_response_body_and_encoding_param} Response.badRequest({ Object? body, Map */ Object>? headers, Encoding? encoding, Map? context, }) : this( 400, headers: body == null ? _adjustErrorHeaders(headers) : headers, body: body ?? 'Bad Request', context: context, encoding: encoding, ); /// Constructs a 401 Unauthorized response. /// /// This indicates indicates that the client request has not been completed /// because it lacks valid authentication credentials. /// /// {@macro shelf_response_body_and_encoding_param} Response.unauthorized( Object? body, { Map */ Object>? headers, Encoding? encoding, Map? context, }) : this( 401, headers: body == null ? _adjustErrorHeaders(headers) : headers, body: body ?? 'Unauthorized', context: context, encoding: encoding, ); /// Constructs a 403 Forbidden response. /// /// This indicates that the server is refusing to fulfill the request. /// /// {@macro shelf_response_body_and_encoding_param} Response.forbidden( Object? body, { Map */ Object>? headers, Encoding? encoding, Map? context, }) : this( 403, headers: body == null ? _adjustErrorHeaders(headers) : headers, body: body ?? 'Forbidden', context: context, encoding: encoding, ); /// Constructs a 404 Not Found response. /// /// This indicates that the server didn't find any resource matching the /// requested URI. /// /// {@macro shelf_response_body_and_encoding_param} Response.notFound( Object? body, { Map */ Object>? headers, Encoding? encoding, Map? context, }) : this( 404, headers: body == null ? _adjustErrorHeaders(headers) : headers, body: body ?? 'Not Found', context: context, encoding: encoding, ); /// Constructs a 500 Internal Server Error response. /// /// This indicates that the server had an internal error that prevented it /// from fulfilling the request. /// /// {@macro shelf_response_body_and_encoding_param} Response.internalServerError({ Object? body, Map */ Object>? headers, Encoding? encoding, Map? context, }) : this( 500, headers: body == null ? _adjustErrorHeaders(headers) : headers, body: body ?? 'Internal Server Error', context: context, encoding: encoding, ); /// Constructs an HTTP response with the given [statusCode]. /// /// [statusCode] must be greater than or equal to 100. /// /// {@macro shelf_response_body_and_encoding_param} Response( this.statusCode, { Object? body, Map */ Object>? headers, Encoding? encoding, Map? context, }) : super(body, encoding: encoding, headers: headers, context: context) { if (statusCode < 100) { throw 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]. If [context] or [headers] contains a `null` value the /// corresponding `key` will be removed if it exists, otherwise the `null` /// value will be ignored. /// For [headers] a value which is an empty list will also cause the /// corresponding key to be removed. /// /// All other context and header values from the [Response] will be included /// in the copied [Response] unchanged. /// /// [body] is the response body. It may be either a [String], a [List], a /// [Stream>], or `[]` (empty list) to indicate no body. @override Response change({ Map */ Object?>? headers, Map? context, Object? body, }) { final headersAll = updateHeaders(this.headersAll, headers); final newContext = updateMap(this.context, context); body ??= extractBody(this); return Response( statusCode, body: body, headers: headersAll, context: newContext, ); } } /// 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 _adjustErrorHeaders( Map */ Object>? headers) { if (headers == null || headers['content-type'] == null) { return addHeader(headers, 'content-type', 'text/plain'); } final contentTypeValue = expandHeaderValue(headers['content-type']!).join(','); var contentType = MediaType.parse(contentTypeValue).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(Object location) { if (location is String) return location; if (location is Uri) return location.toString(); throw ArgumentError( 'Response location must be a String or Uri, was "$location".', ); }