Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(340)

Side by Side Diff: lib/src/request.dart

Issue 966063003: Overhaul the semantics of Request.handlerPath and Request.url. (Closed) Base URL: git@github.com:dart-lang/shelf@master
Patch Set: Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « README.md ('k') | pubspec.yaml » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 8
9 import 'package:http_parser/http_parser.dart'; 9 import 'package:http_parser/http_parser.dart';
10 10
11 import 'hijack_exception.dart'; 11 import 'hijack_exception.dart';
12 import 'message.dart'; 12 import 'message.dart';
13 import 'util.dart'; 13 import 'util.dart';
14 14
15 /// A callback provided by a Shelf handler that's passed to [Request.hijack]. 15 /// A callback provided by a Shelf handler that's passed to [Request.hijack].
16 typedef void HijackCallback( 16 typedef void HijackCallback(
17 Stream<List<int>> stream, StreamSink<List<int>> sink); 17 Stream<List<int>> stream, StreamSink<List<int>> sink);
18 18
19 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to 19 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to
20 /// provide a [HijackCallback] with a socket. 20 /// provide a [HijackCallback] with a socket.
21 typedef void OnHijackCallback(HijackCallback callback); 21 typedef void OnHijackCallback(HijackCallback callback);
22 22
23 /// Represents an HTTP request to be processed by a Shelf application. 23 /// Represents an HTTP request to be processed by a Shelf application.
24 class Request extends Message { 24 class Request extends Message {
25 /// The remainder of the [requestedUri] path and query designating the virtual 25 /// The URL path from the current handler to the requested resource, relative
26 /// "location" of the request's target within the handler. 26 /// to [handlerPath], plus any query parameters.
27 /// 27 ///
28 /// [url] may be an empty, if [requestedUri]targets the handler 28 /// This should be used by handlers for determining which resource to serve,
29 /// root and does not have a trailing slash. 29 /// in preference to [requestedUri]. This allows handlers to do the right
30 /// thing when they're mounted anywhere in the application. Routers should be
31 /// sure to update this when dispatching to a nested handler, using the
32 /// `path` parameter to [change].
30 /// 33 ///
31 /// [url] is never null. If it is not empty, it will start with `/`. 34 /// [url]'s path is always relative. It may be empty, if [requestedUri] ends
35 /// at this handler. [url] will always have the same query parameters as
36 /// [requestedUri].
32 /// 37 ///
33 /// [scriptName] and [url] combine to create a valid path that should 38 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path.
34 /// correspond to the [requestedUri] path.
35 final Uri url; 39 final Uri url;
36 40
37 /// The HTTP request method, such as "GET" or "POST". 41 /// The HTTP request method, such as "GET" or "POST".
38 final String method; 42 final String method;
39 43
40 /// The initial portion of the [requestedUri] path that corresponds to the 44 /// The URL path to the current handler.
41 /// handler.
42 /// 45 ///
43 /// [scriptName] allows a handler to know its virtual "location". 46 /// This allows a handler to know its location within the URL-space of an
47 /// application. Routers should be sure to update this when dispatching to a
48 /// nested handler, using the `path` parameter to [change].
44 /// 49 ///
45 /// If the handler corresponds to the "root" of a server, it will be an 50 /// [handlerPath] is always a root-relative URL path; that is, it always start s
kevmoo 2015/03/04 22:29:35 long line
nweiz 2015/03/04 23:46:53 Done.
46 /// empty string, otherwise it will start with a `/` 51 /// with `/`. It will also end with `/` whenever [url]'s path is non-empty, or
52 /// if [requestUri]'s path ends with `/`.
47 /// 53 ///
48 /// [scriptName] and [url] combine to create a valid path that should 54 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path.
49 /// correspond to the [requestedUri] path. 55 final String handlerPath;
50 final String scriptName;
51 56
52 /// The HTTP protocol version used in the request, either "1.0" or "1.1". 57 /// The HTTP protocol version used in the request, either "1.0" or "1.1".
53 final String protocolVersion; 58 final String protocolVersion;
54 59
55 /// The original [Uri] for the request. 60 /// The original [Uri] for the request.
56 final Uri requestedUri; 61 final Uri requestedUri;
57 62
58 /// The callback wrapper for hijacking this request. 63 /// The callback wrapper for hijacking this request.
59 /// 64 ///
60 /// This will be `null` if this request can't be hijacked. 65 /// This will be `null` if this request can't be hijacked.
(...skipping 14 matching lines...) Expand all
75 DateTime get ifModifiedSince { 80 DateTime get ifModifiedSince {
76 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; 81 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache;
77 if (!headers.containsKey('if-modified-since')) return null; 82 if (!headers.containsKey('if-modified-since')) return null;
78 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); 83 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']);
79 return _ifModifiedSinceCache; 84 return _ifModifiedSinceCache;
80 } 85 }
81 DateTime _ifModifiedSinceCache; 86 DateTime _ifModifiedSinceCache;
82 87
83 /// Creates a new [Request]. 88 /// Creates a new [Request].
84 /// 89 ///
85 /// If [url] and [scriptName] are omitted, they are inferred from 90 /// [handlerPath] must be root-relative. [url]'s path must be fully relative,
86 /// [requestedUri]. 91 /// and it must have the same query parameters as [requestedUri].
87 /// 92 /// [handlerPath] and [url]'s path must combine to be the path component of
88 /// Setting one of [url] or [scriptName] and not the other will throw an 93 /// [requestedUri]. If they're not passed, [handlerPath] will default to `/`
89 /// [ArgumentError]. 94 /// and [url] to `requestedUri.path` without the initial `/`. If only one is
95 /// passed, the other will be inferred.
90 /// 96 ///
91 /// The default value for [protocolVersion] is '1.1'. 97 /// The default value for [protocolVersion] is '1.1'.
92 /// 98 ///
93 /// ## `onHijack` 99 /// ## `onHijack`
94 /// 100 ///
95 /// [onHijack] allows handlers to take control of the underlying socket for 101 /// [onHijack] allows handlers to take control of the underlying socket for
96 /// the request. It should be passed by adapters that can provide access to 102 /// the request. It should be passed by adapters that can provide access to
97 /// the bidirectional socket underlying the HTTP connection stream. 103 /// the bidirectional socket underlying the HTTP connection stream.
98 /// 104 ///
99 /// The [onHijack] callback will only be called once per request. It will be 105 /// The [onHijack] callback will only be called once per request. It will be
(...skipping 10 matching lines...) Expand all
110 /// [HijackException] is caught. 116 /// [HijackException] is caught.
111 /// 117 ///
112 /// An adapter can check whether a request was hijacked using [canHijack], 118 /// An adapter can check whether a request was hijacked using [canHijack],
113 /// which will be `false` for a hijacked request. The adapter may throw an 119 /// which will be `false` for a hijacked request. The adapter may throw an
114 /// error if a [HijackException] is received for a non-hijacked request, or if 120 /// error if a [HijackException] is received for a non-hijacked request, or if
115 /// no [HijackException] is received for a hijacked request. 121 /// no [HijackException] is received for a hijacked request.
116 /// 122 ///
117 /// See also [hijack]. 123 /// See also [hijack].
118 // TODO(kevmoo) finish documenting the rest of the arguments. 124 // TODO(kevmoo) finish documenting the rest of the arguments.
119 Request(String method, Uri requestedUri, {String protocolVersion, 125 Request(String method, Uri requestedUri, {String protocolVersion,
120 Map<String, String> headers, Uri url, String scriptName, 126 Map<String, String> headers, String handlerPath, Uri url,
121 Stream<List<int>> body, Map<String, Object> context, 127 Stream<List<int>> body, Map<String, Object> context,
122 OnHijackCallback onHijack}) 128 OnHijackCallback onHijack})
123 : this._(method, requestedUri, protocolVersion: protocolVersion, 129 : this._(method, requestedUri, protocolVersion: protocolVersion,
124 headers: headers, url: url, scriptName: scriptName, 130 headers: headers, url: url, handlerPath: handlerPath,
125 body: body, context: context, 131 body: body, context: context,
126 onHijack: onHijack == null ? null : new _OnHijack(onHijack)); 132 onHijack: onHijack == null ? null : new _OnHijack(onHijack));
127 133
128 /// This constructor has the same signature as [new Request] except that 134 /// This constructor has the same signature as [new Request] except that
129 /// accepts [onHijack] as [_OnHijack]. 135 /// accepts [onHijack] as [_OnHijack].
130 /// 136 ///
131 /// Any [Request] created by calling [change] will pass [_onHijack] from the 137 /// Any [Request] created by calling [change] will pass [_onHijack] from the
132 /// source [Request] to ensure that [hijack] can only be called once, even 138 /// source [Request] to ensure that [hijack] can only be called once, even
133 /// from a changed [Request]. 139 /// from a changed [Request].
134 Request._(this.method, Uri requestedUri, {String protocolVersion, 140 Request._(this.method, Uri requestedUri, {String protocolVersion,
135 Map<String, String> headers, Uri url, String scriptName, 141 Map<String, String> headers, String handlerPath, Uri url,
136 Stream<List<int>> body, Map<String, Object> context, 142 Stream<List<int>> body, Map<String, Object> context,
137 _OnHijack onHijack}) 143 _OnHijack onHijack})
138 : this.requestedUri = requestedUri, 144 : this.requestedUri = requestedUri,
139 this.protocolVersion = protocolVersion == null ? 145 this.protocolVersion = protocolVersion == null ?
140 '1.1' : protocolVersion, 146 '1.1' : protocolVersion,
141 this.url = _computeUrl(requestedUri, url, scriptName), 147 this.url = _computeUrl(requestedUri, handlerPath, url),
142 this.scriptName = _computeScriptName(requestedUri, url, scriptName), 148 this.handlerPath = _computeHandlerPath(requestedUri, handlerPath, url),
143 this._onHijack = onHijack, 149 this._onHijack = onHijack,
144 super(body == null ? new Stream.fromIterable([]) : body, 150 super(body == null ? new Stream.fromIterable([]) : body,
145 headers: headers, context: context) { 151 headers: headers, context: context) {
146 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); 152 if (method.isEmpty) throw new ArgumentError('method cannot be empty.');
147 153
148 if (!requestedUri.isAbsolute) { 154 if (!requestedUri.isAbsolute) {
149 throw new ArgumentError('requstedUri must be an absolute URI.'); 155 throw new ArgumentError(
156 'requestedUri "$requestedUri" must be an absolute URL.');
150 } 157 }
151 158
152 // TODO(kevmoo) if defined, check that scriptName is a fully-encoded, valid 159 if (requestedUri.fragment.isNotEmpty) {
153 // path component 160 throw new ArgumentError(
154 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { 161 'requestedUri "$requestedUri" may not have a fragment.');
155 throw new ArgumentError('scriptName must be empty or start with "/".');
156 } 162 }
157 163
158 if (this.scriptName == '/') { 164 if (this.handlerPath + this.url.path != this.requestedUri.path) {
159 throw new ArgumentError( 165 throw new ArgumentError('handlerPath "$handlerPath" and url "$url" must '
160 'scriptName can never be "/". It should be empty instead.'); 166 'combine to equal requestedUri path "${requestedUri.path}".');
161 }
162
163 if (this.scriptName.endsWith('/')) {
164 throw new ArgumentError('scriptName must not end with "/".');
165 }
166
167 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) {
168 throw new ArgumentError('url must be empty or start with "/".');
169 }
170
171 if (this.scriptName.isEmpty && this.url.path.isEmpty) {
172 throw new ArgumentError('scriptName and url cannot both be empty.');
173 } 167 }
174 } 168 }
175 169
176 /// Creates a new [Request] by copying existing values and applying specified 170 /// Creates a new [Request] by copying existing values and applying specified
177 /// changes. 171 /// changes.
178 /// 172 ///
179 /// New key-value pairs in [context] and [headers] will be added to the copied 173 /// New key-value pairs in [context] and [headers] will be added to the copied
180 /// [Request]. 174 /// [Request]. If [context] or [headers] includes a key that already exists,
175 /// the key-value pair will replace the corresponding entry in the copied
176 /// [Request]. All other context and header values from the [Request] will be
177 /// included in the copied [Request] unchanged.
181 /// 178 ///
182 /// If [context] or [headers] includes a key that already exists, the 179 /// [path] is used to update both [handlerPath] and [url]. It's designed for
183 /// key-value pair will replace the corresponding entry in the copied 180 /// routing middleware, and represents the path from the current handler to
184 /// [Request]. 181 /// the next handler. It must be a prefix of [url]; [handlerPath] becomes
182 /// `handlerPath + "/" + path`, and [url] becomes relative to that. For
183 /// example:
185 /// 184 ///
186 /// All other context and header values from the [Request] will be included 185 /// print(request.handlerPath); // => /static/
187 /// in the copied [Request] unchanged. 186 /// print(request.url); // => dir/file.html
188 /// 187 ///
189 /// If [scriptName] is provided and [url] is not, [scriptName] must be a 188 /// request = request.change(path: "dir");
190 /// prefix of [this.url]. [url] will default to [this.url] with this prefix 189 /// print(request.handlerPath); // => /static/dir/
191 /// removed. Useful for routing middleware that sends requests to an inner 190 /// print(request.url); // => file.html
192 /// [Handler].
193 Request change({Map<String, String> headers, Map<String, Object> context, 191 Request change({Map<String, String> headers, Map<String, Object> context,
194 String scriptName, Uri url}) { 192 String path}) {
195 headers = updateMap(this.headers, headers); 193 headers = updateMap(this.headers, headers);
196 context = updateMap(this.context, context); 194 context = updateMap(this.context, context);
197 195
198 if (scriptName != null && url == null) { 196 var handlerPath = this.handlerPath;
199 var path = this.url.path; 197 if (path != null) handlerPath += path;
200 if (path.startsWith(scriptName)) {
201 path = path.substring(scriptName.length);
202 url = new Uri(path: path, query: this.url.query);
203 } else {
204 throw new ArgumentError('If scriptName is provided without url, it must'
205 ' be a prefix of the existing url path.');
206 }
207 }
208
209 if (url == null) url = this.url;
210 if (scriptName == null) scriptName = this.scriptName;
211 198
212 return new Request._(this.method, this.requestedUri, 199 return new Request._(this.method, this.requestedUri,
213 protocolVersion: this.protocolVersion, headers: headers, url: url, 200 protocolVersion: this.protocolVersion, headers: headers,
214 scriptName: scriptName, body: this.read(), context: context, 201 handlerPath: handlerPath, body: this.read(), context: context,
215 onHijack: _onHijack); 202 onHijack: _onHijack);
216 } 203 }
217 204
218 /// Takes control of the underlying request socket. 205 /// Takes control of the underlying request socket.
219 /// 206 ///
220 /// Synchronously, this throws a [HijackException] that indicates to the 207 /// Synchronously, this throws a [HijackException] that indicates to the
221 /// adapter that it shouldn't emit a response itself. Asynchronously, 208 /// adapter that it shouldn't emit a response itself. Asynchronously,
222 /// [callback] is called with a [Stream<List<int>>] and 209 /// [callback] is called with a [Stream<List<int>>] and
223 /// [StreamSink<List<int>>], respectively, that provide access to the 210 /// [StreamSink<List<int>>], respectively, that provide access to the
224 /// underlying request socket. 211 /// underlying request socket.
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
257 /// Throws a [StateError] if [this] has already been called. 244 /// Throws a [StateError] if [this] has already been called.
258 void run(HijackCallback callback) { 245 void run(HijackCallback callback) {
259 if (called) throw new StateError("This request has already been hijacked."); 246 if (called) throw new StateError("This request has already been hijacked.");
260 called = true; 247 called = true;
261 newFuture(() => _callback(callback)); 248 newFuture(() => _callback(callback));
262 } 249 }
263 } 250 }
264 251
265 /// Computes `url` from the provided [Request] constructor arguments. 252 /// Computes `url` from the provided [Request] constructor arguments.
266 /// 253 ///
267 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], 254 /// If [url] is `null`, the value is inferred from [requestedUrl] and
268 /// otherwise return [url]. 255 /// [handlerPath] if available. Otherwise [url] is returned.
269 /// 256 Uri _computeUrl(Uri requestedUri, String handlerPath, Uri url) {
270 /// If [url] is provided, but [scriptName] is omitted, throws an 257 if (handlerPath != null && handlerPath != requestedUri.path &&
271 /// [ArgumentError]. 258 !handlerPath.endsWith("/")) {
272 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { 259 handlerPath += "/";
273 if (url == null && scriptName == null) {
274 return new Uri(path: requestedUri.path, query: requestedUri.query);
275 } 260 }
276 261
277 if (url != null && scriptName != null) { 262 if (url != null) {
278 if (url.scheme.isNotEmpty) throw new ArgumentError('url must be relative.'); 263 if (url.scheme.isNotEmpty || url.hasAuthority || url.fragment.isNotEmpty) {
264 throw new ArgumentError('url "$url" may contain only a path and query '
265 'parameters.');
266 }
267
268 if (!requestedUri.path.endsWith(url.path)) {
269 throw new ArgumentError('url "$url" must be a suffix of requestedUri '
270 '"$requestedUri".');
271 }
272
273 if (requestedUri.query != url.query) {
274 throw new ArgumentError('url "$url" must have the same query parameters '
275 'as requestedUri "$requestedUri".');
276 }
277
278 if (url.path.startsWith('/')) {
279 throw new ArgumentError('url "$url" must be relative.');
280 }
281
282 var startOfUrl = requestedUri.path.length - url.path.length;
283 if (requestedUri.path.substring(startOfUrl - 1, startOfUrl) != '/') {
284 throw new ArgumentError('url "$url" must be on a path boundary in '
285 'requestedUri "$requestedUri".');
286 }
287
279 return url; 288 return url;
289 } else if (handlerPath != null) {
290 return new Uri(
291 path: requestedUri.path.substring(handlerPath.length),
292 query: requestedUri.query);
293 } else {
294 // Skip the initial "/".
295 var path = requestedUri.path.substring(1);
296 return new Uri(path: path, query: requestedUri.query);
297 }
298 }
299
300 /// Computes `handlerPath` from the provided [Request] constructor arguments.
301 ///
302 /// If [handlerPath] is `null`, the value is inferred from [requestedUrl] and
303 /// [url] if available. Otherwise [handlerPath] is returned.
304 String _computeHandlerPath(Uri requestedUri, String handlerPath, Uri url) {
305 if (handlerPath != null && handlerPath != requestedUri.path &&
306 !handlerPath.endsWith("/")) {
307 handlerPath += "/";
280 } 308 }
281 309
282 throw new ArgumentError( 310 if (handlerPath != null) {
283 'url and scriptName must both be null or both be set.'); 311 if (!requestedUri.path.startsWith(handlerPath)) {
312 throw new ArgumentError('handlerPath "$handlerPath" must be a prefix of '
313 'requestedUri path "${requestedUri.path}"');
314 }
315
316 if (!handlerPath.startsWith('/')) {
317 throw new ArgumentError(
318 'handlerPath "$handlerPath" must be root-relative.');
319 }
320
321 return handlerPath;
322 } else if (url != null) {
323 var index = requestedUri.path.indexOf(url.path);
324 return requestedUri.path.substring(0, index);
325 } else {
326 return '/';
327 }
284 } 328 }
285
286 /// Computes `scriptName` from the provided [Request] constructor arguments.
287 ///
288 /// If [url] and [scriptName] are `null` it returns an empty string, otherwise
289 /// [scriptName] is returned.
290 ///
291 /// If [script] is provided, but [url] is omitted, throws an
292 /// [ArgumentError].
293 String _computeScriptName(Uri requstedUri, Uri url, String scriptName) {
294 if (url == null && scriptName == null) {
295 return '';
296 }
297
298 if (url != null && scriptName != null) {
299 return scriptName;
300 }
301
302 throw new ArgumentError(
303 'url and scriptName must both be null or both be set.');
304 }
OLDNEW
« no previous file with comments | « README.md ('k') | pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698