Skip to main content

netguard 2.0.0 copy "netguard: ^2.0.0" to clipboard
netguard: ^2.0.0 copied to clipboard

A Dio-based HTTP client for Flutter with caching, connectivity handling, auth token refresh, retries and certificate pinning.

NetGuard ๐Ÿ›ก๏ธ #

A Dio-based HTTP client for Flutter with caching, connectivity handling, authentication token refresh, retries and certificate pinning.

pub package license

NetGuard wraps Dio rather than replacing it. Every Dio type you already use โ€” Response, DioException, Interceptor, CancelToken, FormData โ€” works unchanged, and netGuard.dio gives you the underlying instance whenever you need it.

โœจ Features #

  • HTTP cache with policies โ€” cacheFirst, networkFirst, staleWhileRevalidate, cacheOnly, plus ETag/304 revalidation
  • Connectivity handling โ€” fail fast, wait for the connection, or persist the request and replay it after a restart
  • Authentication โ€” automatic token refresh, both reactive (on 401) and proactive (before the JWT expires), with one refresh shared across concurrent requests
  • Retries โ€” exponential backoff with jitter, Retry-After support, idempotent methods only by default
  • Request deduplication โ€” identical in-flight GETs collapse into one call
  • Certificate pinning โ€” reject certificates you did not authorise
  • Redacted logging โ€” bearer tokens, JWTs and sensitive headers are masked before anything reaches your log sink
  • Observability โ€” per-request metrics and a rolling cache-hit-rate snapshot
  • Cross-platform โ€” iOS, Android, web (JS and wasm), macOS, Windows, Linux

๐Ÿ“ฆ Installation #

dependencies:
  netguard: ^2.0.0
import 'package:netguard/netguard.dart';

Types that require dart:io (such as IOHttpClientAdapter) live in a separate entry point, so the main library stays usable on the web:

import 'package:netguard/io.dart';

๐Ÿš€ Quick start #

final api = NetGuard.withOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: const Duration(seconds: 10),
  logLevel: NetGuardLogLevel.info,
);

try {
  final response = await api.get<Map<String, dynamic>>('/products');
  print(response.data);
} on DioException catch (e) {
  print(e.apiMessage);
}

NetGuard also offers a shared instance:

await NetGuard.configure(baseUrl: 'https://api.example.com');
final response = await NetGuard.instance.get<dynamic>('/products');

๐Ÿ“š Requests #

All the usual verbs are available, and each takes the same set of NetGuard options:

await api.get<dynamic>('/users');
await api.post<dynamic>('/users', data: {'name': 'Ada'});
await api.put<dynamic>('/users/1', data: {'name': 'Ada L.'});
await api.patch<dynamic>('/users/1', data: {'active': true});
await api.delete<dynamic>('/users/1');
await api.head<dynamic>('/users');

Typed responses #

class User {
  User(this.id, this.name);
  final int id;
  final String name;

  static User fromJson(dynamic json) =>
      User(json['id'] as int, json['name'] as String);
}

final user = await api.getAs<User>('/me', decoder: User.fromJson);

A body that does not match raises a DioException naming the endpoint and the expected type, rather than an opaque cast error.

โš ๏ธ Error handling #

NetGuard follows Dio: a non-2xx response throws a DioException.

try {
  await api.get<dynamic>('/protected');
} on DioException catch (e) {
  if (e.isNetworkError) {
    // No connection.
  } else if (e.statusCode == 422) {
    print(e.apiMessage);       // Pulled from message/error/detail/title.
    print(e.apiError);         // The decoded JSON body.
  }
  print(e.userFriendlyMessage);
}

Migrating from 1.x: NetGuard 1.x forced validateStatus: (s) => s < 600 on every request, so 4xx and 5xx responses resolved instead of throwing and try/catch never fired. If your code inspects response.statusCode rather than catching, set api.options.resolveErrorStatuses = true to keep the old behaviour while you migrate.

๐Ÿ’พ Caching #

Caching is opt-in per request:

// Serve the cached copy instantly, refresh in the background.
await api.get<dynamic>('/feed', cachePolicy: CachePolicy.staleWhileRevalidate);

// Prefer the network, fall back to cache when it fails or you are offline.
await api.get<dynamic>('/feed', cachePolicy: CachePolicy.networkFirst);

// Use the cache when fresh, otherwise fetch.
await api.get<dynamic>(
  '/config',
  cachePolicy: CachePolicy.cacheFirst,
  cacheDuration: const Duration(hours: 1),
);
Policy Behaviour
networkOnly Never touches the cache. The default.
cacheFirst Fresh cache wins; otherwise fetch and store.
networkFirst Fetch; fall back to a stale copy on failure.
staleWhileRevalidate Return cache immediately, refresh in the background.
cacheOnly Cache or CacheMissException. Never fetches.

