OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library shelf.response; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:convert'; |
| 9 |
| 10 import 'package:collection/wrappers.dart'; |
| 11 |
| 12 import 'media_type.dart'; |
| 13 import 'message.dart'; |
| 14 import 'util.dart'; |
| 15 |
| 16 /// The response returned by a [Handler]. |
| 17 class Response extends Message { |
| 18 /// The HTTP status code of the response. |
| 19 final int statusCode; |
| 20 |
| 21 /// The date and time after which the response's data should be considered |
| 22 /// stale. |
| 23 /// |
| 24 /// This is parsed from the Expires header in [headers]. If [headers] doesn't |
| 25 /// have an Expires header, this will be `null`. |
| 26 DateTime get expires { |
| 27 if (_expiresCache != null) return _expiresCache; |
| 28 if (!headers.containsKey('expires')) return null; |
| 29 _expiresCache = parseHttpDate(headers['expires']); |
| 30 return _expiresCache; |
| 31 } |
| 32 DateTime _expiresCache; |
| 33 |
| 34 /// The date and time the source of the response's data was last modified. |
| 35 /// |
| 36 /// This is parsed from the Last-Modified header in [headers]. If [headers] |
| 37 /// doesn't have a Last-Modified header, this will be `null`. |
| 38 DateTime get lastModified { |
| 39 if (_lastModifiedCache != null) return _lastModifiedCache; |
| 40 if (!headers.containsKey('last-modified')) return null; |
| 41 _lastModifiedCache = parseHttpDate(headers['last-modified']); |
| 42 return _lastModifiedCache; |
| 43 } |
| 44 DateTime _lastModifiedCache; |
| 45 |
| 46 /// Constructs a 200 OK response. |
| 47 /// |
| 48 /// This indicates that the request has succeeded. |
| 49 /// |
| 50 /// [body] is the response body. It may be either a [String], a |
| 51 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 52 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 53 /// UTF-8. |
| 54 /// |
| 55 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 56 /// in [headers] will be set appropriately. If there is no existing |
| 57 /// Content-Type header, it will be set to "application/octet-stream". |
| 58 Response.ok(body, {Map<String, String> headers, Encoding encoding}) |
| 59 : this(200, body: body, headers: headers, encoding: encoding); |
| 60 |
| 61 /// Constructs a 301 Moved Permanently response. |
| 62 /// |
| 63 /// This indicates that the requested resource has moved permanently to a new |
| 64 /// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's |
| 65 /// automatically set as the Location header in [headers]. |
| 66 /// |
| 67 /// [body] is the response body. It may be either a [String], a |
| 68 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 69 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 70 /// UTF-8. |
| 71 /// |
| 72 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 73 /// in [headers] will be set appropriately. If there is no existing |
| 74 /// Content-Type header, it will be set to "application/octet-stream". |
| 75 Response.movedPermanently(location, {body, Map<String, String> headers, |
| 76 Encoding encoding}) |
| 77 : this._redirect(301, location, body, headers, encoding); |
| 78 |
| 79 /// Constructs a 302 Found response. |
| 80 /// |
| 81 /// This indicates that the requested resource has moved temporarily to a new |
| 82 /// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's |
| 83 /// automatically set as the Location header in [headers]. |
| 84 /// |
| 85 /// [body] is the response body. It may be either a [String], a |
| 86 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 87 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 88 /// UTF-8. |
| 89 /// |
| 90 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 91 /// in [headers] will be set appropriately. If there is no existing |
| 92 /// Content-Type header, it will be set to "application/octet-stream". |
| 93 Response.found(location, {body, Map<String, String> headers, |
| 94 Encoding encoding}) |
| 95 : this._redirect(302, location, body, headers, encoding); |
| 96 |
| 97 /// Constructs a 303 See Other response. |
| 98 /// |
| 99 /// This indicates that the response to the request should be retrieved using |
| 100 /// a GET request to a new URI. [location] is that URI; it can be either a |
| 101 /// [String] or a [Uri]. It's automatically set as the Location header in |
| 102 /// [headers]. |
| 103 /// |
| 104 /// [body] is the response body. It may be either a [String], a |
| 105 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 106 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 107 /// UTF-8. |
| 108 /// |
| 109 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 110 /// in [headers] will be set appropriately. If there is no existing |
| 111 /// Content-Type header, it will be set to "application/octet-stream". |
| 112 Response.seeOther(location, {body, Map<String, String> headers, |
| 113 Encoding encoding}) |
| 114 : this._redirect(303, location, body, headers, encoding); |
| 115 |
| 116 /// Constructs a helper constructor for redirect responses. |
| 117 Response._redirect(int statusCode, location, body, |
| 118 Map<String, String> headers, Encoding encoding) |
| 119 : this(statusCode, |
| 120 body: body, |
| 121 encoding: encoding, |
| 122 headers: _addHeader( |
| 123 headers, 'location', _locationToString(location))); |
| 124 |
| 125 /// Constructs a 304 Not Modified response. |
| 126 /// |
| 127 /// This is used to respond to a conditional GET request that provided |
| 128 /// information used to determine whether the requested resource has changed |
| 129 /// since the last request. It indicates that the resource has not changed and |
| 130 /// the old value should be used. |
| 131 Response.notModified({Map<String, String> headers}) |
| 132 : this(304, headers: _addHeader( |
| 133 headers, 'date', formatHttpDate(new DateTime.now()))); |
| 134 |
| 135 /// Constructs a 403 Forbidden response. |
| 136 /// |
| 137 /// This indicates that the server is refusing to fulfill the request. |
| 138 /// |
| 139 /// [body] is the response body. It may be either a [String], a |
| 140 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 141 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 142 /// UTF-8. |
| 143 /// |
| 144 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 145 /// in [headers] will be set appropriately. If there is no existing |
| 146 /// Content-Type header, it will be set to "application/octet-stream". |
| 147 Response.forbidden(body, {Map<String, String> headers, |
| 148 Encoding encoding}) |
| 149 : this(403, body: body, headers: headers); |
| 150 |
| 151 /// Constructs a 404 Not Found response. |
| 152 /// |
| 153 /// This indicates that the server didn't find any resource matching the |
| 154 /// requested URI. |
| 155 /// |
| 156 /// [body] is the response body. It may be either a [String], a |
| 157 /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String], |
| 158 /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to |
| 159 /// UTF-8. |
| 160 /// |
| 161 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 162 /// in [headers] will be set appropriately. If there is no existing |
| 163 /// Content-Type header, it will be set to "application/octet-stream". |
| 164 Response.notFound(body, {Map<String, String> headers, Encoding encoding}) |
| 165 : this(404, body: body, headers: headers); |
| 166 |
| 167 /// Constructs a 500 Internal Server Error response. |
| 168 /// |
| 169 /// This indicates that the server had an internal error that prevented it |
| 170 /// from fulfilling the request. |
| 171 /// |
| 172 /// [body] is the response body. It may be either a [String], a |
| 173 /// [Stream<List<int>>], or `null` to indicate no body. If it's `null` or not |
| 174 /// passed, a default error message is used. If it's a [String], [encoding] is |
| 175 /// used to encode it to a [Stream<List<int>>]. It defaults to UTF-8. |
| 176 /// |
| 177 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 178 /// in [headers] will be set appropriately. If there is no existing |
| 179 /// Content-Type header, it will be set to "application/octet-stream". |
| 180 Response.internalServerError({body, Map<String, String> headers, |
| 181 Encoding encoding}) |
| 182 : this(500, |
| 183 headers: body == null ? _adjust500Headers(headers) : headers, |
| 184 body: body == null ? 'Internal Server Error' : body); |
| 185 |
| 186 /// Constructs an HTTP response with the given [statusCode]. |
| 187 /// |
| 188 /// [statusCode] must be greater than or equal to 100. |
| 189 /// |
| 190 /// [body] is the response body. It may be either a [String], a |
| 191 /// [Stream<List<int>>], or `null` to indicate no body. If it's `null` or not |
| 192 /// passed, a default error message is used. If it's a [String], [encoding] is |
| 193 /// used to encode it to a [Stream<List<int>>]. It defaults to UTF-8. |
| 194 /// |
| 195 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 196 /// in [headers] will be set appropriately. If there is no existing |
| 197 /// Content-Type header, it will be set to "application/octet-stream". |
| 198 Response(this.statusCode, {body, Map<String, String> headers, |
| 199 Encoding encoding}) |
| 200 : super(_adjustHeaders(headers, encoding), |
| 201 _bodyToStream(body, encoding)) { |
| 202 if (statusCode < 100) { |
| 203 throw new ArgumentError("Invalid status code: $statusCode."); |
| 204 } |
| 205 } |
| 206 } |
| 207 |
| 208 /// Converts [body] to a byte stream. |
| 209 /// |
| 210 /// [body] may be either a [String], a [Stream<List<int>>], or `null`. If it's a |
| 211 /// [String], [encoding] will be used to convert it to a [Stream<List<int>>]. |
| 212 Stream<List<int>> _bodyToStream(body, Encoding encoding) { |
| 213 if (encoding == null) encoding = UTF8; |
| 214 if (body == null) return new Stream.fromIterable([]); |
| 215 if (body is String) return new Stream.fromIterable([encoding.encode(body)]); |
| 216 if (body is Stream) return body; |
| 217 |
| 218 throw new ArgumentError('Response body "$body" must be a String or a ' |
| 219 'Stream.'); |
| 220 } |
| 221 |
| 222 /// Adds information about [encoding] to [headers]. |
| 223 /// |
| 224 /// Returns a new map without modifying [headers]. |
| 225 UnmodifiableMapView<String, String> _adjustHeaders( |
| 226 Map<String, String> headers, Encoding encoding) { |
| 227 if (headers == null) headers = const {}; |
| 228 if (encoding == null) return new UnmodifiableMapView(headers); |
| 229 if (headers['content-type'] == null) { |
| 230 return new UnmodifiableMapView(_addHeader(headers, 'content-type', |
| 231 'application/octet-stream; charset=${encoding.name}')); |
| 232 } |
| 233 |
| 234 var contentType = new MediaType.parse(headers['content-type']) |
| 235 .change(parameters: {'charset': encoding.name}); |
| 236 return new UnmodifiableMapView( |
| 237 _addHeader(headers, 'content-type', contentType.toString())); |
| 238 } |
| 239 |
| 240 /// Adds a header with [name] and [value] to [headers], which may be null. |
| 241 /// |
| 242 /// Returns a new map without modifying [headers]. |
| 243 Map<String, String> _addHeader(Map<String, String> headers, String name, |
| 244 String value) { |
| 245 headers = headers == null ? {} : new Map.from(headers); |
| 246 headers[name] = value; |
| 247 return headers; |
| 248 } |
| 249 |
| 250 /// Adds content-type information to [headers]. |
| 251 /// |
| 252 /// Returns a new map without modifying [headers]. This is used to add |
| 253 /// content-type information when creating a 500 response with a default body. |
| 254 Map<String, String> _adjust500Headers(Map<String, String> headers) { |
| 255 if (headers == null || headers['content-type'] == null) { |
| 256 return _addHeader(headers, 'content-type', 'text/plain'); |
| 257 } |
| 258 |
| 259 var contentType = new MediaType.parse(headers['content-type']) |
| 260 .change(mimeType: 'text/plain'); |
| 261 return _addHeader(headers, 'content-type', contentType.toString()); |
| 262 } |
| 263 |
| 264 /// Converts [location], which may be a [String] or a [Uri], to a [String]. |
| 265 /// |
| 266 /// Throws an [ArgumentError] if [location] isn't a [String] or a [Uri]. |
| 267 String _locationToString(location) { |
| 268 if (location is String) return location; |
| 269 if (location is Uri) return location.toString(); |
| 270 |
| 271 throw new ArgumentError('Response location must be a String or Uri, was ' |
| 272 '"$location".'); |
| 273 } |
OLD | NEW |