Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, 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 | 5 |
| 6 class _HttpIncomingConnection extends StreamController<List<int>> { | 6 class _HttpIncomingConnection extends StreamController<List<int>> { |
| 7 final Function _pause; | 7 final Function _pause; |
| 8 final Function _resume; | 8 final Function _resume; |
| 9 bool _ignore = false; | 9 bool _ignore = false; |
| 10 final SignalCompleter _dataCompleter = new SignalCompleter(); | 10 final SignalCompleter _dataCompleter = new SignalCompleter(); |
| 11 final SignalCompleter _messageCompleter = new SignalCompleter(); | 11 final SignalCompleter _messageCompleter = new SignalCompleter(); |
| 12 | 12 |
| 13 // Common properties. | 13 // Common properties. |
| 14 _HttpHeaders headers; | 14 final _HttpHeaders headers; |
| 15 int contentLength; | |
| 16 bool upgraded = false; | 15 bool upgraded = false; |
| 17 | 16 |
| 18 // ClientResponse properties. | 17 // ClientResponse properties. |
| 19 int statusCode; | 18 int statusCode; |
| 20 String reasonPhrase; | 19 String reasonPhrase; |
| 21 | 20 |
| 22 // Request properties. | 21 // Request properties. |
| 23 String method; | 22 String method; |
| 24 Uri uri; | 23 Uri uri; |
| 25 | 24 |
| 26 _HttpIncomingConnection(void this._pause(), | 25 _HttpIncomingConnection(_HttpHeaders this.headers, |
| 26 void this._pause(), | |
| 27 void this._resume()) | 27 void this._resume()) |
| 28 : super.singleSubscription() { | 28 : super.singleSubscription() { |
| 29 _pause(); | 29 _pause(); |
| 30 } | 30 } |
| 31 | 31 |
| 32 // Is completed once all data have been received. | 32 // Is completed once all data have been received. |
| 33 Signal get dataDone => _dataCompleter.signal; | 33 Signal get dataDone => _dataCompleter.signal; |
| 34 | 34 |
| 35 // Is completed once the message have been handled. | 35 // Is completed once the message have been handled. |
| 36 Signal get messageHandled => _messageCompleter.signal; | 36 Signal get messageHandled => _messageCompleter.signal; |
| 37 | 37 |
| 38 void onPauseStateChange() { | 38 void onPauseStateChange() { |
| 39 if (isPaused) { | 39 if (isPaused) { |
| 40 _pause(); | 40 _pause(); |
| 41 } else { | 41 } else { |
| 42 _reasume(); | 42 _resume(); |
| 43 } | 43 } |
| 44 } | 44 } |
| 45 | 45 |
| 46 void onSubscriptionStateChange() { | 46 void onSubscriptionStateChange() { |
| 47 if (hasSubscribers) { | 47 if (hasSubscribers) { |
| 48 _resume(); | 48 _resume(); |
| 49 } else { | 49 } else { |
| 50 _ignore = true; | 50 _ignore = true; |
| 51 } | 51 } |
| 52 } | 52 } |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 109 return _queryParameters; | 109 return _queryParameters; |
| 110 } | 110 } |
| 111 | 111 |
| 112 Uri get uri => _incomingConnection.uri; | 112 Uri get uri => _incomingConnection.uri; |
| 113 | 113 |
| 114 String get method => _incomingConnection.method; | 114 String get method => _incomingConnection.method; |
| 115 } | 115 } |
| 116 | 116 |
| 117 | 117 |
| 118 class _HttpClientResponse extends _HttpIncoming implements HttpClientResponse { | 118 class _HttpClientResponse extends _HttpIncoming implements HttpClientResponse { |
| 119 _HttpClientResponse(_HttpIncomingConnection _incomingConnection) | 119 final List<RedirectInfo> redirects; |
| 120 | |
| 121 // The HttpClient this response belongs to. | |
| 122 final _HttpClient _httpClient; | |
| 123 | |
| 124 // The HttpClientRequest of this response. | |
| 125 final _HttpClientRequest _httpRequest; | |
| 126 | |
| 127 _HttpClientResponse(_HttpIncomingConnection _incomingConnection, | |
| 128 _HttpClientRequest this._httpRequest, | |
| 129 _HttpClient this._httpClient, | |
| 130 List<RedirectInfo> this.redirects) | |
|
Søren Gjesse
2013/01/09 08:43:49
Do you need to pass this? Isn't it just _httpReque
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 120 : super(_incomingConnection); | 131 : super(_incomingConnection); |
| 121 | 132 |
| 122 int get statusCode => _incomingConnection.statusCode; | 133 int get statusCode => _incomingConnection.statusCode; |
| 123 String get reasonPhrase => _incomingConnection.reasonPhrase; | 134 String get reasonPhrase => _incomingConnection.reasonPhrase; |
| 124 | 135 |
| 136 bool get isRedirect { | |
| 137 if (_httpRequest.method == "GET" || _httpRequest.method == "HEAD") { | |
| 138 return statusCode == HttpStatus.MOVED_PERMANENTLY || | |
| 139 statusCode == HttpStatus.FOUND || | |
| 140 statusCode == HttpStatus.SEE_OTHER || | |
| 141 statusCode == HttpStatus.TEMPORARY_REDIRECT; | |
| 142 } else if (_httpRequest.method == "POST") { | |
| 143 return statusCode == HttpStatus.SEE_OTHER; | |
| 144 } | |
| 145 return false; | |
| 146 } | |
| 147 | |
| 148 /** | |
| 149 * Redirect this connection to a new URL. The default value for | |
| 150 * [method] is the method for the current request. The default value | |
| 151 * for [url] is the value of the [:HttpHeaders.LOCATION:] header of | |
| 152 * the current response. All body data must have been read from the | |
| 153 * current response before calling [redirect]. | |
| 154 * | |
| 155 * All headers added to the request will be added to the redirection | |
| 156 * request(s). However, any body send with the request will not be | |
| 157 * part of the redirection request(s). | |
|
Søren Gjesse
2013/01/09 08:43:49
No need for this comment in the implementation fil
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 158 */ | |
| 159 Future<HttpClientResponse> redirect([String method, | |
| 160 Uri url, | |
| 161 bool followLoops]) { | |
| 162 if (method == null) { | |
| 163 if (statusCode == HttpStatus.SEE_OTHER && _httpRequest.method == "POST") { | |
|
Søren Gjesse
2013/01/09 08:43:49
Add comment with reference to RFC 2616 section 10.
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 164 method = "GET"; | |
| 165 } else { | |
| 166 method = _httpRequest.method; | |
| 167 } | |
| 168 } | |
| 169 if (url == null) { | |
| 170 if (!isRedirect) throw new StateError("Response is not redirecting"); | |
|
Søren Gjesse
2013/01/09 08:43:49
"Response is not redirecting" => Response has no L
Søren Gjesse
2013/01/09 08:43:49
We should probably only check for the Location hea
Anders Johnsen
2013/01/09 10:03:00
Done.
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 171 url = new Uri.fromString(headers.value(HttpHeaders.LOCATION)); | |
| 172 } | |
| 173 if (followLoops != true) { | |
| 174 for (var redirect in redirects) { | |
| 175 if (redirect.location == url) { | |
| 176 return new Future.immediateError( | |
| 177 new RedirectLoopException(redirects)); | |
| 178 } | |
| 179 } | |
| 180 } | |
| 181 return _httpClient.openUrl(method, url) | |
| 182 .then((request) { | |
| 183 // Only follow redirects if initial request did. | |
| 184 request.followRedirects = _httpRequest.followRedirects; | |
| 185 // Allow one less redirect. | |
|
Søren Gjesse
2013/01/09 08:43:49
The "one less" in the comment is not reflected in
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 186 request.maxRedirects = _httpRequest.maxRedirects; | |
| 187 // Copy headers | |
|
Søren Gjesse
2013/01/09 08:43:49
Terminate comment with .
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 188 for (var header in _httpRequest.headers._headers.keys) { | |
| 189 if (header != HttpHeaders.HOST.toLowerCase()) { | |
| 190 request.headers.set(header, _httpRequest.headers[header]); | |
| 191 } | |
| 192 } | |
| 193 request.headers.contentLength = 0; | |
| 194 var redirects = []; | |
|
Søren Gjesse
2013/01/09 08:43:49
Just operate directly on request._responseRedirect
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 195 redirects.addAll(this.redirects); | |
| 196 redirects.add(new _RedirectInfo(statusCode, method, url)); | |
| 197 request._setResponseRedirects(redirects); | |
| 198 return request.close(); | |
| 199 }); | |
| 200 } | |
| 201 | |
| 125 StreamSubscription<List<int>> listen(void onData(List<int> event), | 202 StreamSubscription<List<int>> listen(void onData(List<int> event), |
| 126 {void onError(AsyncError error), | 203 {void onError(AsyncError error), |
| 127 void onDone(), | 204 void onDone(), |
| 128 bool unsubscribeOnError}) { | 205 bool unsubscribeOnError}) { |
| 129 return _incomingConnection.listen( | 206 return _incomingConnection.listen( |
| 130 onData, | 207 onData, |
| 131 onError: onError, | 208 onError: onError, |
| 132 onDone: onDone, | 209 onDone: onDone, |
| 133 unsubscribeOnError: unsubscribeOnError); | 210 unsubscribeOnError: unsubscribeOnError); |
| 134 } | 211 } |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 223 | 300 |
| 224 // Write headers. | 301 // Write headers. |
| 225 headers._write(this); | 302 headers._write(this); |
| 226 writeCRLF(); | 303 writeCRLF(); |
| 227 } | 304 } |
| 228 } | 305 } |
| 229 | 306 |
| 230 | 307 |
| 231 class _HttpClientRequest extends _HttpOutgoing<HttpClientRequest> | 308 class _HttpClientRequest extends _HttpOutgoing<HttpClientRequest> |
| 232 implements HttpClientRequest { | 309 implements HttpClientRequest { |
| 233 _HttpClientRequest(Uri this.uri, | 310 final String method; |
| 311 final Uri uri; | |
| 312 final List<Cookie> cookies = new List<Cookie>(); | |
| 313 | |
| 314 // The HttpClient this request belongs to. | |
| 315 final _HttpClient _httpClient; | |
| 316 | |
| 317 final Completer<HttpClientResponse> _responseCompleter | |
| 318 = new Completer<HttpClientResponse>(); | |
| 319 | |
| 320 | |
| 321 // TODO(ajohnsen): Get default value from client? | |
| 322 bool _followRedirects = true; | |
| 323 | |
| 324 int _maxRedirects = 5; | |
| 325 | |
| 326 List<RedirectInfo> _responseRedirects = []; | |
| 327 | |
| 328 _HttpClientRequest(_HttpOutgoingConnection outgoing, | |
| 329 Uri this.uri, | |
| 234 String this.method, | 330 String this.method, |
| 235 _HttpOutgoingConnection outgoing) | 331 _HttpClient this._httpClient) |
| 236 : super("1.1", outgoing) { | 332 : super("1.1", outgoing) { |
| 237 // GET and HEAD have 'content-length: 0' by default. | 333 // GET and HEAD have 'content-length: 0' by default. |
| 238 if (method == "GET" || method == "HEAD") { | 334 if (method == "GET" || method == "HEAD") { |
| 239 contentLength = 0; | 335 contentLength = 0; |
| 240 } | 336 } |
| 241 } | 337 } |
| 242 | 338 |
| 339 Future<HttpClientResponse> get response => _responseCompleter.future; | |
| 340 | |
| 243 Future<HttpClientResponse> close() { | 341 Future<HttpClientResponse> close() { |
| 244 super.close(); | 342 super.close(); |
| 245 return response; | 343 return response; |
| 246 } | 344 } |
| 247 | 345 |
| 346 int get maxRedirects => _maxRedirects; | |
| 347 void set maxRedirects(int maxRedirects) { | |
| 348 if (_headersWritten) throw new StateError("Request already sent"); | |
| 349 _maxRedirects = maxRedirects; | |
| 350 } | |
| 351 | |
| 352 bool get followRedirects => _followRedirects; | |
| 353 void set followRedirects(bool followRedirects) { | |
| 354 if (_headersWritten) throw new StateError("Request already sent"); | |
| 355 _followRedirects = followRedirects; | |
| 356 } | |
| 357 | |
| 248 void _onIncoming(_HttpIncomingConnection incoming) { | 358 void _onIncoming(_HttpIncomingConnection incoming) { |
| 249 // TODO(ajohnsen): Handle redirect and auth. | 359 // TODO(ajohnsen): Handle auth. |
| 250 _responseCompleter.complete(new _HttpClientResponse(incoming)); | 360 var response = new _HttpClientResponse(incoming, |
| 361 this, | |
| 362 _httpClient, | |
| 363 _responseRedirects); | |
| 364 | |
| 365 Future<HttpClientResponse> future; | |
| 366 | |
| 367 if (followRedirects && | |
| 368 response.isRedirect) { | |
| 369 if (response.redirects.length < maxRedirects) { | |
| 370 // Redirect | |
| 371 future = response.redirect(); | |
| 372 } else { | |
| 373 // End with exception, too many redirects. | |
| 374 future = new Future.immediateError( | |
| 375 new RedirectLimitExceededException(response.redirects)); | |
| 376 } | |
| 377 } else { | |
| 378 future = new Future<HttpClientResponse>.immediate(response); | |
| 379 } | |
| 380 | |
| 381 future.then( | |
| 382 _responseCompleter.complete, | |
| 383 onError: (e) { | |
| 384 _responseCompleter.completeError(e.error, e.stackTrace); | |
| 385 }); | |
| 251 } | 386 } |
| 252 | 387 |
| 253 void _onError(AsyncError error) { | 388 void _onError(AsyncError error) { |
| 254 _responseCompleter.completeError(error.error, error.stackTrace); | 389 _responseCompleter.completeError(error.error, error.stackTrace); |
| 255 } | 390 } |
| 256 | 391 |
| 257 void _writeHeader() { | 392 void _writeHeader() { |
| 258 writeSP() => add([_CharCode.SP]); | 393 writeSP() => add([_CharCode.SP]); |
| 259 writeCRLF() => add([_CharCode.CR, _CharCode.LF]); | 394 writeCRLF() => add([_CharCode.CR, _CharCode.LF]); |
| 260 | 395 |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 292 headers.add("cookie", sb.toString()); | 427 headers.add("cookie", sb.toString()); |
| 293 } | 428 } |
| 294 | 429 |
| 295 headers._finalize(); | 430 headers._finalize(); |
| 296 | 431 |
| 297 // Write headers. | 432 // Write headers. |
| 298 headers._write(this); | 433 headers._write(this); |
| 299 writeCRLF(); | 434 writeCRLF(); |
| 300 } | 435 } |
| 301 | 436 |
| 302 Future<HttpClientResponse> get response => _responseCompleter.future; | 437 void _setResponseRedirects(List<RedirectInfo> redirects) { |
|
Søren Gjesse
2013/01/09 08:43:49
Why do you need this method?
Anders Johnsen
2013/01/09 10:03:00
Done.
| |
| 303 | 438 _responseRedirects = redirects; |
| 304 final String method; | 439 } |
| 305 final Uri uri; | |
| 306 final List<Cookie> cookies = new List<Cookie>(); | |
| 307 final Completer<HttpClientResponse> _responseCompleter | |
| 308 = new Completer<HttpClientResponse>(); | |
| 309 } | 440 } |
| 310 | 441 |
| 311 | 442 |
| 312 // Transformer that transforms data to HTTP Chunked Encoding. | 443 // Transformer that transforms data to HTTP Chunked Encoding. |
| 313 class _ChunkedTransformer extends StreamController<List<int>> | 444 class _ChunkedTransformer extends StreamController<List<int>> |
| 314 implements StreamTransformer<List<int>, List<int>> { | 445 implements StreamTransformer<List<int>, List<int>> { |
| 315 _ChunkedTransformer() : super.singleSubscription(); | 446 _ChunkedTransformer() : super.singleSubscription(); |
| 316 | 447 |
| 317 Stream<List<int>> bind(Stream<List<int>> stream) { | 448 Stream<List<int>> bind(Stream<List<int>> stream) { |
| 318 var subscription = stream.listen( | 449 var subscription = stream.listen( |
| (...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 453 class _HttpClient implements HttpClient { | 584 class _HttpClient implements HttpClient { |
| 454 // TODO(ajohnsen): Use eviction timeout. | 585 // TODO(ajohnsen): Use eviction timeout. |
| 455 static const int DEFAULT_EVICTION_TIMEOUT = 60000; | 586 static const int DEFAULT_EVICTION_TIMEOUT = 60000; |
| 456 | 587 |
| 457 Future<HttpClientRequest> open(String method, | 588 Future<HttpClientRequest> open(String method, |
| 458 String host, | 589 String host, |
| 459 int port, | 590 int port, |
| 460 String path) { | 591 String path) { |
| 461 // TODO(sgjesse): The path set here can contain both query and | 592 // TODO(sgjesse): The path set here can contain both query and |
| 462 // fragment. They should be cracked and set correctly. | 593 // fragment. They should be cracked and set correctly. |
| 463 return _open(method, new Uri.fromComponents( | 594 return _openUrl(method, new Uri.fromComponents( |
| 464 scheme: "http", domain: host, port: port, path: path)); | 595 scheme: "http", domain: host, port: port, path: path)); |
| 465 } | 596 } |
| 466 | 597 |
| 467 Future<HttpClientRequest> openUrl(String method, Uri url) { | 598 Future<HttpClientRequest> openUrl(String method, Uri url) { |
| 468 return _openUrl(method, url); | 599 return _openUrl(method, url); |
| 469 } | 600 } |
| 470 | 601 |
| 471 Future<HttpClientRequest> get(String host, | 602 Future<HttpClientRequest> get(String host, |
| 472 int port, | 603 int port, |
| 473 String path) { | 604 String path) { |
| 474 return open("get", host, port, path); | 605 return open("get", host, port, path); |
| 475 } | 606 } |
| 476 | 607 |
| 477 Future<HttpClientRequest> getUrl(Uri url) { | 608 Future<HttpClientRequest> getUrl(Uri url) { |
| 478 return _open("get", url); | 609 return _openUrl("get", url); |
| 479 } | 610 } |
| 480 | 611 |
| 481 Future<HttpClientRequest> post(String host, | 612 Future<HttpClientRequest> post(String host, |
| 482 int port, | 613 int port, |
| 483 String path) { | 614 String path) { |
| 484 return open("post", host, port, path); | 615 return open("post", host, port, path); |
| 485 } | 616 } |
| 486 | 617 |
| 487 Future<HttpClientRequest> postUrl(Uri url) { | 618 Future<HttpClientRequest> postUrl(Uri url) { |
| 488 return _open("post", url); | 619 return _openUrl("post", url); |
| 489 } | 620 } |
| 490 | 621 |
| 491 void close() { | 622 void close() { |
| 492 for (var queue in _idleConnections.values) { | 623 for (var queue in _idleConnections.values) { |
| 493 for (var connection in queue) { | 624 for (var connection in queue) { |
| 494 connection.destroy(); | 625 connection.destroy(); |
| 495 } | 626 } |
| 496 } | 627 } |
| 497 _idleConnections.clear(); | 628 _idleConnections.clear(); |
| 498 for (var connection in _activeConnections) { | 629 for (var connection in _activeConnections) { |
| 499 connection.destroy(); | 630 connection.destroy(); |
| 500 } | 631 } |
| 501 _activeConnections.clear(); | 632 _activeConnections.clear(); |
| 502 } | 633 } |
| 503 | 634 |
| 504 Future<HttpClientRequest> _open(String method, | 635 Future<HttpClientRequest> _openUrl(String method, |
| 505 Uri uri, | 636 Uri uri, |
| 506 [_HttpClientConnection connection]) { | 637 [_HttpClientConnection connection]) { |
| 507 if (method == null) { | 638 if (method == null) { |
| 508 throw new ArgumentError(method); | 639 throw new ArgumentError(method); |
| 509 } | 640 } |
| 510 // TODO(ajohnsen): Handle HTTPS. | 641 // TODO(ajohnsen): Handle HTTPS. |
| 511 if (uri.domain.isEmpty || uri.scheme != "http") { | 642 if (uri.domain.isEmpty || uri.scheme != "http") { |
| 512 throw new ArgumentError("Unsupported scheme '${uri.scheme}' in $uri"); | 643 throw new ArgumentError("Unsupported scheme '${uri.scheme}' in $uri"); |
| 513 } | 644 } |
| 514 | 645 |
| 515 // TODO(ajohnsen): Proxy? | 646 // TODO(ajohnsen): Proxy? |
| 516 Future future; | 647 Future future; |
| 648 int port = uri.port; | |
| 649 if (port == 0) port = HttpClient.DEFAULT_HTTP_PORT; | |
| 517 if (connection == null) { | 650 if (connection == null) { |
| 518 int port = uri.port; | |
| 519 if (port == 0) port = HttpClient.DEFAULT_HTTP_PORT; | |
| 520 future = _getConnection(uri.domain, port); | 651 future = _getConnection(uri.domain, port); |
| 521 } else { | 652 } else { |
| 522 future = new Future.immediate(connection); | 653 future = new Future.immediate(connection); |
| 523 } | 654 } |
| 524 | 655 |
| 525 return future.then((_HttpClientConnection connection) { | 656 return future.then((_HttpClientConnection connection) { |
| 526 onDone() { | 657 onDone() { |
| 527 // Called when the connection request/response sequence has ended. | 658 // Called when the connection request/response sequence has ended. |
| 528 _returnConnection(connection); | 659 _returnConnection(connection); |
| 529 } | 660 } |
| 530 // Create new internal outgoing connection. | 661 // Create new internal outgoing connection. |
| 531 var outgoing = new _HttpOutgoingConnection(); | 662 var outgoing = new _HttpOutgoingConnection(); |
| 532 // Create new request object, wrapping the outgoing connection. | 663 // Create new request object, wrapping the outgoing connection. |
| 533 var request = new _HttpClientRequest( | 664 var request = new _HttpClientRequest(outgoing, |
| 534 uri, method.toUpperCase(), outgoing); | 665 uri, |
| 666 method.toUpperCase(), | |
| 667 this); | |
| 668 request.headers.host = uri.domain; | |
| 669 request.headers.port = port; | |
| 535 // Start sending the request (lazy, delayed until the user provides | 670 // Start sending the request (lazy, delayed until the user provides |
| 536 // data). | 671 // data). |
| 537 connection.sendRequest(outgoing, onDone) | 672 connection.sendRequest(outgoing, onDone) |
| 538 .then((incoming) { | 673 .then((incoming) { |
| 539 // The full request have been sent and a response is received | 674 // The full request have been sent and a response is received |
| 540 // containing status-code, headers and etc. | 675 // containing status-code, headers and etc. |
| 541 request._onIncoming(incoming); | 676 request._onIncoming(incoming); |
| 542 }) | 677 }) |
| 543 .catchError((error) { | 678 .catchError((error) { |
| 544 // An error occoured before the http-header was parsed. This | 679 // An error occoured before the http-header was parsed. This |
| (...skipping 363 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 908 | 1043 |
| 909 | 1044 |
| 910 class _RedirectInfo implements RedirectInfo { | 1045 class _RedirectInfo implements RedirectInfo { |
| 911 const _RedirectInfo(int this.statusCode, | 1046 const _RedirectInfo(int this.statusCode, |
| 912 String this.method, | 1047 String this.method, |
| 913 Uri this.location); | 1048 Uri this.location); |
| 914 final int statusCode; | 1049 final int statusCode; |
| 915 final String method; | 1050 final String method; |
| 916 final Uri location; | 1051 final Uri location; |
| 917 } | 1052 } |
| OLD | NEW |