OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library shelf.request; | 5 library shelf.request; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection'; | |
9 | 8 |
10 // TODO(kevmoo): use UnmodifiableMapView from SDK once 1.4 ships | |
11 import 'package:collection/wrappers.dart' as pc; | |
12 import 'package:http_parser/http_parser.dart'; | 9 import 'package:http_parser/http_parser.dart'; |
13 import 'package:path/path.dart' as p; | |
14 | 10 |
15 import 'message.dart'; | 11 import 'message.dart'; |
16 | 12 |
17 /// Represents an HTTP request to be processed by a Shelf application. | 13 /// Represents an HTTP request to be processed by a Shelf application. |
18 class Request extends Message { | 14 class Request extends Message { |
19 /// The remainder of the [requestedUri] path designating the virtual | 15 /// The remainder of the [requestedUri] path and query designating the virtual |
20 /// "location" of the request's target within the handler. | 16 /// "location" of the request's target within the handler. |
21 /// | 17 /// |
22 /// [pathInfo] may be an empty string, if [requestedUri ]targets the handler | 18 /// [url] may be an empty, if [requestedUri]targets the handler |
23 /// root and does not have a trailing slash. | 19 /// root and does not have a trailing slash. |
24 /// | 20 /// |
25 /// [pathInfo] is never null. If it is not empty, it will start with `/`. | 21 /// [url] is never null. If it is not empty, it will start with `/`. |
26 /// | 22 /// |
27 /// [scriptName] and [pathInfo] combine to create a valid path that should | 23 /// [scriptName] and [url] combine to create a valid path that should |
28 /// correspond to the [requestedUri] path. | 24 /// correspond to the [requestedUri] path. |
29 final String pathInfo; | 25 final Uri url; |
30 | |
31 /// The portion of the request URL that follows the "?", if any. | |
32 final String queryString; | |
33 | 26 |
34 /// The HTTP request method, such as "GET" or "POST". | 27 /// The HTTP request method, such as "GET" or "POST". |
35 final String method; | 28 final String method; |
36 | 29 |
37 /// The initial portion of the [requestedUri] path that corresponds to the | 30 /// The initial portion of the [requestedUri] path that corresponds to the |
38 /// handler. | 31 /// handler. |
39 /// | 32 /// |
40 /// [scriptName] allows a handler to know its virtual "location". | 33 /// [scriptName] allows a handler to know its virtual "location". |
41 /// | 34 /// |
42 /// If the handler corresponds to the "root" of a server, it will be an | 35 /// If the handler corresponds to the "root" of a server, it will be an |
43 /// empty string, otherwise it will start with a `/` | 36 /// empty string, otherwise it will start with a `/` |
44 /// | 37 /// |
45 /// [scriptName] and [pathInfo] combine to create a valid path that should | 38 /// [scriptName] and [url] combine to create a valid path that should |
46 /// correspond to the [requestedUri] path. | 39 /// correspond to the [requestedUri] path. |
47 final String scriptName; | 40 final String scriptName; |
48 | 41 |
49 /// The HTTP protocol version used in the request, either "1.0" or "1.1". | 42 /// The HTTP protocol version used in the request, either "1.0" or "1.1". |
50 final String protocolVersion; | 43 final String protocolVersion; |
51 | 44 |
52 /// The original [Uri] for the request. | 45 /// The original [Uri] for the request. |
53 final Uri requestedUri; | 46 final Uri requestedUri; |
54 | 47 |
55 /// If this is non-`null` and the requested resource hasn't been modified | 48 /// If this is non-`null` and the requested resource hasn't been modified |
56 /// since this date and time, the server should return a 304 Not Modified | 49 /// since this date and time, the server should return a 304 Not Modified |
57 /// response. | 50 /// response. |
58 /// | 51 /// |
59 /// This is parsed from the If-Modified-Since header in [headers]. If | 52 /// This is parsed from the If-Modified-Since header in [headers]. If |
60 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. | 53 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. |
61 DateTime get ifModifiedSince { | 54 DateTime get ifModifiedSince { |
62 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; | 55 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
63 if (!headers.containsKey('if-modified-since')) return null; | 56 if (!headers.containsKey('if-modified-since')) return null; |
64 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); | 57 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
65 return _ifModifiedSinceCache; | 58 return _ifModifiedSinceCache; |
66 } | 59 } |
67 DateTime _ifModifiedSinceCache; | 60 DateTime _ifModifiedSinceCache; |
68 | 61 |
69 Request(this.pathInfo, String queryString, this.method, | 62 /// Creates a new [Request]. |
70 this.scriptName, this.protocolVersion, this.requestedUri, | 63 /// |
71 Map<String, String> headers, {Stream<List<int>> body}) | 64 /// If [url] and [scriptName] are omitted, they are inferred from |
72 : this.queryString = queryString == null ? '' : queryString, | 65 /// [requestedUri]. |
73 super(new pc.UnmodifiableMapView(new HashMap.from(headers)), | 66 /// |
74 body == null ? new Stream.fromIterable([]) : body) { | 67 /// Setting one of [url] or [scriptName] and not the other will throw an |
| 68 /// [ArgumentError]. |
| 69 /// |
| 70 /// The default value for [protocolVersion] is '1.1'. |
| 71 // TODO(kevmoo) finish documenting the rest of the arguments. |
| 72 Request(this.method, Uri requestedUri, {String protocolVersion, |
| 73 Map<String, String> headers, Uri url, String scriptName, |
| 74 Stream<List<int>> body}) |
| 75 : this.requestedUri = requestedUri, |
| 76 this.protocolVersion = protocolVersion == null ? |
| 77 '1.1' : protocolVersion, |
| 78 this.url = _computeUrl(requestedUri, url, scriptName), |
| 79 this.scriptName = _computeScriptName(requestedUri, url, scriptName), |
| 80 super(body == null ? new Stream.fromIterable([]) : body, |
| 81 headers: headers) { |
75 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); | 82 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
76 | 83 |
77 if (scriptName.isNotEmpty && !scriptName.startsWith('/')) { | 84 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed |
| 85 if (requestedUri.scheme.isEmpty) { |
| 86 throw new ArgumentError('requstedUri must be an absolute URI.'); |
| 87 } |
| 88 |
| 89 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { |
78 throw new ArgumentError('scriptName must be empty or start with "/".'); | 90 throw new ArgumentError('scriptName must be empty or start with "/".'); |
79 } | 91 } |
80 | 92 |
81 if (scriptName == '/') { | 93 if (this.scriptName == '/') { |
82 throw new ArgumentError( | 94 throw new ArgumentError( |
83 'scriptName can never be "/". It should be empty instead.'); | 95 'scriptName can never be "/". It should be empty instead.'); |
84 } | 96 } |
85 | 97 |
86 if (scriptName.endsWith('/')) { | 98 if (this.scriptName.endsWith('/')) { |
87 throw new ArgumentError('scriptName must not end with "/".'); | 99 throw new ArgumentError('scriptName must not end with "/".'); |
88 } | 100 } |
89 | 101 |
90 if (pathInfo.isNotEmpty && !pathInfo.startsWith('/')) { | 102 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { |
91 throw new ArgumentError('pathInfo must be empty or start with "/".'); | 103 throw new ArgumentError('url must be empty or start with "/".'); |
92 } | 104 } |
93 | 105 |
94 if (scriptName.isEmpty && pathInfo.isEmpty) { | 106 if (this.scriptName.isEmpty && this.url.path.isEmpty) { |
95 throw new ArgumentError('scriptName and pathInfo cannot both be empty.'); | 107 throw new ArgumentError('scriptName and url cannot both be empty.'); |
96 } | 108 } |
97 } | 109 } |
| 110 } |
98 | 111 |
99 /// Convenience property to access [pathInfo] data as a [List]. | 112 /// Computes `url` from the provided [Request] constructor arguments. |
100 List<String> get pathSegments { | 113 /// |
101 var segs = p.url.split(pathInfo); | 114 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], |
102 if (segs.length > 0) { | 115 /// otherwise return [url]. |
103 assert(segs.first == p.url.separator); | 116 /// |
104 segs.removeAt(0); | 117 /// If [url] is provided, but [scriptName] is omitted, throws an |
105 } | 118 /// [ArgumentError]. |
106 return segs; | 119 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { |
| 120 if (url == null && scriptName == null) { |
| 121 return new Uri(path: requestedUri.path, query: requestedUri.query, |
| 122 fragment: requestedUri.fragment); |
107 } | 123 } |
| 124 |
| 125 if (url != null && scriptName != null) { |
| 126 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed |
| 127 if (url.scheme.isNotEmpty) throw new ArgumentError('url must be relative.'); |
| 128 return url; |
| 129 } |
| 130 |
| 131 throw new ArgumentError( |
| 132 'url and scriptName must both be null or both be set.'); |
108 } | 133 } |
| 134 |
| 135 /// Computes `scriptName` from the provided [Request] constructor arguments. |
| 136 /// |
| 137 /// If [url] and [scriptName] are `null` it returns an empty string, otherwise |
| 138 /// [scriptName] is returned. |
| 139 /// |
| 140 /// If [script] is provided, but [url] is omitted, throws an |
| 141 /// [ArgumentError]. |
| 142 String _computeScriptName(Uri requstedUri, Uri url, String scriptName) { |
| 143 if (url == null && scriptName == null) { |
| 144 return ''; |
| 145 } |
| 146 |
| 147 if (url != null && scriptName != null) { |
| 148 return scriptName; |
| 149 } |
| 150 |
| 151 throw new ArgumentError( |
| 152 'url and scriptName must both be null or both be set.'); |
| 153 } |
OLD | NEW |