Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for request timeout #521

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
60 changes: 40 additions & 20 deletions lib/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export 'src/streamed_response.dart';
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
_withClient((client) => client.head(url, headers: headers));
Future<Response> head(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
natebosch marked this conversation as resolved.
Show resolved Hide resolved
_withClient(
(client) => client.head(url, headers: headers, timeout: timeout));

/// Sends an HTTP GET request with the given headers to the given URL.
///
Expand All @@ -42,8 +44,10 @@ Future<Response> head(Uri url, {Map<String, String>? headers}) =>
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_withClient((client) => client.get(url, headers: headers));
Future<Response> get(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
_withClient(
(client) => client.get(url, headers: headers, timeout: timeout));

/// Sends an HTTP POST request with the given headers and body to the given URL.
///
Expand All @@ -64,9 +68,12 @@ Future<Response> get(Uri url, {Map<String, String>? headers}) =>
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_withClient((client) =>
client.post(url, headers: headers, body: body, encoding: encoding));
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_withClient((client) => client.post(url,
headers: headers, body: body, encoding: encoding, timeout: timeout));

/// Sends an HTTP PUT request with the given headers and body to the given URL.
///
Expand All @@ -87,9 +94,12 @@ Future<Response> post(Uri url,
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_withClient((client) =>
client.put(url, headers: headers, body: body, encoding: encoding));
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_withClient((client) => client.put(url,
headers: headers, body: body, encoding: encoding, timeout: timeout));

/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL.
Expand All @@ -111,9 +121,12 @@ Future<Response> put(Uri url,
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> patch(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_withClient((client) =>
client.patch(url, headers: headers, body: body, encoding: encoding));
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_withClient((client) => client.patch(url,
headers: headers, body: body, encoding: encoding, timeout: timeout));

/// Sends an HTTP DELETE request with the given headers to the given URL.
///
Expand All @@ -123,9 +136,12 @@ Future<Response> patch(Uri url,
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_withClient((client) =>
client.delete(url, headers: headers, body: body, encoding: encoding));
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_withClient((client) => client.delete(url,
headers: headers, body: body, encoding: encoding, timeout: timeout));

/// Sends an HTTP GET request with the given headers to the given URL and
/// returns a Future that completes to the body of the response as a [String].
Expand All @@ -139,8 +155,10 @@ Future<Response> delete(Uri url,
///
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<String> read(Uri url, {Map<String, String>? headers}) =>
_withClient((client) => client.read(url, headers: headers));
Future<String> read(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
_withClient(
(client) => client.read(url, headers: headers, timeout: timeout));

/// Sends an HTTP GET request with the given headers to the given URL and
/// returns a Future that completes to the body of the response as a list of
Expand All @@ -155,8 +173,10 @@ Future<String> read(Uri url, {Map<String, String>? headers}) =>
///
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) =>
_withClient((client) => client.readBytes(url, headers: headers));
Future<Uint8List> readBytes(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
_withClient(
(client) => client.readBytes(url, headers: headers, timeout: timeout));

Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
var client = Client();
Expand Down
58 changes: 39 additions & 19 deletions lib/src/base_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,63 @@ import 'streamed_response.dart';
/// maybe [close], and then they get various convenience methods for free.
abstract class BaseClient implements Client {
@override
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed('HEAD', url, headers);
Future<Response> head(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
_sendUnstreamed('HEAD', url, headers, timeout: timeout);

@override
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed('GET', url, headers);
Future<Response> get(Uri url,
{Map<String, String>? headers, Duration? timeout}) =>
_sendUnstreamed('GET', url, headers, timeout: timeout);

@override
Future<Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_sendUnstreamed('POST', url, headers, body, encoding);
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_sendUnstreamed('POST', url, headers,
body: body, encoding: encoding, timeout: timeout);

@override
Future<Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_sendUnstreamed('PUT', url, headers, body, encoding);
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_sendUnstreamed('PUT', url, headers,
body: body, encoding: encoding, timeout: timeout);

@override
Future<Response> patch(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_sendUnstreamed('PATCH', url, headers, body, encoding);
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_sendUnstreamed('PATCH', url, headers,
body: body, encoding: encoding, timeout: timeout);

@override
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
_sendUnstreamed('DELETE', url, headers, body, encoding);
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout}) =>
_sendUnstreamed('DELETE', url, headers,
body: body, encoding: encoding, timeout: timeout);

@override
Future<String> read(Uri url, {Map<String, String>? headers}) async {
final response = await get(url, headers: headers);
Future<String> read(Uri url,
{Map<String, String>? headers, Duration? timeout}) async {
final response = await get(url, headers: headers, timeout: timeout);
_checkResponseSuccess(url, response);
return response.body;
}

@override
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) async {
final response = await get(url, headers: headers);
Future<Uint8List> readBytes(Uri url,
{Map<String, String>? headers, Duration? timeout}) async {
final response = await get(url, headers: headers, timeout: timeout);
_checkResponseSuccess(url, response);
return response.bodyBytes;
}
Expand All @@ -68,12 +88,12 @@ abstract class BaseClient implements Client {
/// later point, or it could already be closed when it's returned. Any
/// internal HTTP errors should be wrapped as [ClientException]s.
@override
Future<StreamedResponse> send(BaseRequest request);
Future<StreamedResponse> send(BaseRequest request, {Duration? timeout});

/// Sends a non-streaming [Request] and returns a non-streaming [Response].
Future<Response> _sendUnstreamed(
String method, Uri url, Map<String, String>? headers,
[body, Encoding? encoding]) async {
{dynamic body, Encoding? encoding, Duration? timeout}) async {
natebosch marked this conversation as resolved.
Show resolved Hide resolved
var request = Request(method, url);

if (headers != null) request.headers.addAll(headers);
Expand All @@ -90,7 +110,7 @@ abstract class BaseClient implements Client {
}
}

return Response.fromStream(await send(request));
return Response.fromStream(await send(request, timeout: timeout));
}

/// Throws an error if [response] is not successful.
Expand Down
4 changes: 2 additions & 2 deletions lib/src/base_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,11 @@ abstract class BaseRequest {
/// the request is complete. If you're planning on making multiple requests to
/// the same server, you should use a single [Client] for all of those
/// requests.
Future<StreamedResponse> send() async {
Future<StreamedResponse> send({Duration? timeout}) async {
var client = Client();

try {
var response = await client.send(this);
var response = await client.send(this, timeout: timeout);
var stream = onDone(response.stream, client.close);
return StreamedResponse(ByteStream(stream), response.statusCode,
contentLength: response.contentLength,
Expand Down
37 changes: 28 additions & 9 deletions lib/src/browser_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,24 @@ class BrowserClient extends BaseClient {

/// Sends an HTTP request and asynchronously returns the response.
@override
Future<StreamedResponse> send(BaseRequest request) async {
Future<StreamedResponse> send(BaseRequest request, {Duration? timeout}) {
final completer = Completer<StreamedResponse>();
_send(request, timeout, completer);
return completer.future;
}

Future<void> _send(BaseRequest request, Duration? timeout,
Completer<StreamedResponse> completer) async {
Timer? timer;
late void Function() onTimeout;
natebosch marked this conversation as resolved.
Show resolved Hide resolved
if (timeout != null) {
timer = Timer(timeout, () {
onTimeout();
});
onTimeout = () {
completer.completeError(TimeoutException('Request aborted', timeout));
};
}
var bytes = await request.finalize().toBytes();
var xhr = HttpRequest();
_xhrs.add(xhr);
Expand All @@ -50,34 +67,36 @@ class BrowserClient extends BaseClient {
..responseType = 'arraybuffer'
..withCredentials = withCredentials;
request.headers.forEach(xhr.setRequestHeader);

var completer = Completer<StreamedResponse>();
if (timeout != null) {
onTimeout = () {
xhr.abort();
natebosch marked this conversation as resolved.
Show resolved Hide resolved
completer.completeError(TimeoutException('Request aborted', timeout));
};
}

unawaited(xhr.onLoad.first.then((_) {
var body = (xhr.response as ByteBuffer).asUint8List();
timer?.cancel();
completer.complete(StreamedResponse(
natebosch marked this conversation as resolved.
Show resolved Hide resolved
ByteStream.fromBytes(body), xhr.status!,
contentLength: body.length,
request: request,
headers: xhr.responseHeaders,
reasonPhrase: xhr.statusText));
_xhrs.remove(xhr);
}));

unawaited(xhr.onError.first.then((_) {
// Unfortunately, the underlying XMLHttpRequest API doesn't expose any
// specific information about the error itself.
timer?.cancel();
completer.completeError(
ClientException('XMLHttpRequest error.', request.url),
StackTrace.current);
_xhrs.remove(xhr);
}));

xhr.send(bytes);

try {
return await completer.future;
} finally {
_xhrs.remove(xhr);
}
}

/// Closes the client.
Expand Down
34 changes: 25 additions & 9 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ abstract class Client {
/// Sends an HTTP HEAD request with the given headers to the given URL.
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> head(Uri url, {Map<String, String>? headers});
Future<Response> head(Uri url,
{Map<String, String>? headers, Duration? timeout});

/// Sends an HTTP GET request with the given headers to the given URL.
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> get(Uri url, {Map<String, String>? headers});
Future<Response> get(Uri url,
{Map<String, String>? headers, Duration? timeout});

/// Sends an HTTP POST request with the given headers and body to the given
/// URL.
Expand All @@ -60,7 +62,10 @@ abstract class Client {
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding});
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout});

/// Sends an HTTP PUT request with the given headers and body to the given
/// URL.
Expand All @@ -81,7 +86,10 @@ abstract class Client {
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding});
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout});

/// Sends an HTTP PATCH request with the given headers and body to the given
/// URL.
Expand All @@ -102,13 +110,19 @@ abstract class Client {
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> patch(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding});
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout});

/// Sends an HTTP DELETE request with the given headers to the given URL.
///
/// For more fine-grained control over the request, use [send] instead.
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding});
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
Duration? timeout});

/// Sends an HTTP GET request with the given headers to the given URL and
/// returns a Future that completes to the body of the response as a String.
Expand All @@ -118,7 +132,8 @@ abstract class Client {
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
Future<String> read(Uri url, {Map<String, String>? headers});
Future<String> read(Uri url,
{Map<String, String>? headers, Duration? timeout});

/// Sends an HTTP GET request with the given headers to the given URL and
/// returns a Future that completes to the body of the response as a list of
Expand All @@ -129,10 +144,11 @@ abstract class Client {
///
/// For more fine-grained control over the request and response, use [send] or
/// [get] instead.
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers});
Future<Uint8List> readBytes(Uri url,
{Map<String, String>? headers, Duration? timeout});

/// Sends an HTTP request and asynchronously returns the response.
Future<StreamedResponse> send(BaseRequest request);
Future<StreamedResponse> send(BaseRequest request, {Duration? timeout});

/// Closes the client and cleans up any resources associated with it.
///
Expand Down