dart_http2_flutter_sse

pub package License: MIT

A production-ready HTTP/2 client package for Dart and Flutter. Provides a clean, developer-friendly API similar to dio or http, with robust HTTP/2 support.

Features

  • HTTP/2 requests: GET, POST, PUT, PATCH, DELETE
  • Request/response headers, query parameters, JSON body, form body, raw bytes, and streaming body
  • Response streaming for large downloads
  • Connection reuse, multiplexing, and connection pooling
  • TLS/ALPN where available (native platforms)
  • Timeout support: connect, send, receive timeouts
  • Cancellation tokens for cancelling active requests
  • Retry policy with exponential backoff and jitter
  • Interceptors for request, response, and error handling
  • Logging interceptor with safe redaction for tokens/passwords
  • Certificate pinning support (native platforms)
  • Graceful fallback — HTTP/1.1 on Web, with clear platform documentation
  • Strong typing, null safety, no unnecessary dependencies
  • Fully tested with unit and integration tests

Platform Support

Platform HTTP/2 Support Notes
Android ✅ Native Via dart:io HttpClient
iOS ✅ Native Via dart:io HttpClient
macOS ✅ Native Via dart:io HttpClient
Linux ✅ Native Via dart:io HttpClient
Windows ✅ Native Via dart:io HttpClient
Web ⚠️ Limited Uses browser's fetch (HTTP/1.1). Browser may negotiate HTTP/2 but no direct control. No certificate pinning, no streaming uploads.

Installation

Add to your pubspec.yaml:

dependencies:
  dart_http2_flutter_sse: ^1.0.0

Then run:

flutter pub get

Quick Start

import 'package:dart_http2_flutter_sse/dart_http2_flutter_sse.dart';

final client = Http2Client();

final response = await client.get(
  'https://api.example.com/users',
  queryParameters: {'page': 1},
);

print(response.statusCode); // 200
print(response.data);        // Decoded JSON body

Usage

Basic Requests

// GET
final getRes = await client.get('https://api.example.com/items');

// POST with JSON body
final postRes = await client.post(
  'https://api.example.com/items',
  body: {'name': 'New Item', 'price': 29.99},
);

// POST with form data
final formRes = await client.post(
  'https://api.example.com/login',
  body: {'username': 'john', 'password': 'secret'},
  bodyType: BodyType.form,
);

// PUT
final putRes = await client.put(
  'https://api.example.com/items/1',
  body: {'name': 'Updated Item'},
);

// PATCH
final patchRes = await client.patch(
  'https://api.example.com/items/1',
  body: {'price': 19.99},
);

// DELETE
final deleteRes = await client.delete('https://api.example.com/items/1');

Query Parameters & Headers

final response = await client.get(
  'https://api.example.com/search',
  queryParameters: {
    'q': 'flutter',
    'page': 1,
    'limit': 20,
  },
  headers: {
    'Authorization': 'Bearer your-token',
    'X-Custom': 'value',
  },
);

Response Handling

final response = await client.get('https://api.example.com/data');

print(response.statusCode);    // 200
print(response.isOk);          // true
print(response.bodyString);    // Raw body string
print(response.data);          // Auto-decoded JSON (Map, List, or String)
print(response.dataAsMap);     // Null-safe JSON map access
print(response.headers);       // Response headers
print(response.contentLength); // Content-Length from headers
print(response.duration);      // Request duration

Streaming Download

final request = Http2Request.get('https://example.com/large-file.zip');

int totalBytes = 0;
await for (final chunk in client.streamResponse(request)) {
  totalBytes += chunk.bodyBytes?.length ?? 0;
  print('Received $totalBytes bytes so far...');
}
print('Download complete: $totalBytes bytes');

Streaming Upload

final fileStream = File('large-file.txt').openRead();

final response = await client.post(
  'https://api.example.com/upload',
  body: fileStream,
  bodyType: BodyType.stream,
  onSendProgress: (sent, total) {
    print('Uploaded $sent of $total bytes');
  },
);

