makeRequest method

Future<Response> makeRequest(
  1. String method,
  2. Uri url, {
  3. Map<String, String>? headers,
  4. dynamic body,
  5. bool useAuth = true,
  6. bool isAuthRetry = false,
  7. int maxRetries = 3,
  8. int retryCount = 0,
})

Centralized request with retry logic and exponential backoff.

Implementation

Future<http.Response> makeRequest(
  String method,
  Uri url, {
  Map<String, String>? headers,
  dynamic body,
  bool useAuth = true,
  bool isAuthRetry = false,
  int maxRetries = 3,
  int retryCount = 0,
}) async {
  // Clone so the Authorization injection never mutates the caller's map
  // (it would otherwise leak a stale bearer token across reuses/retries).
  final requestHeaders = headers != null
      ? Map<String, String>.from(headers)
      : <String, String>{'Content-Type': 'application/json'};
  if (useAuth && apiKey != null && apiKey!.isNotEmpty) {
    requestHeaders['Authorization'] = 'Bearer $apiKey';
  }

  http.Response response;
  try {
    switch (method.toUpperCase()) {
      case 'GET':
        response = await _client.get(url, headers: requestHeaders);
        break;
      case 'POST':
        response =
            await _client.post(url, headers: requestHeaders, body: body);
        break;
      case 'PUT':
        response =
            await _client.put(url, headers: requestHeaders, body: body);
        break;
      case 'PATCH':
        response =
            await _client.patch(url, headers: requestHeaders, body: body);
        break;
      case 'DELETE':
        final request = http.Request('DELETE', url)
          ..headers.addAll(requestHeaders)
          ..body = body ?? '';
        final streamedResponse = await _client.send(request);
        response = await http.Response.fromStream(streamedResponse);
        break;
      default:
        throw Exception('Unsupported HTTP method: $method');
    }
  } catch (e) {
    log('Network error: $e');
    if (retryCount < maxRetries) {
      final delay = Duration(seconds: 1 << retryCount);
      log('Retrying in ${delay.inSeconds}s... (${retryCount + 1}/$maxRetries)');
      await Future.delayed(delay);
      return makeRequest(
        method,
        url,
        headers: headers,
        body: body,
        useAuth: useAuth,
        isAuthRetry: isAuthRetry,
        maxRetries: maxRetries,
        retryCount: retryCount + 1,
      );
    }
    throw Exception('Network request failed after $maxRetries attempts: $e');
  }

  // Handle 5xx errors with retry
  if (response.statusCode >= 500 && response.statusCode < 600) {
    if (retryCount < maxRetries) {
      final delay = Duration(seconds: 1 << retryCount);
      log('Server error ${response.statusCode}. Retrying in ${delay.inSeconds}s...');
      await Future.delayed(delay);
      return makeRequest(
        method,
        url,
        headers: headers,
        body: body,
        useAuth: useAuth,
        isAuthRetry: isAuthRetry,
        maxRetries: maxRetries,
        retryCount: retryCount + 1,
      );
    }
  }

  // Handle 401
  if (response.statusCode == 401 && useAuth && !isAuthRetry) {
    log('Auth error (401). API key may be invalid.');
  }

  if (response.statusCode < 200 || response.statusCode >= 300) {
    log('API Error ${response.statusCode}: ${response.body}');
    throw Exception('API Error: ${response.statusCode} - ${response.body}');
  }

  return response;
}