NetGuard sends If-None-Match / If-Modified-Since when it holds a validator, and a 304 refreshes the entry without re-downloading the body. A server Cache-Control: max-age overrides cacheDuration.

Invalidation #

await api.cache.invalidateWhere('/orders');   // Everything under /orders.
await api.cache.clearNamespace();             // This client's entries.
print(api.cache.getStats());

Cache keys include the base URL, method and sorted query parameters, so two clients pointed at different hosts never collide and ?a=1&b=2 matches ?b=2&a=1.

๐ŸŒ Connectivity #

api.options.handleNetwork = true;
api.options.offlineBehavior = OfflineBehavior.fail;
Behaviour Effect
fail Throw NetworkOfflineException immediately. The default.
wait Block until the connection returns, bounded by offlineTimeout.
queue Persist the request, replay it when back online.

queue is for fire-and-forget mutations. The caller still receives a NetworkOfflineException right away; the eventual response arrives through onQueueReplay, because there is nothing honest to return synchronously:

api.options
  ..offlineBehavior = OfflineBehavior.queue
  ..onQueueReplay = (result) {
    if (result.isSuccess) print('Replayed ${result.request.path}');
  };

Queued requests survive an app restart, carry an Idempotency-Key, and drop their credentials so a replay picks up a current token rather than a stale one.

Status #

api.statusStream.listen((status) {
  if (status == NetworkStatus.offline) showOfflineBanner();
});

print(api.isOnline);
print(api.networkInfo);

By default NetGuard trusts connectivity_plus. To also verify reachability โ€” catching captive portals and data-less connections โ€” point a probe at your own endpoint:

api.configureConnectivityProbe(
  endpoints: ['https://api.example.com/health'],
);

Do not point this at a third party: it leaks usage data, costs battery, and fails CORS on the web.

๐Ÿ” Authentication #

api.configureAuth(
  callbacks: AdvanceAuthCallbacks(
    initialToken: storedAccessToken,
    initialRefreshToken: storedRefreshToken,
    onRefreshToken: () async {
      final response = await api.post<Map<String, dynamic>>(
        '/auth/refresh',
        data: {'refresh_token': storedRefreshToken},
        isLogin: true, // Never trigger a refresh for the refresh call itself.
      );
      return response.data?['access_token'] as String?;
    },
    onTokenRefreshed: (token) => secureStorage.write('access_token', token),
    onLogout: () async {
      await secureStorage.deleteAll();
      navigator.pushReplacementNamed('/login');
    },
  ),
  config: const AuthConfig(
    proactiveRefresh: true,
    refreshLeeway: Duration(seconds: 30),
    maxRetryAttempts: 2,
  ),
);

With proactiveRefresh on, NetGuard reads the JWT exp claim and refreshes before sending, so you do not spend a request discovering the token is stale. Opaque tokens fall back to reactive refresh on 401.

Concurrent 401s share a single refresh: the first request performs it, the rest await the same future and then retry. Replays traverse this client's own interceptor chain, so your own interceptors still run.

Mark your sign-in call with isLogin: true so a 401 from bad credentials does not trigger a refresh loop.

๐Ÿ” Retries #

api.options.retryPolicy = const RetryPolicy(
  maxAttempts: 3,
  initialDelay: Duration(milliseconds: 300),
  backoffFactor: 2,
  jitter: 0.25,
);

// Or per request.
await api.get<dynamic>('/flaky', retryPolicy: const RetryPolicy(maxAttempts: 5));

Retries apply to transport failures and to 408/425/429/500/502/503/504. A Retry-After header is honoured. Only idempotent methods retry by default; set retryNonIdempotent: true if your POST endpoints are safe to repeat.

๐Ÿ”’ Certificate pinning #

api.pinCertificates(
  CertificatePinning(
    fingerprints: {
      'a1b2c3โ€ฆ',  // Current certificate.
      'd4e5f6โ€ฆ',  // Backup, for rotation.
    },
    allowedHosts: {'api.example.com'},
  ),
);

Obtain a fingerprint with:

openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null \
  | openssl x509 -outform DER | openssl dgst -sha256

Operational warning: a rotated certificate without a matching app release bricks every installed client. Always pin a backup, and ship well before expiry. If you cannot guarantee that, do not pin. Pinning is a no-op on the web, where the browser owns TLS validation.