Timeouts

final client = Http2Client(
  timeoutConfiguration: const TimeoutConfiguration(
    connectTimeout: Duration(seconds: 10),
    sendTimeout: Duration(seconds: 30),
    receiveTimeout: Duration(seconds: 30),
  ),
);

// Or use the factory for all-equal timeouts:
final client2 = Http2Client(
  timeoutConfiguration: TimeoutConfiguration.all(
    Duration(seconds: 15),
  ),
);

Cancellation

final cts = CancellationTokenSource();

// Start a request
final future = client.get(
  'https://api.example.com/slow-endpoint',
  cancellationToken: cts.token,
);

// Cancel it later
cts.cancel();

try {
  await future;
} on CancelledException catch (e) {
  print('Request was cancelled: $e');
}

Retry with Exponential Backoff

final client = Http2Client(
  retryConfig: const RetryConfig(
    maxRetries: 3,
    baseDelay: Duration(seconds: 1),
    maxDelay: Duration(seconds: 30),
    useJitter: true,     // Adds randomness to avoid thundering herd
    retryableStatusCodes: {
      408, // Request Timeout
      429, // Too Many Requests
      500, // Internal Server Error
      502, // Bad Gateway
      503, // Service Unavailable
      504, // Gateway Timeout
    },
    retryOnConnectionErrors: true,
  ),
);

Custom Retry Logic

final client = Http2Client(
  retryConfig: RetryConfig(
    maxRetries: 2,
    retryOnError: (error) {
      // Only retry on specific conditions
      if (error is TimeoutException) return true;
      if (error is HttpStatusException && error.statusCode == 429) return true;
      return false;
    },
  ),
);

Interceptors

// Logging interceptor (safe redaction built-in)
final client = Http2Client(
  interceptors: [
    LoggingInterceptor(const LoggingConfig(
      logRequest: true,
      logResponse: true,
      logRequestBody: false,  // Off by default for security
      logResponseBody: false, // Off by default for security
      logErrors: true,
    )),
  ],
);

// Custom interceptor
class AuthInterceptor extends Interceptor {
  final String token;

  AuthInterceptor(this.token);

  @override
  void onRequest(Http2Request request, RequestHandler handler) {
    request.headers['Authorization'] = 'Bearer $token';
    handler.next(request);
  }

  @override
  void onResponse(Http2Response response, ResponseHandler handler) {
    print('Response: ${response.statusCode}');
    handler.next(response);
  }

  @override
  void onError(Http2Exception error, ErrorHandler handler) {
    print('Error: $error');
    handler.next(error);
  }
}

final client = Http2Client(
  interceptors: [
    AuthInterceptor('your-token'),
    LoggingInterceptor(),
  ],
);

Certificate Pinning

final client = Http2Client(
  enableCertificatePinning: true,
  pinnedCertificates: {
    'api.example.com': [
      '-----BEGIN CERTIFICATE-----\nMIIF...',
    ],
  },
);

Note: Certificate pinning is only supported on native platforms (Android, iOS, macOS, Linux, Windows). Not available on Web.

Error Handling

try {
  final response = await client.get('https://api.example.com/data');
} on HttpStatusException catch (e) {
  print('HTTP error: ${e.statusCode} - ${e.message}');
  print('Response body: ${e.response?.bodyString}');
} on TimeoutException catch (e) {
  print('${e.timeoutType} timeout exceeded');
} on ConnectionException catch (e) {
  print('Connection failed to ${e.host}: ${e.message}');
} on CancelledException catch (e) {
  print('Request was cancelled');
} on CertificatePinningException catch (e) {
  print('Certificate pin mismatch for ${e.hostname}');
} on Http2Exception catch (e) {
  print('Other HTTP/2 error: ${e.message}');
}

Progress Tracking

