apix
Production-ready Flutter/Dart API client with auth refresh queue, exponential retry, smart caching and error tracking (Sentry-ready). Powered by Dio.
5.0.0
349 downloads/mo
#961 most downloaded on pub.dev
Germinator97/apix
What this package is like to depend on
Last release 11 days ago
12 Aug 2026
Ships fairly regularly
a new release about every 4 weeks
Nearly every release is documented
notes for 13 of 14 stable releases
Nothing withdrawn
no release was ever pulled
5 months old
14 releases · first in 2026
14 releases in the last 12 months
see the full history below
Release timeline
14 releases · Mar 2026 to Aug 2026Releases
latest 14-
5.0.012 Aug 2026Release notes
Open source →Two audits of the package, then the consumer's review of both: 47 defects, 20 reproduced against the real client. None raised, logged or reddened a test.
Nothing breaks your build — no public symbol removed or narrowed, checked by a 4.1.0-era consumer compiled against this release (
test/migration_4_1_0_compiles_test.dart). Everything below is runtime.Breaking — defaults
Four defaults used to send personal data to a third party out of the box.
SentrySetupOptions.sendDefaultPii→false. It wastrue, against the SDK's own default: an app that configured nothing shipped request headers, cookies and the user's IP to the tracker.ErrorTrackingConfig.captureResponseBody→false.ErrorTrackingConfig.redactUrls→true: query values are replaced before a URL reaches the tracker. Names are kept.LoggerConfiglogs no request or response body, error path included.LoggerConfig.trace()still keeps both.
⚠️ The middle two remove a field you may be diagnosing from. Set them back deliberately rather than discovering thinner Sentry tickets.
Breaking — behaviour
- Cache entries are scoped to the caller (
CacheConfig.varyHeaders, default['Authorization']) and keyed on the request body. A token refresh is now a miss — vary on a stable identity header, orconst []to opt out. CacheStorage.keys()no longer deletes expired entries — useCacheInterceptor.evictExpired().- A failing cache write no longer re-sends the request.
- A failed
SentrySetup.initstarts the app without Sentry, not at all. SecureStorageServicestops the Android plugin repairing unreadable entries behind it (resetOnError: false). Under the plugin's default the read still answerednull— after destroying a credential withoutonBeforeRecoveryDeleteever firing. A failed initialisation now raises instead of resetting: inject your ownFlutterSecureStorageto keep the old behaviour.- Expect a one-off rise in tracker events, in three classes that reached no
observer before: broken sessions,
responseValidatorrejections,cacheOnlymisses. Real failures, not new noise. A filter keyed on exception names cannot tell them from theirdart:ionamesakes.
Added
- Every typed-method family on every verb — 12 shapes x 5 verbs. GET and POST had 12 each, PUT and PATCH 2, DELETE none.
onSendProgress/onReceiveProgresson those methods.ApiClient.cacheInterceptor— the invalidation API on the instance in use.CacheConfig.onCacheHit/onCacheError— a hit reaches no observer, and a storage failure was absorbed in silence.CacheInterceptor.evictExpired().SecureStorageService.onBeforeRecoveryDelete— fires before the recovery deletes a credential.RequestOptions.forceRevalidate(),MultipartReplayException,CacheBodyEncoding,ErrorTrackingConfig.redactUrls.
Fixed
Cache
- Query parameters written into the path were dropped from the key, so
/users?page=1and?page=2shared one entry. - A failing write re-sent the request and returned the second response.
- A hit changed the body's type:
text/plain12345came back an int. - A
304served the body as stale and never restarted the TTL. no-cacheandmust-revalidatewere parsed and ignored.invalidateUrl('/users')also removed/users-archivedand/users/123.clearCache()under-reported by the entrieskeys()had just swept.- Concurrent
FileCacheStoragewrites to one key raced on a shared temp file.
Multipart
- Nested fields and files were dropped:
{'a': {'b': {'file': File}}}sent an empty body, and returned200. - An upload died after a token refresh or a retry, and its
StateErrorreplaced the server's status.
Observability
- A refresh failure reached no log and no tracker, and leaked its metric.
- A
responseValidatorrejection was recorded as a success, cached, then served unvalidated. - An error body reached the log sink whatever
logResponseBodysaid. ErrorMapperInterceptorrewrote an already-typed exception.- A reused
RequestOptionswas observed once; every retry stranded a metric. profilesSampleRateand both replay rates were accepted and ignored;customBeforeSendTransactiongot an emptyHint.
Client
- The
…OrEmpty/…OrNullvariants broke on a bare[]. They tolerate every shape of empty now, including an absentdatakey; the strict ones name the key they wanted. ApiException.codereturned the HTTP status disguised as a business code.RetryConfigbroke the==/hashCodecontract.RetryInterceptorswallowed failures of its own retry machinery.- A post-refresh token read did not raise
TokenProviderException. - A hanging
onAuthFailurefroze the refresh queue.
Docs
- The README showed
extra: {'cacheTtl': …}andextra: {'forceRefresh': true}, which never existed in the code. UsedefaultTtlandforceRevalidate(). - A cache hit reaches no logger, metric or span; an interceptor passed through
ApiClientConfig.interceptorssees a rawDioException;TokenProviderOperation.clearis never raised by apix. varyHeaders,forceRevalidate(),MultipartReplayExceptionandCacheBodyEncodingare documented at last.SecureStorageService.withBiometrics()refuses on a device with no PIN, pattern, password or enrolled biometric — it does not degrade silently, as this file previously claimed. Do not catch that refusal and keep writing: the plugin is left without a cipher for the rest of the process.
-
4.1.011 Aug 2026Release notes
Open source →Two rounds of the same defect, reported by a consumer and then found by auditing for others like it: a
Futurecompleted with an error that nobody listens to is reported to the zone — a Sentry event nobody can act on.Fixed
RequestDeduplicatorno longer reports an unhandled error on every failed request. ItsCompleterexists for duplicates that usually never arrive, so nothing listens to it — harmless on success, butcompleteErrorwith no listener reports to the zone. Invisible until aCancelTokenmade cancellation routine.DeduplicationInterceptor, added in 4.0.0, shares that deduplicator, so the defect had just become reachable without a cache.ErrorTrackingConfig.onErrorno longer leaks the same way. ItsFuturewas neither awaited nor ignored, so a tracker failing asynchronously reported an error nobody could receive — from inside the component whose job is receiving errors.- A failing observation callback no longer breaks the request. A
logHandler,onMetrics,onBreadcrumbor span starter that threw turned a200into anApiException— an analytics backend having a bad minute failed the business request it was only observing. - A deduplicated request is observed once. Outer and inner requests share a
CancelToken, so a cancellation put the outer one through the error chain twice: one network call, two log lines, two tracker events.
Changed
- Expect fewer events on the deduplicated error path — one per request instead of two. Nothing to change on your side; the counts in your dashboard drop by design.
Docs
EncryptedCacheStorage.has()can delete (it decrypts to answer, and purges what it cannot open),AuthInterceptorswallows a throwingonAuthFailureto avoid deadlocking the refresh queue, andTooManyRequestsException .retryAfteris populated regardless ofRetryConfig.respectRetryAfter.
-
4.0.010 Aug 2026Release notes
Open source →This release answers an integration report from a consumer, 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-Afterit parsed and dropped, a duration it measured and never reported.Migration at a glance
If you… Then… catch on ClientExceptionfor a 429still works — TooManyRequestsExceptionis a subtype. ReadretryAfterto say how long to waitassert an exact retry delay in a test add jitter: 0— delays are now spread ±20 % by defaultrely on CacheStrategy.networkOnlywriting to your storeit no longer writes. If you were counting on that, use networkFirstwrote a no-op CacheStoragejust to get deduplicationdelete it — pass deduplicationConfigand nocacheConfiggroup Sentry issues by the type passed to onErrorthe onResponsepath now sendsHttpTrackingExceptionas anHttpException. Existing issues may regroupimport 'package:dio/dio.dart'forResponseType,Interceptor,FormData…import package:apix/apix.dartinstead; adapters live inpackage:apix/testing.dartsubclass ApiExceptionwith your owncodefieldremove it and pass super.code— a same-typed field now shadows the inherited one, and a differently-typed one (int code) will not compileuse 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.007 Aug 2026Release notes
Open source →This release is about the cache. Two of its promises were not kept:
cacheFirstdid not refresh in the background despite saying so, and the TTL was only as strong as whicheverCacheStorageyou plugged in. Both are now true. Error tracking also stops flattening every failure into oneDioException.Migration at a glance
If you… Then… use CacheStrategy.cacheFirstresponses may now be older than defaultTtl. Checkresponse.isStale, or move tonetworkFirstwhere freshness matterscall CacheRequestExtension.isFromCache(response)replace with response.isFromCacheimplement CacheStorageyourselfdelete the expiry filter from get— keep it inhasinspect the argument of ErrorTrackingConfig.onErrorit is the typed ApiExceptionnow, not aDioException.e is DioExceptionstops matching silently — use(e as ApiException).originalErrorcatch on ApiExceptionaround acacheOnlycallyou will now actually catch CacheException; before, it slipped throughcatch on HttpExceptionfor 4xx/5xxstill works — ClientException/ServerExceptionare subtypesuse SentrySetupwithfilterNetworkNoiseyour 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.008 Jul 2026Release notes
Open source →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.
- ⚠️ BREAKING BEHAVIOR — Retry is now HTTP-method-aware;
-
2.2.012 May 2026Release notes
Open source →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.004 May 2026Release notes
Open source →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.001 Apr 2026Release notes
Open source →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.5.001 Apr 2026Nothing published for this version
-
1.4.026 Mar 2026Release notes
Open source →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.024 Mar 2026Release notes
Open source →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.020 Mar 2026Release notes
Open source →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.019 Mar 2026Release notes
Open source →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.018 Mar 2026Release notes
Open source →🎉 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