Envoy — Titan's HTTP Client & API Layer

pub package License: MIT

Envoy is a full-featured HTTP client built for the Titan ecosystem. It provides interceptors, caching, auth integration, metrics, WebSocket, SSE, and route-based data loading — all with zero external HTTP dependencies.

Features

Feature Class Purpose
HTTP Client Envoy Full-featured client with interceptor pipeline, timeouts, and redirects
Request Missive Request builder with method, headers, body, query params, cancel token
Response Dispatch Response wrapper with status, headers, body, and timing metadata
Interceptor Courier Base interceptor with onRequest, onResponse, onError hooks
Logging LogCourier Request/response logging with configurable detail levels
Retry RetryCourier Automatic retry with exponential backoff and configurable conditions
Auth AuthCourier Token injection with automatic refresh on 401 responses
Caching CacheCourier HTTP caching with 5 strategies (networkFirst, cacheFirst, staleWhileRevalidate, etc.)
Metrics MetricsCourier Per-request metrics collection with Colossus integration
Dedup DedupCourier Deduplicates concurrent identical GET requests
Cookies CookieCourier Cookie jar management (set-cookie/cookie headers)
Throttle Gate Request throttling with configurable concurrency limits
Cancel Token Recall Cancel in-flight requests
Form Data Parcel Multipart form data with file uploads
WebSocket EnvoySocket WebSocket client with SocketStatus lifecycle management
SSE EnvoySse Server-Sent Events client with event parsing
SSL Pinning EnvoyPin Certificate pinning with SHA-256 fingerprints
Proxy EnvoyProxy HTTP/SOCKS proxy configuration
Cache MemoryCache In-memory TTL-based cache with LRU eviction
Pillar Base EnvoyPillar Base class for HTTP-backed Pillars
DI Module EnvoyModule DI module for registering Envoy in Titan's Vault
Data Extensions EnvoyPillarExtension envoyQuarry and envoyCodex for route-based data loading

Installation

dependencies:
  titan: ^1.1.3
  titan_envoy: ^1.0.0

Quick Start

import 'package:titan_envoy/titan_envoy.dart';

// Create a client
final envoy = Envoy(baseUrl: 'https://api.example.com');

// Simple GET
final dispatch = await envoy.get('/users');
print(dispatch.data); // parsed JSON

// POST with body
final created = await envoy.post('/users', data: {'name': 'Kael'});

// PUT, PATCH, DELETE
await envoy.put('/users/1', data: {'name': 'Kael the Great'});
await envoy.patch('/users/1', data: {'title': 'Hero'});
await envoy.delete('/users/1');

Interceptors (Couriers)

Couriers form a pipeline that processes every request and response:

final envoy = Envoy(baseUrl: 'https://api.example.com');

// Add logging
envoy.addCourier(LogCourier());

// Add automatic retry (3 attempts with exponential backoff)
envoy.addCourier(RetryCourier(maxRetries: 3));

// Add auth token injection
envoy.addCourier(AuthCourier(
  tokenProvider: () async => getAccessToken(),
  refreshTokenProvider: () async => refreshToken(),
));

// Add response caching
envoy.addCourier(CacheCourier(
  cache: MemoryCache(maxEntries: 100),
  policy: CachePolicy(
    strategy: CacheStrategy.staleWhileRevalidate,
    ttl: Duration(minutes: 5),
  ),
));

// Deduplicate concurrent identical requests
envoy.addCourier(DedupCourier());

Custom Courier

class ApiKeyCourier extends Courier {
  final String apiKey;
  ApiKeyCourier(this.apiKey);

  @override
  Future<Missive> onRequest(Missive missive) async {
    return missive.copyWith(
      headers: {...missive.headers, 'X-Api-Key': apiKey},
    );
  }
}

Caching

Five built-in cache strategies:

// Network first, fall back to cache
CachePolicy(strategy: CacheStrategy.networkFirst, ttl: Duration(minutes: 10))

// Cache first, fetch if missing or expired
CachePolicy(strategy: CacheStrategy.cacheFirst, ttl: Duration(hours: 1))

// Return stale data immediately, revalidate in background
CachePolicy(strategy: CacheStrategy.staleWhileRevalidate, ttl: Duration(minutes: 5))

// Always network, never cache
CachePolicy(strategy: CacheStrategy.networkOnly)

// Always cache, never network
CachePolicy(strategy: CacheStrategy.cacheOnly)

