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

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

Powered by Google App Engine
This is Rietveld 408576698