Add redirect support to the HTTP client

Redirection is implemented by re-using the same HttpClientConnection
object for several request/response pairs. There is s redirect method
on the HttpClientConnection object for manual redirection. The
HttpClientConnection object also has properties followRedirects and
maxRedirects to configure automatic redirect handling.

When redirection happen the HttpClientConnection will collect the
redirection history for client einspection and for redirect loop
detection.

R=ajohnsen@google.com, ager@google.com

BUG=none
TEST=tests/standalone/io/http_redirect_test.dart

Review URL: https://chromiumcodereview.appspot.com//10386024

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7415 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
sgjesse@google.com 2012-05-08 13:32:45 +00:00
parent 8f84e326cd
commit 6e1f264b8a
5 changed files with 310 additions and 10 deletions

View file

@ -505,6 +505,34 @@ interface HttpClientConnection {
*/
void set onError(void callback(e));
/**
* Set this property to [:true:] if this connection should
* automatically follow redirects. The default is [:true:].
*/
bool followRedirect;
/**
* Set this property to the maximum number of redirects to follow
* when [followRedirect] is [:true:]. If this number is exceeded the
* [onError] callback will be called with a [RedirectLimitExceeded]
* exception. The default value is 5.
*/
int maxRedirects;
/**
* Returns the series of redirects this connection has been through.
*/
List<RedirectInfo> get redirects();
/**
* Redirect this connection to a new URL. The default value for
* [method] is the method for the current request. The default value
* for [url] is the value of the [:HttpStatus.LOCATION:] header of
* the current response. All body data must have been read from the
* current response before calling [redirect].
*/
void redirect([String method, Uri url]);
/**
* Detach the underlying socket from the HTTP client. When the
* socket is detached the HTTP client will no longer perform any
@ -566,6 +594,14 @@ interface HttpClientResponse default _HttpClientResponse {
*/
int get contentLength();
/**
* Returns whether the status code is one of the normal redirect
* codes [:HttpStatus.MOVED_PERMANENTLY:], [:HttpStatus.FOUND:],
* [:HttpStatus.MOVED_TEMPORARILY:], [:HttpStatus.SEE_OTHER:] and
* [:HttpStatus.TEMPORARY_REDIRECT:].
*/
bool get isRedirect();
/**
* Returns the response headers.
*/
@ -579,6 +615,27 @@ interface HttpClientResponse default _HttpClientResponse {
}
/**
* Redirect information.
*/
interface RedirectInfo {
/**
* Returns the status code used for the redirect.
*/
int get statusCode();
/**
* Returns the method used for the redirect.
*/
String get method();
/**
* Returns the location for the redirect.
*/
Uri get location();
}
/**
* When detaching a socket from either the [:HttpServer:] or the
* [:HttpClient:] due to a HTTP connection upgrade there might be
@ -597,3 +654,22 @@ class HttpException implements Exception {
String toString() => "HttpException: $message";
final String message;
}
class RedirectException extends HttpException {
const RedirectException(String message,
List<RedirectInfo> this.redirects) : super(message);
final List<RedirectInfo> redirects;
}
class RedirectLimitExceeded extends RedirectException {
const RedirectLimitExceeded(List<RedirectInfo> redirects)
: super("Redirect limit exceeded", redirects);
}
class RedirectLoop extends RedirectException {
const RedirectLoop(List<RedirectInfo> redirects)
: super("Redirect loop detected", redirects);
}

View file

@ -1148,6 +1148,13 @@ class _HttpClientResponse
int get statusCode() => _statusCode;
String get reasonPhrase() => _reasonPhrase;
bool get isRedirect() {
return statusCode == HttpStatus.MOVED_PERMANENTLY ||
statusCode == HttpStatus.FOUND ||
statusCode == HttpStatus.SEE_OTHER ||
statusCode == HttpStatus.TEMPORARY_REDIRECT;
}
InputStream get inputStream() {
if (_inputStream == null) {
_inputStream = new _HttpInputStream(this);
@ -1171,7 +1178,32 @@ class _HttpClientResponse
void _onHeadersComplete() {
_headers._mutable = false;
_buffer = new _BufferList();
if (_connection._onResponse != null) {
if (isRedirect && _connection.followRedirects) {
if (_connection._redirects == null ||
_connection._redirects.length < _connection.maxRedirects) {
// Check the location header.
List<String> location = headers[HttpHeaders.LOCATION];
if (location == null || location.length > 1) {
throw new RedirectException("Invalid redirect",
_connection._redirects);
}
// Check for redirect loop
if (_connection._redirects != null) {
Uri redirectUrl = new Uri.fromString(location[0]);
for (int i = 0; i < _connection._redirects.length; i++) {
if (_connection._redirects[i].location.toString() ==
redirectUrl.toString()) {
throw new RedirectLoop(_connection._redirects);
}
}
}
// Drain body and redirect.
inputStream.onData = inputStream.read;
inputStream.onClosed = _connection.redirect;
} else {
throw new RedirectLimitExceeded(_connection._redirects);
}
} else if (_connection._onResponse != null) {
_connection._onResponse(this);
}
}
@ -1182,8 +1214,8 @@ class _HttpClientResponse
}
void _onDataEnd() {
if (_inputStream != null) _inputStream._closeReceived();
_connection._responseDone();
if (_inputStream != null) _inputStream._closeReceived();
}
// Delegate functions for the HttpInputStream implementation.
@ -1322,6 +1354,26 @@ class _HttpClientConnection
_onErrorCallback = callback;
}
void redirect([String method, Uri url]) {
if (_socketConn != null) {
throw new HttpException("Cannot redirect with body data pending");
}
if (method == null) method = _method;
if (url == null) {
url = new Uri.fromString(_response.headers.value(HttpHeaders.LOCATION));
}
if (_redirects == null) {
_redirects = new List<_RedirectInfo>();
}
_redirects.add(new _RedirectInfo(_response.statusCode, method, url));
_request = null;
_response = null;
// Open redirect URL using the same connection instance.
_client._openUrl(method, url, this);
}
List<RedirectInfo> get redirects() => _redirects;
Function _onRequest;
Function _onResponse;
Function _onErrorCallback;
@ -1332,6 +1384,11 @@ class _HttpClientConnection
HttpClientResponse _response;
String _method;
// Redirect handling
bool followRedirects = true;
int maxRedirects = 5;
List<_RedirectInfo> _redirects;
// Callbacks.
var requestReceived;
}
@ -1371,14 +1428,28 @@ class _HttpClient implements HttpClient {
HttpClientConnection open(
String method, String host, int port, String path) {
_open(method, host, port, path);
}
HttpClientConnection _open(String method,
String host,
int port,
String path,
[_HttpClientConnection connection]) {
if (_shutdown) throw new HttpException("HttpClient shutdown");
if (method == null || host == null || port == null || path == null) {
throw new IllegalArgumentException(null);
}
return _prepareHttpClientConnection(host, port, method, path);
return _prepareHttpClientConnection(host, port, method, path, connection);
}
HttpClientConnection openUrl(String method, Uri url) {
_openUrl(method, url);
}
HttpClientConnection _openUrl(String method,
Uri url,
[_HttpClientConnection connection]) {
if (url.scheme != "http") {
throw new HttpException("Unsupported URL scheme ${url.scheme}");
}
@ -1396,20 +1467,20 @@ class _HttpClient implements HttpClient {
} else {
path = url.path;
}
return open(method, url.domain, port, path);
return _open(method, url.domain, port, path, connection);
}
HttpClientConnection get(String host, int port, String path) {
return open("GET", host, port, path);
return _open("GET", host, port, path);
}
HttpClientConnection getUrl(Uri url) => openUrl("GET", url);
HttpClientConnection getUrl(Uri url) => _openUrl("GET", url);
HttpClientConnection post(String host, int port, String path) {
return open("POST", host, port, path);
return _open("POST", host, port, path);
}
HttpClientConnection postUrl(Uri url) => openUrl("POST", url);
HttpClientConnection postUrl(Uri url) => _openUrl("POST", url);
void shutdown() {
_openSockets.forEach((String key, Queue<_SocketConnection> connections) {
@ -1432,7 +1503,11 @@ class _HttpClient implements HttpClient {
}
HttpClientConnection _prepareHttpClientConnection(
String host, int port, String method, String path) {
String host,
int port,
String method,
String path,
[_HttpClientConnection connection]) {
void _connectionOpened(_SocketConnection socketConn,
_HttpClientConnection connection) {
@ -1447,7 +1522,10 @@ class _HttpClient implements HttpClient {
}
}
_HttpClientConnection connection = new _HttpClientConnection(this);
// Create a new connection if we are not re-using an existing one.
if (connection == null) {
connection = new _HttpClientConnection(this);
}
// If there are active connections for this key get the first one
// otherwise create a new one.
@ -1550,3 +1628,13 @@ class _DetachedSocket implements DetachedSocket {
Socket _socket;
List<int> _unparsedData;
}
class _RedirectInfo implements RedirectInfo {
const _RedirectInfo(int this.statusCode,
String this.method,
Uri this.location);
final int statusCode;
final String method;
final Uri location;
}

View file

@ -0,0 +1,134 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
#import("dart:io");
#import("dart:uri");
HttpServer setupServer() {
HttpServer server = new HttpServer();
server.listen("127.0.0.1", 0, 5);
void addRedirectHandler(int number, int statusCode) {
server.addRequestHandler(
(HttpRequest request) => request.path == "/$number",
(HttpRequest request, HttpResponse response) {
response.headers.set(HttpHeaders.LOCATION,
"http://127.0.0.1:${server.port}/${number + 1}");
response.statusCode = statusCode;
response.outputStream.close();
});
}
// Setup redirect chain.
int n = 1;
addRedirectHandler(n++, HttpStatus.MOVED_PERMANENTLY);
addRedirectHandler(n++, HttpStatus.MOVED_TEMPORARILY);
addRedirectHandler(n++, HttpStatus.SEE_OTHER);
addRedirectHandler(n++, HttpStatus.TEMPORARY_REDIRECT);
for (int i = n; i < 10; i++) {
addRedirectHandler(i, HttpStatus.MOVED_PERMANENTLY);
}
// Setup redirect loop.
server.addRequestHandler(
(HttpRequest request) => request.path == "/A",
(HttpRequest request, HttpResponse response) {
response.headers.set(HttpHeaders.LOCATION,
"http://127.0.0.1:${server.port}/B");
response.statusCode = HttpStatus.MOVED_PERMANENTLY;
response.outputStream.close();
}
);
server.addRequestHandler(
(HttpRequest request) => request.path == "/B",
(HttpRequest request, HttpResponse response) {
response.headers.set(HttpHeaders.LOCATION,
"http://127.0.0.1:${server.port}/A");
response.statusCode = HttpStatus.MOVED_TEMPORARILY;
response.outputStream.close();
}
);
return server;
}
void checkRedirects(int redirectCount, HttpClientConnection conn) {
if (redirectCount < 2) {
Expect.isNull(conn.redirects);
} else {
Expect.equals(redirectCount - 1, conn.redirects.length);
for (int i = 0; i < redirectCount - 2; i++) {
Expect.equals(conn.redirects[i].location.path, "/${i + 2}");
}
}
}
void testManualRedirect() {
HttpServer server = setupServer();
HttpClient client = new HttpClient();
int redirectCount = 0;
HttpClientConnection conn =
client.getUrl(new Uri.fromString("http://127.0.0.1:${server.port}/1"));
conn.followRedirects = false;
conn.onResponse = (HttpClientResponse response) {
response.inputStream.onData = () => response.inputStream.read();
response.inputStream.onClosed = () {
redirectCount++;
if (redirectCount < 10) {
Expect.isTrue(response.isRedirect);
checkRedirects(redirectCount, conn);
conn.redirect();
} else {
Expect.equals(HttpStatus.NOT_FOUND, response.statusCode);
server.close();
client.shutdown();
}
};
};
}
void testAutoRedirect() {
HttpServer server = setupServer();
HttpClient client = new HttpClient();
HttpClientConnection conn =
client.getUrl(new Uri.fromString("http://127.0.0.1:${server.port}/1"));
conn.onResponse = (HttpClientResponse response) {
response.inputStream.onData = () => Expect.fail("Response not expected");
response.inputStream.onClosed = () => Expect.fail("Response not expected");
};
conn.onError = (e) {
Expect.isTrue(e is RedirectLimitExceeded);
Expect.equals(5, e.redirects.length);
server.close();
client.shutdown();
};
}
void testRedirectLoop() {
HttpServer server = setupServer();
HttpClient client = new HttpClient();
int redirectCount = 0;
HttpClientConnection conn =
client.getUrl(new Uri.fromString("http://127.0.0.1:${server.port}/A"));
conn.onResponse = (HttpClientResponse response) {
response.inputStream.onData = () => Expect.fail("Response not expected");
response.inputStream.onClosed = () => Expect.fail("Response not expected");
};
conn.onError = (e) {
Expect.isTrue(e is RedirectLoop);
Expect.equals(2, e.redirects.length);
server.close();
client.shutdown();
};
}
main() {
testManualRedirect();
testAutoRedirect();
testRedirectLoop();
}

View file

@ -431,6 +431,7 @@ void testReasonPhrase() {
HttpClient httpClient = new HttpClient();
HttpClientConnection conn =
httpClient.get("127.0.0.1", port, "/reasonformoving");
conn.followRedirects = false;
conn.onResponse = (HttpClientResponse response) {
Expect.equals(HttpStatus.MOVED_PERMANENTLY, response.statusCode);
Expect.equals("Don't come looking here any more", response.reasonPhrase);

View file

@ -114,6 +114,7 @@ void testNoUpgrade() {
};
HttpClientConnection conn = client.get("127.0.0.1", server.port, "/");
conn.followRedirects = false;
WebSocketClientConnection wsconn = new WebSocketClientConnection(conn);
wsconn.onNoUpgrade = (response) {
Expect.equals(HttpStatus.MOVED_PERMANENTLY, response.statusCode);