Cancel Requests

final recall = Recall();

// Start a request with a cancel token
final future = envoy.get('/slow-endpoint', recall: recall);

// Cancel it
recall.cancel('User navigated away');

Request Throttling (Gate)

final envoy = Envoy(
  baseUrl: 'https://api.example.com',
  gate: Gate(maxConcurrent: 4), // max 4 concurrent requests
);

WebSocket

final socket = EnvoySocket(url: 'wss://api.example.com/ws');

// Listen for messages
socket.stream.listen((message) => print('Received: $message'));

// Send messages
socket.send('Hello, server!');
socket.sendJson({'type': 'ping'});

// Check status
print(socket.status); // SocketStatus.connected

// Close
await socket.close();

Server-Sent Events (SSE)

final sse = EnvoySse(url: 'https://api.example.com/events');

sse.stream.listen((event) {
  print('Event: ${event.event}');
  print('Data: ${event.data}');
  print('ID: ${event.id}');
});

await sse.close();

Multipart Form Data

final parcel = Parcel()
  ..addField('name', 'Kael')
  ..addFile(ParcelFile(
    field: 'avatar',
    filename: 'avatar.png',
    bytes: avatarBytes,
    contentType: 'image/png',
  ));

final dispatch = await envoy.post('/upload', data: parcel);

Security

// SSL certificate pinning
final envoy = Envoy(
  baseUrl: 'https://api.example.com',
  pin: EnvoyPin(fingerprints: [
    'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
  ]),
);

// Proxy configuration
final envoy = Envoy(
  baseUrl: 'https://api.example.com',
  proxy: EnvoyProxy(host: 'proxy.corp.com', port: 8080),
);

Titan Integration

EnvoyPillar

Base class for Pillars that need HTTP:

class UserPillar extends EnvoyPillar {
  UserPillar() : super(baseUrl: 'https://api.example.com');

  late final users = core<List<User>>([]);

  Future<void> loadUsers() async {
    final dispatch = await envoy.get('/users');
    users.set((dispatch.data as List).map(User.fromJson).toList());
  }
}

EnvoyModule

Register Envoy instances via Titan's DI:

class AppModule extends TitanModule {
  @override
  void register(TitanContainer container) {
    EnvoyModule.register(
      container,
      baseUrl: 'https://api.example.com',
      couriers: [LogCourier(), RetryCourier()],
    );
  }
}

Data Loading Extensions

Use envoyQuarry and envoyCodex for route-based data fetching:

class ProductPillar extends EnvoyPillar {
  ProductPillar() : super(baseUrl: 'https://api.example.com');

  // SWR data query
  late final product = envoyQuarry<Product>(
    path: '/products/1',
    fromJson: Product.fromJson,
  );

  // Paginated data
  late final products = envoyCodex<Product>(
    path: '/products',
    fromJson: Product.fromJson,
  );
}

Colossus Integration

Track API metrics with the MetricsCourier:

envoy.addCourier(MetricsCourier(
  onMetric: (metric) {
    // metric.latency, metric.statusCode, metric.url, metric.bytes
    Colossus.instance.trackApiMetric(metric.toJson());
  },
));

Error Handling

try {
  final dispatch = await envoy.get('/users');
} on EnvoyError catch (e) {
  switch (e.type) {
    case EnvoyErrorType.connectTimeout:
      print('Connection timed out');
    case EnvoyErrorType.receiveTimeout:
      print('Response timed out');
    case EnvoyErrorType.cancel:
      print('Request was cancelled');
    case EnvoyErrorType.response:
      print('Server error: ${e.response?.statusCode}');
    case EnvoyErrorType.other:
      print('Unknown error: ${e.message}');
    default:
      print('Error: ${e.message}');
  }
}

Ecosystem

Envoy is part of the Titan ecosystem:

Package Purpose
titan Core reactive engine — Pillar, Core, Derived, DI
titan_basalt Infrastructure — Trove, Moat, Portcullis, Saga
titan_bastion Flutter widgets — Vestige, Beacon, Spark
titan_atlas Routing — Atlas, Passage, Sentinel
titan_argus Auth — Argus, Garrison
titan_colossus Performance monitoring — Colossus, Pulse, Scry
titan_envoy HTTP client — Envoy, Courier, Gate

License

MIT — see LICENSE for details.

Libraries

titan_envoy
Envoy — Titan's HTTP Client & API Layer