send method
Sends request and returns the wrapped LuckyResponse.
The method applies the throttlePolicy before each attempt, merges
connector-level defaults with request-level overrides via ConfigMerger,
resolves an optional async body, dispatches through dio, and—when
throwOnError is true—throws a typed LuckyException for non-2xx
responses.
When a retryPolicy is configured, failed attempts are transparently retried according to the policy's rules. LuckyThrottleException is never retried regardless of the retryPolicy.
Implementation
Future<LuckyResponse> send(Request request) async {
int attempt = 0;
while (true) {
attempt++;
// 1. Throttle before every attempt (initial + retries).
await throttlePolicy?.acquire();
try {
// 2. Merge headers (Connector defaults, then Request overrides).
final headers = ConfigMerger.mergeHeaders(
defaultHeaders(),
request.headers(),
);
// 3. Merge query parameters.
final query = ConfigMerger.mergeQuery(
defaultQuery(),
request.queryParameters(),
);
// 4. Merge Dio options.
final options = ConfigMerger.mergeOptions(
defaultOptions(),
request.buildOptions(),
request.method,
headers,
);
// 5. Store logging flags in extra so interceptors can inspect them.
options.extra ??= {};
options.extra!['logRequest'] = request.logRequest;
options.extra!['logResponse'] = request.logResponse;
// 6. Apply the authenticator when auth is enabled for this request.
final effectiveUseAuth =
ConfigMerger.resolveUseAuth(useAuth, request.useAuth);
if (effectiveUseAuth && authenticator != null) {
authenticator!.apply(options);
}
// 7. Resolve the body, awaiting it if it is a Future (e.g. multipart).
final body = await _resolveBody(request);
// 8. Dispatch the request through Dio.
final response = await dio.request(
request.resolveEndpoint(),
queryParameters: query,
data: body,
options: options,
);
final luckyResponse = LuckyResponse(response);
// 9. Check if the retry policy wants another attempt on this response.
final rp = retryPolicy;
if (rp != null &&
attempt < rp.maxAttempts &&
rp.shouldRetryOnResponse(luckyResponse, attempt)) {
await Future.delayed(rp.delayFor(attempt));
continue;
}
// 10. Lucky—not Dio—is responsible for HTTP error handling.
if (throwOnError && !luckyResponse.isSuccessful) {
throw _buildException(luckyResponse);
}
return luckyResponse;
} on LuckyThrottleException {
// Throttle exceptions are never retried — propagate immediately.
// IMPORTANT: this block must remain above `on LuckyException` because
// LuckyThrottleException IS a LuckyException. Moving or removing it
// would allow throttle errors to silently enter the retry loop.
rethrow;
} on LuckyException catch (e) {
// Note: this block is also reached when throwOnError=true causes
// _buildException() to throw (e.g. NotFoundException for 404). Custom
// RetryPolicy implementations should be aware that shouldRetryOnException
// is called for both network-level errors and HTTP-error exceptions.
final rp = retryPolicy;
if (rp != null &&
attempt < rp.maxAttempts &&
rp.shouldRetryOnException(e, attempt)) {
await Future.delayed(rp.delayFor(attempt));
continue;
}
rethrow;
} on DioException catch (e) {
final converted = _convertDioException(e);
final rp = retryPolicy;
if (rp != null &&
attempt < rp.maxAttempts &&
rp.shouldRetryOnException(converted, attempt)) {
await Future.delayed(rp.delayFor(attempt));
continue;
}
throw converted;
} finally {
// Release the slot after every attempt, success or failure.
// No-op for all policies except ConcurrencyThrottlePolicy.
// Note: with retry, `continue` inside the try block also triggers
// this finally before the next iteration — each attempt releases
// its own slot.
throttlePolicy?.release();
}
}
}