Skip to main content

go_http 0.2.1 copy "go_http: ^0.2.1" to clipboard
go_http: ^0.2.1 copied to clipboard

Reliable HTTP client for Dart — built for control, cancellation and consistency.

go_http #

Reliable HTTP client for Dart — built for control, cancellation and consistency.

pub package License: BSD 3-Clause

go_http is a modern, cross-platform HTTP client for Dart inspired by Python httpx. It provides fine-grained control over requests, structured timeouts, per-phase cancellation, pluggable auth and transport, batch execution, and a powerful interceptor system.

Features #

  • Cross-platform — works identically on Dart CLI, Flutter Mobile/Desktop, and Web
  • Cancellation — every request can be cancelled mid-flight with CancellationToken
  • Retry Policy — smart retry with equal-jitter exponential backoff (idempotent methods only)
  • Structured timeouts — per-phase (connect, read, write, pool) via Timeout
  • Interceptors — request/response/error chain (LoggingInterceptor, AuthInterceptor)
  • Auth SPI — BasicAuth, DigestAuth (RFC 2617/7616, MD5/SHA-256, qop, cnonce), FunctionAuth
  • Bearer token refresh — auto-refresh on 401 and single retry
  • Cookie Store — RFC-matching jar with MemoryCookieStore (domain/path, multi-value Set-Cookie)
  • Headers — case-insensitive multi-value collection, sensitive-value masking in toString
  • Content-Encoding — gzip + deflate (with raw fallback), brotli/zstd via registerBrotli/registerZstd
  • Body encoding — json → application/json, Map → application/x-www-form-urlencoded
  • Multipart — multipart/form-data encoder (zero dependencies, streaming)
  • Proxy — per-URL-pattern mounts (ProxyMounts, URLPattern), NO_PROXY support, SOCKS5
  • URL & QueryParams — immutable Url (httpx-style copyWith/join), immutable QueryParams
  • Event hooks — multicast request/response callbacks, hot-swappable at runtime
  • Batch executor — bounded-concurrency batch execution with per-result Result<T>
  • Result sinks — JsonlSink, CsvSink (with formula-injection sanitization)
  • Type-safe decoders — pluggable Decoder<T> (JsonDecoder, BytesDecoder, custom)
  • Response model — text, json(), raiseForStatus(), charset-aware decoding, hasRedirectLocation, history
  • MockTransport — handler-backed transport for testing
  • Exception hierarchy — 30+ typed error classes (ConnectTimeoutError, ReadTimeoutError, ProxyError, HttpStatusError, …)
  • Status codes — StatusCode enum with 33 entries and category predicates
  • Metrics — timeline events via MetricsSink / ConsoleMetricsSink
  • Base URL — relative request URIs resolved against a client-level baseUrl

Installation #

dependencies:
  go_http: ^0.2.1
dart pub get

Quick Start #

GET #

import 'package:go_http/go_http.dart';

final client = GoHttpClient();

try {
  final response = await client.get<Uint8List>(
    Uri.parse('https://httpbin.org/get'),
  );
  print('Status: ${response.statusCode}');
  print('Body: ${response.text}');
} finally {
  client.dispose();
}

POST with JSON #

import 'package:go_http/go_http.dart';

final client = GoHttpClient();

try {
  final response = await client.post<Uint8List>(
    Uri.parse('https://httpbin.org/post'),
    json: {'name': 'John', 'email': 'john@example.com'},
  );
  print('Created: ${response.statusCode}');
  print(response.json());
} finally {
  client.dispose();
}

POST with form-encoded data #

final response = await client.post<Uint8List>(
  Uri.parse('https://httpbin.org/post'),
  data: {'key1': 'value1', 'key2': 'value2'},
);

Core Concepts #

buildRequest / send / request #

The client exposes a 3-layer API (mirroring httpx):

  1. buildRequest(Request) — merge client config (default headers, cookies, query params, base URL) onto a raw Request
  2. send<T>(Request, ...) — dispatch a prepared Request through the full pipeline (interceptors, transport, retry, redirect). Does not re-merge.
  3. request<T>(Request, ...) = send(buildRequest(req)) — convenience for the common case

