NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
A smart Flutter network package with built-in error handling, configurable retry, optional SSL certificate pinning (Android/iOS), and multilingual error messages (en/ar).
Last release 1 months ago
19 Aug 2026
Ships fairly regularly
a new release about every 3 months
Most releases are documented
notes for 12 of 14 stable releases
Nothing withdrawn
no release was ever pulled
7 months old
14 releases · first in 2026
One column per month.
`CertificatePinningConfig` now accepts a single fingerprint. The floor drops from two distinct pins to one (kMinimumFingerprints is now 1); an empty l
CertificatePinningConfig now accepts a single fingerprint. The floor
drops from two distinct pins to one (kMinimumFingerprints is now 1);
an empty list still throws. Environments with no successor certificate to
name — a self-signed staging host, or one rotated in lockstep with the app —
can now pin without inventing a second value.allowedSHAFingerprints stores
it once, de-duplicated in the order given.Existing two-pin configurations are unaffected; this only widens what constructs.
The renewal caveat is unchanged and now rests on you rather than the constructor: a whole-certificate pin stops matching the day the server renews, so a single-pin production app goes dark on renewal day with no fix short of a store release. Pin the current certificate and its successor for anything facing users.
Certificate pinning is now delegated to `http_certificate_pinning` instead of the package's own SPKI implementation. Everything outside pinning is unc
Certificate pinning is now delegated to
http_certificate_pinning
instead of the package's own SPKI implementation. Everything outside pinning is
unchanged; apps that do not set certificatePinning need no code changes beyond
the SDK bump.
sha256/<base64> values from 1.x are rejected at construction.CertificatePinningConfig is reshaped: pins (a per-host map) becomes
allowedSHAFingerprints (a flat list applied to every request), and a new
timeout replaces nothing.CertificatePinningConfig.fromAssets(), enforce,
includeSubdomains, onPinFailure, the OnPinFailureCallback typedef, and
the kMinimumPinsPerHost constant (now kMinimumFingerprints).NetworkConfig at any host that must not be pinned.^3.5.4 / Flutter >=3.3.0, as required by
http_certificate_pinning.asn1lib and crypto dropped; http_certificate_pinning
added.Replace each SPKI pin with the SHA-256 fingerprint of the certificate itself:
openssl s_client -connect api.example.com:443 -servername api.example.com \
< /dev/null 2>/dev/null \
| openssl x509 -fingerprint -sha256 -noout
// 1.x
CertificatePinningConfig(
pins: {
'api.example.com': ['sha256/<current>', 'sha256/<backup>'],
},
includeSubdomains: true,
)
// 2.0.0
CertificatePinningConfig(
allowedSHAFingerprints: [
'AA:BB:CC:...', // certificate in production today
'CC:DD:EE:...', // successor certificate, already issued
],
)
⚠️ Operational change, not just an API one. A whole-certificate fingerprint stops matching the day the certificate is renewed, even when the key pair is reused — the 1.x SPKI pin survived renewal, this one does not. Both fingerprints must therefore be certificates that already exist and whose renewal you control, and the successor's fingerprint has to ship before the current certificate expires. Plan certificate renewal as a release-coordinated event.
For a staged rollout, previously enforce: false, gate the whole
certificatePinning field on a flag instead and watch for
CertificatePinningException — see "Shielded and unshielded builds" in the
README.
CertificatePinningException — same type, same host field, still extends
ApiException, still locale-aware and never carrying the presented
fingerprint.allowBadCertificate combined with certificatePinning still throws
ArgumentError at startup.`CertificatePinningConfig.fromAssets()` — configure pinning from certificates bundled as Flutter assets instead of hand-pasted hashes:
CertificatePinningConfig.fromAssets() — configure pinning from
certificates bundled as Flutter assets instead of hand-pasted hashes:
final pinning = await CertificatePinningConfig.fromAssets(
certificatePaths: {
'api.example.com': [
'assets/certs/api.example.com.pem',
'assets/certs/backup.pub.pem',
],
},
);
Every asset is read and parsed up front, so a wrong path or an unreadable
certificate fails during initialize() rather than on the first request in
production.
Bare public keys are accepted. An asset may be either a CERTIFICATE
block (the ASN.1 is walked to locate SubjectPublicKeyInfo) or a
PUBLIC KEY block (already a SubjectPublicKeyInfo, hashed directly). The
second form is the point: the mandatory backup pin can be derived from a key
pair that has no certificate yet, so pinning ships without waiting on a CA.
Both forms yield identical pins for the same key pair.
bundle parameter — defaults to rootBundle; inject an AssetBundle
in tests.
certificatePinning defaults to null
and the package behaves exactly as it does without it.fromAssets delegates to the unnamed constructor, so all existing validation
still applies — including the two-distinct-pins rule. A certificate and a
public key extracted from that same certificate are the same key, and
therefore one pin, not two.ArgumentError naming the offending path: a missing
or unreadable asset, a non-PEM file, a label other than CERTIFICATE or
PUBLIC KEY, multiple PEM blocks, a body that is not valid base64, or a
certificate that cannot be parsed.Certificate pinning was removed in this release. It is restored in 1.4.0 — prefer upgrading straight to 1.4.0 over pinning to this version.
Nothing published for this version
Nothing published for this version
Certificate pinning — NetworkConfig accepts an optional certificatePinning: CertificatePinningConfig(...). Pins are the base64 SHA-256 of a certificat
NetworkConfig accepts an optional
certificatePinning: CertificatePinningConfig(...). Pins are the base64
SHA-256 of a certificate's SubjectPublicKeyInfo in the conventional
sha256/<base64> form shared with HPKP and OkHttp's CertificatePinner.
Hashing the SPKI rather than the whole certificate means a pin survives
certificate renewal whenever the key pair is reused.pins maps a host to its accepted pins;
includeSubdomains extends a host's pins to its subdomains; enforce: false
reports mismatches without blocking them, for a staged rollout.onPinFailure callback — reports the host and the pins the server
actually presented, for telemetry.CertificatePinningException — a pin failure is distinguishable from a
generic network error. It extends ApiException, so existing handlers keep
working, and carries the offending host. Messages are locale-aware in
English and Arabic; the presented pins are never placed in the message.CertificatePinningConfig,
CertificatePinningException, OnPinFailureCallback,
kMinimumPinsPerHost.DioExceptionType.badCertificate
was previously treated as a retryable transport error, so a rejected TLS
handshake was replayed up to the configured attempt count. Replaying it
re-presents the same certificate to the same host for the same verdict, and
multiplied a security signal that should be raised once. This applies to all
certificate failures, pinned or not.certificatePinning defaults to null and the
package behaves exactly as it did in 1.2.0 when it is not set. const NetworkConfig(...) call sites continue to compile.pins are pinned. Unlisted hosts — analytics, crash
reporting, CDNs — fall through to normal TLS validation rather than failing
closed.asn1lib (certificate parsing) and crypto (SHA-256).ArgumentError at startup. Keep the second key offline.CertificatePinningConfig is constructed, so a typo fails at startup rather
than on the first API call in production.allowBadCertificate: true combined with certificatePinning throws
ArgumentError when the client is built. The check lives there rather than in
NetworkConfig's const constructor because a const constructor can only
assert, and asserts are stripped from release builds — exactly where an app
that looks pinned but is not would do the damage.validateCertificate, which evaluates the leaf certificate
on every connection, rather than badCertificateCallback, which fires only
after chain validation has already failed.Configurable retry — NetworkConfig now accepts a retry policy. RetryPolicy exposes attempts, delays, methods and statuses, all of which were previousl
NetworkConfig now accepts a retry policy.
RetryPolicy exposes attempts, delays, methods and statuses, all of
which were previously hardcoded. Pass retry: null to disable retry app-wide.request(), download() and uploadFile() take a
retry: argument that overrides the app-wide policy for a single call. Use
RetryPolicy.off to opt one request out, or a policy with a higher
attempts to opt one in.RetryPolicy.retry behaves exactly
as it did in 1.1.0.RetryPolicy.delays and RetryPolicy.methods are read from the app-wide
policy only. Backoff is fixed when the client is built and cannot vary per
request; the method allowlist guards calls that did not supply a policy, so a
policy attached to a request bypasses it — that is what makes
retry: RetryPolicy(attempts: 5) retry a POST.attempts is capped at RetryPolicy.maxAttempts (10), asserted at
construction.`ErrorHandler` — DioExceptionType.transformTimeout now returns a proper localized message (TransformTimeout) with status 408, instead of leaking the r
ErrorHandler — DioExceptionType.transformTimeout now returns a proper
localized message (TransformTimeout) with status 408, instead of leaking
the raw 'transformTimeout' key as the user-facing message.equatable dependency removed from pubspec.yaml. It was left behind
after the Failure classes were deleted in 1.0.3 and is no longer used.ConnectionError locale key (both en and ar);
connectionError maps to NoInternetConnection, so the key was dead.`ErrorHandler` now maps DioExceptionType.connectionError to the NoInternetConnection error type, so connection failures surface the correct localized
ErrorHandler now maps DioExceptionType.connectionError to the
NoInternetConnection error type, so connection failures surface the correct
localized "no internet connection" message instead of a generic error.dart pub upgrade).`ServerFailure` and `CacheFailure` removed — failures.dart and its public exports have been deleted. The Failure abstraction was out of scope for a ne
ServerFailure and CacheFailure removed — failures.dart and its
public exports have been deleted. The Failure abstraction was out of scope
for a network package; it leaked domain-layer concerns into the library and
forced an unnecessary equatable dependency on consumers.
Migration: catch ApiException directly in your repository layer, or
define your own Failure types and map from ApiException there.
// before
} on ApiException catch (e) {
return Left(ServerFailure.fromException(e));
}
// after – option A: catch ApiException directly
} on ApiException catch (e) {
return Left(MyServerFailure(e.message, e.statusCode));
}
equatable dependency removed — the package no longer depends on
equatable. Remove it from your own pubspec.yaml if you were relying on
the transitive export.
`ErrorHandler.handleError` now passes an ApiException through unchanged instead of re-wrapping it as a generic 'UnexpectedError' (status 0). This prev
ErrorHandler.handleError now passes an ApiException through unchanged
instead of re-wrapping it as a generic 'UnexpectedError' (status 0). This
prevented the real error type, status code, and message from reaching callers
whenever an ApiException entered the catch block (e.g. thrown by a custom
interceptor).RequestService.execute — ensureConnected(), withMobileTimeouts(),
and resolveUrl() are now inside the single try/catch block, so every
error type (connectivity, timeout, bad response, certificate, cancel) flows
through ErrorHandler via one consistent code path.`ApiService.instance` now throws a StateError instead of silently creating a broken client when initialize() has not been called yet.
ApiService.instance now throws a StateError instead of silently
creating a broken client when initialize() has not been called yet.ApiService.isInitialized getter added — use it to safely check
whether initialize() has been called before accessing instance.withMobileTimeouts no longer discards user-supplied Options fields.
It now mutates the existing object in-place, only setting receiveTimeout
when the caller has not already provided one.removeAppLocale() now restores the locale that was active at
initialize() time (from NetworkConfig.defaultHeaders['Accept-Language'])
instead of always falling back to 'en'.NetworkLocale.clearCustomTranslations([locale]) added — removes custom
translations for a specific locale, or for all locales when called without
an argument.ApiService singleton with initialize(NetworkConfig) entry point
Initial stable release.
ApiService singleton with initialize(NetworkConfig) entry pointNetworkConfig — configure base URL, timeouts, default headers, SSL, and unauthorized callbackHttpMethod enum — get, post, put, patch, deleteRequestService — unified HTTP request execution with connectivity guard and mobile timeout extensionDownloadRequestService — file download with progress callbackUploadRequestService — multipart file upload with extra fields and progress callbackNetworkLocale — locale-aware error messages; built-in English and Arabic translations covering all standard HTTP status codes and network error types; extensible via addTranslations()setAppLocale(locale) — sets Accept-Language header and switches error-message locale in one callErrorHandler — converts DioException to ApiException with translated messagesApiException — rich exception with statusCode, apiErrorCode, errorCategory, hasApiErrorCode(), and getResponseField()ServerFailure / CacheFailure — domain-layer Failure wrappers (Equatable)compute isolatePrettyDioLogger (debug builds only)baseUrl override, cancel token, query parameters, and send/receive progress callbacksexample/ app demonstrating all features against JSONPlaceholder APIYour coding agent can read these notes before it upgrades. Set up the MCP server →