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