send method

Future<Response?> send(
  1. Request request, {
  2. Duration? timeout,
  3. bool returnsBinary = false,
})

Implementation

Future<http.Response?> send(
  http.Request request, {
  Duration? timeout,
  bool returnsBinary = false,
}) async {
  Future<http.Response?> doSendOnce(http.Request req) async {
    final streamed = await _client.send(req).timeout(timeout!, onTimeout: () {
      debugPrint('HTTP Request Timeout nach ${timeout?.inSeconds} Sekunden');
      throw TimeoutException('HTTP Request Timeout', timeout);
    });

    if (!returnsBinary) {
      return http.Response.fromStream(streamed);
    }

    // For binary responses (images/docs): read manually to reduce stream errors
    // and avoid unhandled exceptions from partially closed connections.
    final builder = BytesBuilder(copy: false);
    await for (final chunk in streamed.stream) {
      builder.add(chunk);
    }
    return http.Response.bytes(
      builder.takeBytes(),
      streamed.statusCode,
      headers: streamed.headers,
      request: streamed.request,
      isRedirect: streamed.isRedirect,
      persistentConnection: streamed.persistentConnection,
      reasonPhrase: streamed.reasonPhrase,
    );
  }

  http.Request clone(http.Request req) {
    final r = http.Request(req.method, req.url);
    r.headers.addAll(req.headers);
    r.bodyBytes = req.bodyBytes;
    r.encoding = req.encoding;
    r.followRedirects = req.followRedirects;
    r.maxRedirects = req.maxRedirects;
    r.persistentConnection = req.persistentConnection;
    return r;
  }

  bool isRetryableStreamError(Object e) {
    if (e is http.ClientException) {
      final msg = e.message.toLowerCase();
      return msg.contains('connection closed') ||
          msg.contains('connection reset') ||
          msg.contains('broken pipe');
    }
    if (e is IOException) return true;
    return false;
  }

  // Verwende einen längeren Standard-Timeout, wenn keiner angegeben ist
  timeout ??= Duration(minutes: 2);

  final runner = returnsBinary
      ? _binaryDownloadSemaphore.withPermit<http.Response?>(() async {
          return _sendWithRetries(
            () => doSendOnce(clone(request)),
            isRetryable: isRetryableStreamError,
          );
        })
      : _sendWithRetries(
          () => doSendOnce(clone(request)),
          isRetryable: isRetryableStreamError,
        );

  try {
    final response = await runner;
    BackendReachability.instance.clearFailure();
    return response;
  } on SocketException catch (e) {
    if (!_logReachabilityFailureOnce(e)) {
      debugPrint('Socket-Fehler beim Senden des Requests: ${e.message}');
    }
    if (e.message.contains('Software caused connection abort') ||
        e.message.contains('Write failed')) {
      debugPrint(
          'App wahrscheinlich im Hintergrund, Socket wurde vom System geschlossen');
    }
    rethrow;
  } catch (e) {
    if (!_logReachabilityFailureOnce(e)) {
      debugPrint('Fehler beim Senden des Requests: $e');
    }
    rethrow;
  }
}