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