apix 4.0.0
apix: ^4.0.0 copied to clipboard
Production-ready Flutter/Dart API client with auth refresh queue, exponential retry, smart caching and error tracking (Sentry-ready). Powered by Dio.
4.0.0 #
This release answers an integration report from Ma Pension, filed after moving a wallet app onto 3.0.0. Eight gaps, none of which broke a build — three of them were only findable by reading apix's source, which is why they survived the last release.
The theme is the same throughout: apix knew something the consumer could not
reach. An error code it read past, a Retry-After it parsed and dropped, a
duration it measured and never reported.
Migration at a glance #
| If you… | Then… |
|---|---|
catch on ClientException for a 429 |
still works — TooManyRequestsException is a subtype. Read retryAfter to say how long to wait |
| assert an exact retry delay in a test | add jitter: 0 — delays are now spread ±20 % by default |
rely on CacheStrategy.networkOnly writing to your store |
it no longer writes. If you were counting on that, use networkFirst |
wrote a no-op CacheStorage just to get deduplication |
delete it — pass deduplicationConfig and no cacheConfig |
group Sentry issues by the type passed to onError |
the onResponse path now sends HttpTrackingException as an HttpException. Existing issues may regroup |
import 'package:dio/dio.dart' for ResponseType, Interceptor, FormData… |
import package:apix/apix.dart instead; adapters live in package:apix/testing.dart |
subclass ApiException with your own code field |
remove it and pass super.code — a same-typed field now shadows the inherited one, and a differently-typed one (int code) will not compile |
| use none of the above | nothing to do |
⚠️ Breaking behavior #
-
Retry delays are no longer deterministic.
RetryConfig.jitterdefaults to0.2, spreading each computed backoff across ±20 % of itself.- Opt-in was the safer-looking choice and the wrong one: a thundering herd harms whoever did not read this changelog. After an outage, every client that failed in the same second used to retry at the same instants.
jitter: 0.0restores the previous sequence exactly. Any test asserting a precise delay needs it.- A server-named
Retry-Afteris never jittered.
-
CacheStrategy.networkOnlyno longer writes to the store. It always documented "never read cache"; only the reading half was enforced, so every response was still written. On a wallet that moved balances and transactions through a store nobody ever read from.- The guard was missing from two write sites —
onResponseand the deduplicated path — and a guard on the first alone still leaked on exactly the path a consumer enabling deduplication would take.
- The guard was missing from two write sites —
-
ApiExceptiongained acodefield, which collides with any subclass that already declared one. Same type (String): the subclass shadows the inherited field and the analyzer warns. Different type (int code): it no longer compiles. Drop the field and forwardsuper.codeinstead — this was found by rebuilding apix's own example app against 4.0.0, not by reading. -
HttpTrackingExceptionnow extendsHttpException. The type handed toErrorTrackingConfig.onErrorchanges on theonResponsepath, so trackers that group by runtime type may regroup existing issues.
Added #
-
ApiException.code— the application error code, read from the response body undererrorCodeKey(default'code', configurable likedataKey). Branch on a stable business code instead of an HTTP status that drifts from400to409to422across server revisions. Always aString, even when the server sends a number; null on failures that have no body. -
TooManyRequestsException— a 429 with its parsedretryAfter, so the user can be told how long to wait instead of receiving the same generic message as for a malformed request. -
DeduplicationConfig/DeduplicationInterceptor— deduplication without a cache.RequestDeduplicatorwas only ever instantiated byCacheInterceptor, so getting one meant installing the other. When both are configured, the cache's own deduplication is switched off rather than collapsing each request twice. -
EncryptedCacheStorage— a decorator that seals body and headers before they reach anyCacheStorage. You supplyencrypt/decrypt, so apix takes on neither a crypto dependency nor your key. Cache keys stay in clear text — the invalidation API reads them — so keep identifiers out of URLs you cache. An entry that cannot be decrypted reads as a miss and is purged. -
TracingInterceptor/TracingConfig— one performance span per request, as a child of the current Sentry transaction. apix already measured duration, size and status; nothing ever opened a span, so durations could only land in a breadcrumb: visible after an incident, never aggregated.- One span covers the whole logical request, retries included.
- A response served from cache opens none — it spent no time on the network.
-
RetryInterceptor.onRetry— fires before each retry waits, carrying the attempt number, the delay and the cause. A retry storm used to be invisible: only the final failure surfaced. -
package:apix/testing.dart—HttpClientAdapter,ResponseBodyand friends, so stubbing an adapter no longer forces a direct dio import (and with it, apix's dio version range) onto your test suite.
Changed #
-
The dio barrel now also re-exports
ResponseType,RequestOptions,Interceptorand its handlers,DioException,DioExceptionType,FormData,MultipartFileandHeaders— derived from where apix's own API hands a dio type to a consumer.create(interceptors:)took aList<Interceptor>that could not be written without importing dio; binary downloads had no way to nameResponseType. -
Retry-Afterparsing moved to a shared helper used by both the retry interceptor and the error mapper, so what the caller is told to wait and what the interceptor actually waits cannot drift apart. -
The
onResponseerror-tracking path now attaches a stack trace. It had none, while theonErrorpath always did.
3.0.0 #
This release is about the cache. Two of its promises were not kept: cacheFirst
did not refresh in the background despite saying so, and the TTL was only as
strong as whichever CacheStorage you plugged in. Both are now true. Error
tracking also stops flattening every failure into one DioException.
Migration at a glance #
| If you… | Then… |
|---|---|
use CacheStrategy.cacheFirst |
responses may now be older than defaultTtl. Check response.isStale, or move to networkFirst where freshness matters |
call CacheRequestExtension.isFromCache(response) |
replace with response.isFromCache |
implement CacheStorage yourself |
delete the expiry filter from get — keep it in has |
inspect the argument of ErrorTrackingConfig.onError |
it is the typed ApiException now, not a DioException. e is DioException stops matching silently — use (e as ApiException).originalError |
catch on ApiException around a cacheOnly call |
you will now actually catch CacheException; before, it slipped through |
catch on HttpException for 4xx/5xx |
still works — ClientException / ServerException are subtypes |
use SentrySetup with filterNetworkNoise |
your 5xx start reaching Sentry. Expect more events, not fewer — they were being dropped |
use nothing but networkFirst (the default) |
nothing to do |
⚠️ Breaking behavior #
CacheStrategy.cacheFirstnow serves stale data — it does what it always documented: serve the cache immediately, refresh behind (stale-while-revalidate). An expired entry is returned, flaggedisStale, while one background request renews it.- Network volume is unchanged: a fresh entry costs nothing, a stale one costs exactly one request — asserted by a test, not assumed. Only the waiting disappears.
- The risk to check: a response may now be older than
defaultTtl. Where that is unacceptable — an amount, a balance, an authorisation — usenetworkFirst, or surfaceresponse.isStale. - Other strategies are unaffected.
Breaking #
-
ErrorTrackingConfig.onErrorreceives the typedApiException, not the rawDioException. Trackers group by runtime type, so a 500, a 404 and a timeout used to land in a single issue titledDioException. Nothing fails to compile;e is DioExceptionjust stops matching — reach the original through(e as ApiException).originalError. -
CacheRequestExtension.isFromCache(response)removed — useresponse.isFromCache. The old form was a static on an extension ofRequestOptionstaking aResponse, so autocomplete never surfaced it. -
CacheStorage.getmust no longer filter on expiry — only affects custom implementations. It returns entries expired or not;nullmeans absent. The interceptor owns the TTL, so a backend can no longer weaken it by forgetting to filter.haskeeps filtering.
Added #
-
response.isFromCache/response.isStale—isStaleis true wherever apix knowingly returns expired data:cacheFirstrevalidating, and the offline fallback ofnetworkFirst/httpCacheAware. On an amount, a balance or a status, surface it. A304is not stale. The underlying keys are exported asfromCacheKey/fromCacheStaleKey. -
FileCacheStorage— a cache that survives restarts, with no new dependency: one JSON file per entry, in a directory you supply.- Bounded by default (
maxEntries: 200) — a process cache dies with the app, a disk cache does not. Expired entries are evicted first.maxEntries: nullopts out. - Reads never throw (a corrupt file is a miss, and is discarded); writes are atomic; only its own files are touched.
- ⚠️ Entries are stored in clear text — never point it at credentials, tokens, personal data or amounts.
- Bounded by default (
Fixed #
-
ClientExceptionandServerExceptionwere never thrown —ErrorMapperspecialised only 401/403/404 and mapped everything else to a bareHttpException, leaving both clauses dead at every call site despite the documented hierarchy. Unspecialised 4xx now map toClientException, 5xx toServerException; other statuses stayHttpException. Not breaking — both areHttpExceptionsubtypes. -
ApiX errors were discarded by ApiX's own Sentry filter —
SentryException.typeis a bare class name, so the noise filter could not tell apix'sHttpExceptionfromdart:io's and dropped every 5xx. Classification is now by type hierarchy. Name matching is unchanged for everything else. -
CacheExceptionwas not anApiException— acacheOnlymiss escaped the typed-error contract entirely, becauseRequestInterceptorHandler.rejectskips the following error interceptors. -
cacheOnlyserved expired entries — it now rejects them, distinguishing a miss from an expiry. -
Cache eviction ignored expiry —
InMemoryCacheStoragecould drop a fresh entry while keeping a stale one.
2.3.0 #
Changed #
- ⚠️ BREAKING BEHAVIOR — Retry is now HTTP-method-aware;
POSTandPATCHare no longer retried by default- Automatic retry previously replayed any request whose status code matched
retryStatusCodes, ignoring the HTTP method. A5xxreturned after the server had already committed (e.g. a gateway502/504timeout following a payment) would replay a non-idempotent request and produce a duplicate (double charge / double top-up). RetryInterceptornow retries only requests whose method is in the newRetryConfig.retryableMethods, which defaults to the idempotent methods per RFC 7231 §4.2.2:{GET, HEAD, OPTIONS, TRACE, PUT, DELETE}. Method matching is case-insensitive.- Migration — if you relied on
POST/PATCHbeing retried, either widen the set globally withRetryConfig(retryableMethods: {...'POST'}), or opt in per request (recommended) withRequestOptions.forceRetry()— see below. - Unchanged: no retry on a no-response network error (
statusCode == null),Retry-Afterhandling, and the per-requestdisableRetry()opt-out (still takes precedence).
- Automatic retry previously replayed any request whose status code matched
Added #
-
RetryConfig.retryableMethods—Set<String>of upper-case HTTP methods eligible for retry (default = idempotent methods)- Fully configurable: remove a method a backend mishandles, or add one you know is safe.
-
RequestOptions.forceRetry()— per-request opt-in to retry a non-idempotent method- Overrides the method guard only — for a request that is provably safe to replay, typically a
POST/PATCHprotected by anIdempotency-Key. - Never overrides the no-response network guard, the status-code guard, or
maxAttempts;disableRetry()still wins if both are set. - Symmetric counterpart of
disableRetry(); backed by the exportedforceRetryKeyextra.
- Overrides the method guard only — for a request that is provably safe to replay, typically a
-
Dio
Options,CancelTokenandResponsere-exported from theapixbarrel — no more directpackage:dioimport for common calls (Options(extra: {noRetryKey: true}),cancelToken:, typing a returnedResponse<T>, ...)
Fixed #
- dio 5.10.0 compatibility across the declared
>=5.4.0 <7.0.0range — dio 5.10.0 introduced theDioExceptionType.transformTimeoutenum value (breaking the exhaustive exception-mapping switches) and an optional parameter onErrorInterceptorHandler.reject. Exception mapping now routestransformTimeout— and any futureDioExceptionType— through its default branch (ErrorMapperInterceptormaps it to a genericApiException;AuthInterceptortreats it as a non-network failure), so apix builds on both the floor and the latest of its declared dio range.
2.2.0 #
Added #
SentrySetupOptions.configureOptions— Escape hatch forSentryFlutterOptionsnot exposed by apix- Callback
void Function(SentryFlutterOptions)invoked last duringSentryFlutter.init, after every apix default - Lets consumers enable Sentry options introduced in newer SDK versions without waiting for an apix release (e.g.
enableTombstoneinsentry_flutter9.14+,enableAppHangTrackingV2, replay tuning) - Can override anything, including
beforeSend/beforeSendTransaction— for composition that preserves apix's network-noise filter, prefercustomBeforeSend/customBeforeSendTransaction - Exceptions thrown in the callback are swallowed in release builds and rethrown in debug, to avoid breaking app startup on a typo
- Callback
2.1.0 #
Fixed #
-
*AndDecode/*AndParse— Parsing failures now surface as typedApiException(critical)- New
ParsingException(extendsApiException) thrown whenfromJsonor a customparsercallback throws (e.g. truncated JSON, type mismatch) - Closes a gap in the 2.0.0 contract:
on ApiException catchnow catches every client-side parse failure originalErrorandstackTraceare preserved- User-thrown
ApiExceptionfrom insidefromJson/parseris rethrown unchanged (no double-wrap)
- New
-
AuthInterceptor— Network blip no longer logs the user out (major)- When the refresh request fails with a connection / timeout error, the original request is rejected with
NetworkException(typed:ConnectionException,TimeoutException) onAuthFailureis not invoked on network failures- Real auth failures (401/403 from the refresh endpoint) still produce
AuthExceptionand callonAuthFailure(regression preserved)
- When the refresh request fails with a connection / timeout error, the original request is rejected with
-
AuthInterceptor— Token provider failures now typed (moderate)- New
TokenProviderException(operation: read | write | clear)(extendsApiException) - Wraps errors thrown by
getAccessToken,getRefreshToken, and the user-suppliedonTokenRefreshedcallback - Surfaces directly to callers:
on TokenProviderException catchdistinguishes credential storage issues from network/HTTP errors
- New
-
AuthExceptionnow preserves the underlying causeAuthException(message, originalError: ..., stackTrace: ...)— typed cause flows through to the caller viaoriginalError
Added #
-
RetryConfig.respectRetryAfter— Honor the server'sRetry-Afterheader on retryable responses (defaulttrue)- Parses both delta-seconds (
"60") and HTTP-date ("Wed, 21 Oct 2026 07:28:00 GMT") formats per RFC 7231 §7.1.3 - Capped at
RetryConfig.maxDelayMs; falls back to exponential backoff if the header is absent or malformed - Public
RetryInterceptor.parseRetryAfter(value, {now})exposed for advanced use and testing
- Parses both delta-seconds (
-
ApiClientConfig.strictContentType— Detect captive portals / wrong Content-Type (defaultfalse)- When
true,*AndDecodemethods verify the response'sContent-Typestarts withapplication/json - Throws
UnexpectedContentTypeException(extendsApiException) on mismatch — exposesexpectedContentTypeandactualContentTypefields *AndParsemethods are unaffected (they accept any payload type by design)
- When
-
ApiClientConfig.responseValidator— Hook for legacy APIs that signal business errors via HTTP 200ResponseValidatortypedef:ApiException? Function(Response)- Returning a non-null exception fails the request with that typed exception; returning
nulllets the response pass through - Only fires on 2xx responses (4xx/5xx still go through
ErrorMapperInterceptor) - Preserves the exact subclass returned (e.g. a custom
BusinessException)
Changed #
*AndDecodemethods now use<dynamic>internally with explicit_requireDatavalidation (instead of relying on Dio's eager generic cast)- Eliminates confusing
TypeErroron non-JSON responses; replaced by clearApiExceptionmessages _requireDatanow also throwsApiExceptionwhen the body is non-null but not aMap<String, dynamic>
- Eliminates confusing
2.0.0 #
Breaking #
ApiClientmethods now throwApiExceptioninstead ofDioException- All HTTP methods (
get,post,put,delete,patch) and typed variants unwrapDioExceptionautomatically - Code using
on DioException catchonApiClientmethods must migrate toon ApiException catch(or subtypes) client.dio(raw Dio access) still throwsDioException— onlyApiClientmethods are affectedgetResult()handles bothApiExceptionandDioException(fallback for raw Dio usage)
- All HTTP methods (
Fixed #
-
AuthInterceptor— Refresh request isolation (critical)- Refresh requests no longer inject the expired access token in the Authorization header
- Refresh requests that return 401 no longer cause a deadlock (recursive refresh loop)
- Auth-retried requests that fail again with 401 no longer trigger infinite refresh-retry loops
-
ApiClientmethods now throw typedApiExceptiondirectly (critical)- All HTTP methods (
get,post,put,delete,patch) unwrapDioExceptionautomatically on ClientException catch,on UnauthorizedException catch, etc. now work as expected- No need to catch
DioExceptionand extract.errormanually getResult()also works correctly with bothApiExceptionandDioException(fallback for raw Dio usage)
- All HTTP methods (
-
CacheInterceptor— Cache key generation (critical)- Fixed double-encoding of query parameters in cache keys
invalidateUrl()now resolves relative URLs against the client's base URL
-
CacheInterceptor— Deduplicated requests (major)- Deduplicated requests now use the main Dio instance (with auth, logging, etc.) instead of a bare Dio that lost all interceptors
-
ApiClient— Null safety on response body (major)*AndDecodemethods now throwApiExceptioninstead ofTypeErroron null response body (e.g. 204 No Content)_extractDatanow throwsApiExceptioninstead ofTypeErroron non-Map responses
-
ErrorMapperInterceptor— Nested error message extraction (major)- Now extracts messages from nested error objects:
{ "error": { "message": "..." } } - Supports common API formats:
error.message,error.detail,error.description captureStatusCodesfilter now applies consistently inonError(previously only filtered inonResponse)
- Now extracts messages from nested error objects:
-
MetricsInterceptor— Request ID collisions (moderate)- Uses monotonic counter instead of
_inFlight.lengthfor unique IDs - Adds orphan cleanup for entries older than 5 minutes
- Uses monotonic counter instead of
-
SecureStorageService— Corruption blast radius (moderate)read()andcontainsKey()now delete only the corrupted key instead of callingdeleteAll()
-
Interceptor resilience (moderate)
AuthInterceptor,RetryInterceptor,CacheInterceptornow wrap asynconRequest/onError/onResponsein try/catch to prevent silent request hangs on unexpected exceptions
-
Sentry noise filter (minor)
- No longer accidentally filters out ApiX's own
TimeoutException,HttpException,ClientException
- No longer accidentally filters out ApiX's own
Added #
-
AuthConfig.onAuthFailure— Centralized callback when token refresh fails- Called exactly once per refresh attempt (even with concurrent requests queued)
- Receives the error object for diagnostic:
onAuthFailure: (tokenProvider, error) async { ... } - Use to clear tokens, redirect to login, or log the failure reason
-
RetryConfig.maxDelayMs— Maximum delay cap for exponential backoff- Defaults to 30000ms (30 seconds)
- Prevents overflow on high attempt counts
-
InMemoryCacheStorage.maxEntries— Optional size limit with FIFO eviction -
CacheEntry.tryFromJson()— Null-safe factory for corrupted storage data
Changed #
AuthExceptionnow extendsUnauthorizedException(wasApiException)- Catchable with
on UnauthorizedException catchalongside normal 401 errors
- Catchable with
OnAuthFailureCallbacksignature:(TokenProvider, Object? error)— includes the failure reason- Empty tokens (
"") are now ignored inonRequestand_performSimplifiedRefresh
1.4.0 #
Added #
-
ApiClientConfig.dataKey- Configurable key for envelope unwrapping (default:'data')- Used by all
*Datamethods to extract payload fromresponse.data[dataKey] - Customizable per client:
ApiClientConfig(baseUrl: '...', dataKey: 'result')
- Used by all
-
Data methods (envelope unwrapping) - Extract and format
response.data[dataKey]for envelope APIs- GET single:
getAndDecodeData,getAndDecodeDataOrNull,getAndParseData,getAndParseDataOrNull - GET list:
getListAndDecodeData,getListAndDecodeDataOrNull,getListAndDecodeDataOrEmpty,getListAndParseData,getListAndParseDataOrNull,getListAndParseDataOrEmpty - POST single:
postAndDecodeData,postAndDecodeDataOrNull,postAndParseData,postAndParseDataOrNull - POST list:
postListAndDecodeData,postListAndDecodeDataOrNull,postListAndDecodeDataOrEmpty,postListAndParseData,postListAndParseDataOrNull,postListAndParseDataOrEmpty
- GET single:
Changed #
ApiClienttyped response methods redesigned - 3 clear levels of response handling:- Standard:
get,post,put,delete,patch→ rawResponse<T> - Parse/Decode:
{verb}AndParse,{verb}AndDecode→ formatresponse.data(non-nullable, all verbs) - Data:
{verb}And{Parse|Decode}Data→ unwrap envelope then format (GET & POST only, with OrNull/List variants)
- Standard:
Removed #
getAndParseOrNull,getAndDecodeOrNull- Replaced bygetAndParseDataOrNull,getAndDecodeDataOrNullpostAndParseOrNull,postAndDecodeOrNull- Replaced bypostAndParseDataOrNull,postAndDecodeDataOrNullgetListAndDecode,getListAndParse- Replaced bygetListAndDecodeData,getListAndParseDatagetListAndDecodeOrNull,getListAndDecodeOrEmpty- Replaced bygetListAndDecodeDataOrNull,getListAndDecodeDataOrEmptygetListAndParseOrNull,getListAndParseOrEmpty- Replaced bygetListAndParseDataOrNull,getListAndParseDataOrEmpty
1.3.0 #
Added #
-
SecureStorageService.withBiometrics()- Factory constructor for biometric-protected storage- iOS: Face ID / Touch ID via
userPresenceaccess control flag - Android: Biometric-backed encryption via
AndroidOptions.biometric()(API 28+) - Customizable prompt titles for Android
- iOS: Face ID / Touch ID via
-
SentrySetup.addBreadcrumbFromMap()- Helper method forErrorTrackingConfig.onBreadcrumb- Simplifies error tracking configuration to a single line
- Example:
errorTrackingConfig: ErrorTrackingConfig(onError: SentrySetup.captureException, onBreadcrumb: SentrySetup.addBreadcrumbFromMap)
-
Resultfunctional methods - Enhanced Result type with Either-like operationsgetOrElse(defaultValue)- Returns value or default on failureflatMap(transform)/flatMapAsync- Chains Result-returning operationsmapError(transform)- Transforms the error typerecover(fallback)- Recovers from failure with fallback value
-
ApiClientflexible parsing methods - Support for any response type, not just JSONgetAndParse(path, parser)- Parse any response type (int, String, DateTime, etc.)putAndParse,patchAndParse- PUT/PATCH variants- Note: OrNull and List variants were redesigned in 1.4.0 as Data methods with envelope unwrapping
Fixed #
SecureStorageService- Auto-clear storage on bad padding exception- Handles corrupted encrypted data (e.g., after app reinstall or key rotation)
- Affected methods:
read(),readAll(),containsKey() - Returns safe defaults (
null,{},false) instead of throwing
1.2.0 #
Added #
ErrorMapperInterceptor- Automatically transformsDioExceptioninto typedApiExceptionsubclasses- Timeout errors →
TimeoutException - Connection errors →
ConnectionException - HTTP 401 →
UnauthorizedException - HTTP 403 →
ForbiddenException - HTTP 404 →
NotFoundException - Other HTTP errors →
HttpException - Message extracted from response body (
message,error,detail,error_description) - Added automatically to all clients created via
ApiClientFactory
- Timeout errors →
Changed #
- Dependencies updated for latest versions compatibility:
dio:>=5.4.0 <7.0.0(was>=5.0.0)sentry_flutter:>=9.0.0 <10.0.0(was>=8.0.0)flutter_secure_storage:>=10.0.0 <11.0.0(was>=9.0.0)
SecureStorageService- Uses new secure defaults (RSA OAEP + AES-GCM) on AndroidSentrySetup- Updated for sentry_flutter 9.x API compatibility
1.1.0 #
Added #
authConfigparameter inApiClientFactory.create- Configure authentication directlyretryConfigparameter inApiClientFactory.create- Configure retry logic directlycacheConfigparameter inApiClientFactory.create- Configure caching directlyloggerConfigparameter inApiClientFactory.create- Configure logging directlyerrorTrackingConfigparameter inApiClientFactory.create- Configure error tracking directly (Sentry, Crashlytics, etc.)metricsConfigparameter inApiClientFactory.create- Configure request metrics directly
Changed #
- Renamed
captureException→onError,addBreadcrumb→onBreadcrumb - Updated README documentation to match actual API signatures
1.0.0 #
🎉 First Stable Release #
ApiX is now production-ready with a complete feature set for Flutter/Dart API clients.
Features #
- Core API Client - Dio-powered client with configurable timeouts, headers, and interceptors
- Authentication - TokenProvider interface with refresh token queue and automatic retry
- Secure Token Storage - Built-in SecureTokenProvider with flutter_secure_storage
- Retry Logic - Exponential backoff with configurable status codes and max attempts
- Smart Caching - CacheFirst, NetworkFirst, and HTTP-aware strategies with TTL
- Observability - Logger, Metrics, and Sentry interceptors for debugging and monitoring
- Result Pattern - Functional error handling with Success/Failure types
- Exception Hierarchy - NetworkException, HttpException, and typed client/server errors
Highlights #
- 401 tests passing
- Full API documentation
- Example app included
- CI/CD with GitHub Actions
0.3.0 #
Added #
-
SecureStorageService: Wrapper for
flutter_secure_storagewith simplified APIwrite(key, value),read(key),delete(key),deleteAll()containsKey(key),readAll()- Default secure options for Android and iOS
- Injectable
FlutterSecureStoragefor custom configuration
-
SecureTokenProvider: Ready-to-use
TokenProviderimplementation- Zero-boilerplate token management
- Configurable storage keys (
accessTokenKey,refreshTokenKey) - Exposed
storagegetter for secondary usage (Firebase tokens, API keys) - Works with
SecureStorageServicevia composition
-
Simplified Token Refresh: New
refreshEndpointapproach inAuthConfigrefreshEndpoint: Relative URL for automatic refresh callsrefreshHeaders: Optional custom headers for refresh requestonTokenRefreshed: Callback with rawResponsefor parsingrefreshTokenBodyKey: Configurable body key (default: 'refresh_token')hasSimplifiedRefresh: Getter to check if simplified flow is configured
Changed #
AuthInterceptornow supports both simplified and legacy refresh flows- Simplified flow takes priority when
refreshEndpointis configured
Backward Compatibility #
- Existing
onRefreshcallback still works as before - All new fields are optional with sensible defaults
0.0.1 #
- Initial release with core features:
- ApiClient with configurable timeouts and interceptors
- TokenProvider interface for authentication
- AuthInterceptor with refresh token queue
- RetryInterceptor with exponential backoff
- CacheInterceptor with multiple strategies
- LoggerInterceptor for debugging
- ErrorTrackingInterceptor for error reporting
- Result pattern for functional error handling