๐Ÿ“ Logging #

api.enableLogging(
  level: NetGuardLogLevel.info,
  emitCurl: true,
);

Authorization, Cookie, X-Api-Key and friends are masked, and bearer tokens or JWTs appearing anywhere in a body are masked too โ€” so a log you paste into an issue tracker does not leak a session.

Route logs into your own logging package with a custom sink:

NetGuardLogger.global
  ..level = NetGuardLogLevel.info
  ..sink = (record) => myLogger.log(record.level.name, record.message);

๐Ÿ“Š Metrics #

api.options.onMetrics = (m) {
  analytics.timing(m.path, m.duration, source: m.source.name);
};

print(api.metrics);  // requests: 42, ok: 40, cacheHitRate: 31.0%, avg: 128ms

๐Ÿงน Cancellation #

await api.get<dynamic>('/widgets', cancelGroup: 'dashboard');
await api.get<dynamic>('/alerts', cancelGroup: 'dashboard');

@override
void dispose() {
  api.cancelGroup('dashboard');
  super.dispose();
}

โฑ๏ธ Rate limiting #

final api = NetGuard.withOptions(
  baseUrl: 'https://api.example.com',
  maxConcurrentRequests: 4,
  minRequestInterval: const Duration(milliseconds: 50),
);

Identical in-flight GETs are also deduplicated, so two widgets requesting the same endpoint on the same frame produce one network call. Disable with deduplicateRequests: false.

๐Ÿ”ค Body encoding #

api.options.bodyEncoder = (body) => myCipher.encrypt(jsonEncode(body));
await api.post<dynamic>('/secure', data: payload, encodeBody: true);

The default encoder is base64(jsonEncode(body)). That is encoding, not encryption โ€” trivially reversible, and it provides no confidentiality. It exists for APIs that expect a base64 envelope. TLS already protects the payload in transit; if you need application-layer confidentiality, supply a function performing authenticated encryption with a key the client does not ship.

๐Ÿงช Testing #

Swap the adapter to drive requests without a network:

final api = NetGuard.withOptions(baseUrl: 'https://mock.test');
api.httpClientAdapter = myMockAdapter;

See test/support/mock_adapter.dart for a small scripted adapter used by this package's own suite.

๐Ÿ“– Migrating from 1.x #

Deprecated APIs #

These still work and are removed in 3.0.0. Your IDE names the replacement on each one.

Deprecated Use instead
options.encryptionFunction options.bodyEncoder
encryptBody: true encodeBody: true
useCache: true cachePolicy: CachePolicy.staleWhileRevalidate
options.throwOnOffline = true options.offlineBehavior = OfflineBehavior.fail
options.throwOnOffline = false options.offlineBehavior = OfflineBehavior.wait
options.autoRetryOnNetworkRestore = true options.offlineBehavior = OfflineBehavior.queue
options.maxNetworkRetries = n options.retryPolicy = RetryPolicy(maxAttempts: n)
options.networkRetryDelay = d options.retryPolicy = RetryPolicy(initialDelay: d)
options.showLogs = true options.logLevel = NetGuardLogLevel.verbose
options.showLogs = false options.logLevel = NetGuardLogLevel.error

Renamed, with no alias #

1.x 2.0
response.isRedirect response.isRedirection
IOHttpClientAdapter from the main import import 'package:netguard/io.dart'

Behavioural changes #

Was Now
4xx/5xx resolved silently Throws; opt out with options.resolveErrorStatuses = true
Offline returned a fake 503 body Throws NetworkOfflineException
Auth replays skipped your interceptors Replays traverse the client's own chain

See CHANGELOG.md for the 1.x bugs this release fixes.

๐Ÿ“˜ Full API reference #

DOCUMENTATION.md documents every public type and member โ€” the request pipeline, all 47 public types, RequestOptions.extra keys, and the concurrency guarantees of the auth refresh.

๐Ÿค Contributing #

See CONTRIBUTING.md. Run flutter analyze and flutter test before opening a pull request.

๐Ÿ“„ License #

MIT โ€” see LICENSE.

2
likes
160
points
124
downloads

Documentation

API reference

Publisher

verified publisherniteshmandal1.com.np

Weekly Downloads

A Dio-based HTTP client for Flutter with caching, connectivity handling, auth token refresh, retries and certificate pinning.

Repository (GitHub)
View/report issues
Contributing

Topics

#http #networking #dio #cache #authentication

License

MIT (license)

Dependencies

connectivity_plus, crypto, dio, flutter, hive, path_provider

More

Packages that depend on netguard