Escape hatch: mutate a prepared request before sending.

final req = client.buildRequest(Request.get(uri));
req.headers.add('x-custom', '1');
final res = await client.send(req);

Cancellation #

import 'dart:async';

final client = GoHttpClient();
final cancelSource = CancellationSource();

Timer(const Duration(seconds: 2), () {
  cancelSource.cancel('Taking too long');
});

try {
  final response = await client.get<Uint8List>(
    Uri.parse('https://httpbin.org/delay/5'),
    cancel: cancelSource.token,
  );
} on CancellationError catch (e) {
  print('Cancelled: ${e.reason}');
} finally {
  cancelSource.dispose();
  client.dispose();
}

Structured Timeouts #

final client = GoHttpClient(
  timeout: const Timeout(
    connect: Duration(seconds: 10),
    read: Duration(seconds: 30),
    write: Duration(seconds: 30),
    pool: Duration(seconds: 10),
  ),
);

// Per-request override: disable connect timeout only
final response = await client.get<Uint8List>(
  uri,
  options: RequestOptions(
    timeout: Timeout(connect: null),
  ),
);

null = disabled for that phase. useClientDefault (the default) = inherit.

Retry Policy #

Default: 3 attempts, 300–2000ms equal-jitter backoff. Retries only for idempotent methods (GET, HEAD, OPTIONS) on network errors, timeouts, and 429/503/504.

final client = GoHttpClient(
  retryPolicy: DefaultRetryPolicy(
    maxAttempts: 5,
    baseDelay: const Duration(milliseconds: 500),
    maxDelay: const Duration(seconds: 5),
  ),
  metrics: ConsoleMetricsSink(),
);

Interceptors #

class TimingInterceptor extends Interceptor {
  @override
  Future<Request> onRequest(Request request) async {
    print('[${request.methodString}] ${request.uri}');
    return request;
  }

  @override
  Future<Response> onResponse(Response response) async {
    print('=> ${response.statusCode} (${response.elapsed})');
    return response;
  }
}

final client = GoHttpClient(
  interceptors: [TimingInterceptor(), LoggingInterceptor()],
);

Auth #

Basic:

final client = GoHttpClient(
  interceptors: [AuthInterceptor(auth: BasicAuth('user', 'pass'))],
);

Digest (RFC 2617/7616):

final client = GoHttpClient(
  interceptors: [AuthInterceptor(auth: DigestAuth('user', 'pass'))],
);

Bearer token with auto-refresh:

String? token;

final client = GoHttpClient(
  interceptors: [
    AuthInterceptor(
      tokenProvider: () async => token,
      tokenRefresher: () async {
        token = await fetchNewToken();
        return token;
      },
    ),
  ],
);

Event Hooks #

final client = GoHttpClient(
  eventHooks: EventHooks(
    request: [(req) => print('>> ${req.uri}')],
    response: [(res) => print('<< ${res.statusCode}')],
  ),
);

// Hot-swap at runtime:
client.eventHooks = client.eventHooks.copyWith(
  response: [(res) => metrics.record(res)],
);

Proxy #

final client = GoHttpClient(
  proxyMounts: ProxyMounts({
    URLPattern.parse('http://*'): Proxy.parse('http://proxy:8080'),
    URLPattern.parse('https://*.internal'): null, // direct
  }),
  trustEnv: true, // also honor HTTP_PROXY / HTTPS_PROXY / NO_PROXY
);
final client = GoHttpClient(
  verify: '/etc/ssl/certs/ca-certificates.crt', // custom CA file
  // verify: false, // disable TLS verification (not recommended)
);

Headers #

final headers = Headers({'content-type': 'application/json'});
headers.add('set-cookie', 'session=abc');
headers.add('set-cookie', 'token=xyz');

print(headers['content-type']); // "application/json"
print(headers.getAll('set-cookie')); // ["session=abc", "token=xyz"]

// Sensitive headers are masked in toString:
print(headers); // ... authorization: [secure] ...

URL & QueryParams #