final response = await client.get(
  'https://api.example.com/large-file',
  onReceiveProgress: (received, total) {
    final progress = total > 0 ? (received / total * 100).toStringAsFixed(1) : '?';
    print('Downloaded $received / $total bytes ($progress%)');
  },
);

Architecture

lib/
├── dart_http2_flutter_sse.dart      # Barrel export
├── src/
│   ├── client.dart                  # Http2Client public API
│   ├── models/
│   │   ├── http2_request.dart       # Request model with body serialization
│   │   ├── http2_response.dart      # Response model with JSON decoding
│   │   └── http2_exceptions.dart    # Typed exception hierarchy
│   ├── interceptors/
│   │   ├── interceptor.dart         # Interceptor interface + chain
│   │   ├── logging_interceptor.dart # Request/response logging with redaction
│   │   └── retry_interceptor.dart   # Retry with exponential backoff
│   ├── transport/
│   │   ├── transport.dart           # Abstract transport interface
│   │   ├── http2_transport.dart     # HTTP/2 transport (native)
│   │   ├── http1_fallback_transport.dart # HTTP/1.1 fallback
│   │   └── web_transport_stub.dart  # Web platform transport
│   └── utils/
│       ├── cancellation_token.dart  # Cancellation token system
│       ├── timeout_manager.dart     # Timeout enforcement
│       └── connection_pool.dart     # Connection pooling

Comparison: HTTP/2 vs HTTP/1.1

Feature HTTP/2 (this package) HTTP/1.1 (http package)
Multiplexing ✅ Multiple streams over single connection ❌ One request at a time per connection
Header compression ✅ HPACK compression ❌ Plain text headers
Server push ✅ Supported ❌ Not supported
Binary protocol ✅ Binary framing ❌ Text-based
Connection reuse ✅ Connection pooling built-in ⚠️ Limited (keep-alive)
Stream prioritization ✅ Supported ❌ Not supported
TLS requirement ⚠️ Most implementations require TLS ✅ Works over plain HTTP
Latency ✅ Lower (fewer round trips) ❌ Higher (head-of-line blocking)
Web support ⚠️ Limited (browser negotiation) ✅ Full support
Certificate pinning ✅ Supported (native) ❌ Not available

Comparison with Dio

Feature dart_http2_flutter_sse Dio
HTTP/2 focus ✅ First-class HTTP/2 support ⚠️ HTTP/1.1 with h2 via adapter
Package size ✅ Minimal dependencies ❌ Heavier (crypto, etc.)
Interceptors ✅ Full interceptor chain ✅ Full interceptor chain
Retry ✅ Built-in with exponential backoff ⚠️ Via add-on package
Cancellation ✅ Cancellation tokens ✅ CancelToken
Certificate pinning ✅ Built-in ⚠️ Via custom adapter
Streaming ✅ Response streaming ✅ Via ResponseType.stream
Web support ✅ HTTP/1.1 fallback ✅ Full support
pub.flutter-io.cn popularity New ✅ Very popular

Running Tests

# Run unit tests
flutter test

# Run integration tests (requires network)
flutter test --dart-define=RUN_INTEGRATION_TESTS=true integration_test/

Limitations

  • Web platform: HTTP/2 is not directly controllable from Dart on the web. The browser may negotiate HTTP/2 with the server, but this package falls back to HTTP/1.1 semantics. Certificate pinning and streaming uploads are not available on Web.
  • ALPN negotiation: On native platforms, ALPN (Application-Layer Protocol Negotiation) is handled by the underlying TLS library. If the server does not support HTTP/2, the connection falls back to HTTP/1.1.
  • Server push: While HTTP/2 supports server push, this client does not currently expose a push-specific API. Pushed resources are handled transparently.
  • Flow control: HTTP/2 stream flow control is handled by the underlying implementation and is not directly configurable through this package.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

dart_http2_flutter_sse
A production-ready HTTP/2 client package for Dart and Flutter.