send method

Future<LuckyResponse> send(
  1. Request request
)

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();
    }
  }
}