final url = Url.parse('HTTPS://EXAMPLE.COM:443/path?a=1');
print(url.scheme); // "https" (lowercased)
print(url.port); // null (443 is default)

final joined = url.join('/sub?b=2');
print(joined); // https://example.com/sub?b=2

final q = QueryParams({'a': '1', 'b': '2'})
    .add('b', '3');
print(q.toQueryString()); // a=1&b=2&b=3

Multipart #

final mp = Multipart(
  fields: [MultipartField('name', 'file.txt')],
  files: [MultipartFile.bytes('file', 'photo.jpg', imageBytes)],
);

final response = await client.post<Uint8List>(
  uri,
  options: RequestOptions(body: mp),
);
// Content-Type and body are auto-encoded.

Content-Encoding #

// gzip and deflate are decoded automatically by default.
// Register brotli/zstd when packages are available:
registerBrotli(() => BrotliDecoder());
registerZstd(() => ZstdDecoder());

// Disable decompression per request:
final response = await client.get<Uint8List>(
  uri,
  options: RequestOptions(autoDecompress: false),
);

Batch Executor #

final executor = BatchExecutor(client, concurrency: 10);
final results = await executor.run<Uint8List>(
  uris.map((u) => Request.get(u)).toList(),
  onProgress: (done, total) => print('$done/$total'),
);

for (final result in results) {
  result.when(
    ok: (res) => print('OK: ${res.statusCode}'),
    fail: (err, req) => print('FAIL: $err'),
  );
}

Result Sinks #

final sink = ResultSink.jsonl('results.jsonl');
for (final result in results) {
  sink.write(result);
}
await sink.close();

Response Model #

final response = await client.get<Uint8List>(uri);

response.text; // body decoded as text (charset-aware)
response.json(); // body decoded as JSON
response.raiseForStatus(); // throw HttpStatusError on 4xx/5xx (chainable)
response.hasRedirectLocation; // true for navigable 3xx with Location
response.history; // redirect chain
response.elapsed; // full round-trip duration
response.charsetEncoding; // from Content-Type
response.encoding = 'windows-1251'; // override charset

MockTransport #

final mt = MockTransport((req) async {
  return Response(
    request: req,
    statusCode: 200,
    headers: {'content-type': 'text/plain'},
    data: Uint8List.fromList(utf8.encode('mock body')),
  );
});

final client = GoHttpClient(transport: mt);
final res = await client.get<Uint8List>(uri);
print(mt.callCount); // 1

Error Handling #

try {
  final response = await client.get<Uint8List>(uri);
} on ConnectTimeoutError catch (e) {
  print('Connection timed out after ${e.timeout}');
} on ReadTimeoutError catch (e) {
  print('Server did not send data in time');
} on ConnectError catch (e) {
  print('DNS or TCP-level failure: ${e.message}');
} on HttpStatusError catch (e) {
  print('HTTP ${e.statusCode}: ${e.response.text}');
} on CancellationError catch (e) {
  print('Cancelled: ${e.reason}');
} on HttpError catch (e) {
  print('Generic HTTP error: $e');
}

HttpStatusError deliberately does not extend RequestError, so catch (RequestError) never accidentally swallows a 4xx/5xx.

Platform Support #

Platform Transport Status
Dart CLI / Server IoTransport (dart:io) ✅ Full support
Flutter Mobile/Desktop IoTransport (dart:io) ✅ Full support
Flutter Web WebTransport (dart:html) ✅ Full support

The correct transport is selected automatically via conditional imports. Stub classes ensure compilation on all platforms with zero configuration.

Examples #

dart run example/simple_get.dart
dart run example/cancel_request.dart
dart run example/retry_policy.dart
dart run example/download_progress.dart
dart run example/post_json.dart

License #

BSD 3-Clause. See LICENSE.

1
likes
0
points
35
downloads

Publisher

unverified uploader

Weekly Downloads

Reliable HTTP client for Dart — built for control, cancellation and consistency.

Repository (GitHub)
View/report issues

Topics

#http #networking #api #http-client

License

unknown (license)

Dependencies

crypto, meta

More

Packages that depend on go_http