Skip to main content

polly_dart_http

polly_dart_http integrates package:http with polly_dart's cancellation system. It provides CancellableHttpClient — a drop-in http.BaseClient wrapper that aborts in-flight requests when a CancellationToken is cancelled.

Installation

dependencies:
polly_dart: ^0.0.8
polly_dart_http: ^0.0.1
http: ^1.2.0

Quick start

import 'package:polly_dart/polly_dart.dart';
import 'package:polly_dart_http/polly_dart_http.dart';

final pipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 5))
.addRetry(RetryStrategyOptions(maxRetryAttempts: 3))
.build();

final body = await pipeline.execute((context) async {
final client = CancellableHttpClient(token: context.cancellationToken);
try {
final response = await client.get(Uri.parse('https://api.example.com/data'));
return response.body;
} finally {
client.close();
}
});

When the TimeoutStrategy fires after 5 seconds, it cancels the context. CancellableHttpClient detects this and throws OperationCancelledException — the pipeline sees a timeout, not a hanging request.

API

CancellableHttpClient

CancellableHttpClient({
http.Client? inner, // optional inner client; defaults to http.Client()
required CancellationToken token,
})

Extends http.BaseClient, so it works anywhere http.Client is accepted.

send() — application-level cancellation

The overridden send() method races your request against token.whenCancelled using Future.any. The pipeline stops waiting immediately when cancelled; the underlying socket may still complete in the background (result is discarded).

final request = http.Request('GET', Uri.parse('https://api.example.com/data'));
final response = await client.send(request);

Use send() when you already have a BaseRequest object (e.g., from a middleware or interceptor chain).

sendAbortable() — socket-level cancellation

For new request construction, sendAbortable() uses http.AbortableRequest internally, which closes the socket as soon as the token is cancelled. This saves bandwidth on large responses.

final response = await client.sendAbortable(
'GET',
Uri.parse('https://api.example.com/data'),
headers: {'Authorization': 'Bearer $token'},
);
// POST with a body
final response = await client.sendAbortable(
'POST',
Uri.parse('https://api.example.com/users'),
headers: {'Content-Type': 'application/json'},
body: '{"name": "Alice"}',
);

Supported body types: String, List<int>, Map<String, String>.

Cancellation behaviour

ScenarioResult
Token not cancelledRequest completes normally
Token cancelled before send()Throws OperationCancelledException synchronously
Token cancelled during send()Throws OperationCancelledException immediately (background socket may finish)
Token cancelled during sendAbortable()Socket is closed, throws RequestAbortedException from package:http

Real-world example

import 'dart:convert';
import 'package:polly_dart/polly_dart.dart';
import 'package:polly_dart_http/polly_dart_http.dart';
import 'package:http/http.dart' as http;

class ApiClient {
final ResiliencePipeline _pipeline;

ApiClient()
: _pipeline = ResiliencePipelineBuilder()
.addTimeout(Duration(seconds: 10))
.addRetry(RetryStrategyOptions(
maxRetryAttempts: 3,
delay: Duration(milliseconds: 500),
backoffType: DelayBackoffType.exponential,
))
.addCircuitBreaker()
.build();

Future<Map<String, dynamic>> get(String url) {
return _pipeline.execute((context) async {
final client = CancellableHttpClient(token: context.cancellationToken);
try {
// Use sendAbortable for socket-level cancellation on GET requests
final response = await client.sendAbortable('GET', Uri.parse(url));

if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}

final bytes = await response.stream.toBytes();
return json.decode(utf8.decode(bytes)) as Map<String, dynamic>;
} finally {
client.close();
}
});
}

Future<Map<String, dynamic>> post(String url, Map<String, dynamic> body) {
return _pipeline.execute((context) async {
final client = CancellableHttpClient(token: context.cancellationToken);
try {
final response = await client.sendAbortable(
'POST',
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: json.encode(body),
);

if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception('HTTP ${response.statusCode}');
}

final bytes = await response.stream.toBytes();
return json.decode(utf8.decode(bytes)) as Map<String, dynamic>;
} finally {
client.close();
}
});
}
}

With a custom inner client

You can supply your own http.Client — useful for testing or when you already have a configured client (e.g., one with custom SSL certificates):

final myClient = http.Client(); // or IOClient(HttpClient()..badCertificateCallback = ...)

final client = CancellableHttpClient(
inner: myClient,
token: context.cancellationToken,
);

Testing

CancellableHttpClient is easy to test by supplying a fake inner client:

class FakeClient extends http.BaseClient {
final Duration delay;
FakeClient(this.delay);


Future<http.StreamedResponse> send(http.BaseRequest request) async {
await Future.delayed(delay);
return http.StreamedResponse(Stream.value([]), 200, request: request);
}
}

test('cancels mid-request', () async {
final token = CancellationToken();
final client = CancellableHttpClient(
inner: FakeClient(Duration(milliseconds: 300)),
token: token,
);

final future = client.send(http.Request('GET', Uri.parse('https://example.com')));

await Future.delayed(Duration(milliseconds: 50));
token.cancel();

await expectLater(future, throwsA(isA<OperationCancelledException>()));
client.close();
});

Next steps