send method

  1. @override
Future<StreamedResponse> send(
  1. BaseRequest request
)

Sends an HTTP request and asynchronously returns the response.

Implementers should call BaseRequest.finalize to get the body of the request as a ByteStream. They shouldn't make any assumptions about the state of the stream; it could have data written to it asynchronously at a later point, or it could already be closed when it's returned. Any internal HTTP errors should be wrapped as ClientExceptions.

Implementation

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
  ObslyLogger.debug(
      'πŸ”₯ ObslyHttpClient.send() INTERCEPTED: ${request.method} ${request.url}');
  ObslyLogger.debug(
      'πŸ” State check: _disposed=$_disposed, _innerClient=${_innerClient?.runtimeType ?? 'null'}');

  // Reserve metadata at the very start of the request
  EventReservation? reservation;
  try {
    final hub = _integration._hub;
    reservation = hub?.reserveEventMetadata();
  } catch (_) {}

  // πŸ”§ HOTFIX: Auto-repair disposed client instead of falling back
  if (_disposed || _innerClient == null) {
    ObslyLogger.warn('πŸ”§ Auto-repairing disposed/null ObslyHttpClient...');
    _disposed = false;
    _innerClient = http.Client();
    ObslyLogger.debug(
        'βœ… ObslyHttpClient auto-repaired: _disposed=$_disposed, _innerClient=${_innerClient?.runtimeType}');
  }

  final metadata = HTTPRequestMetadata(
    method: request.method,
    url: request.url,
    headers: request.headers,
    body: await _extractRequestBody(request),
    startTime: DateTime.now().toUtc(),
    stopwatch: Stopwatch()..start(),
  );

  // Check if should capture before making request
  if (!metadata.shouldCapture(
      ConfigController.instance.config?.requestBlacklist?.toSet() ?? {})) {
    ObslyLogger.debug(
        'Request filtered out by URL blacklist: ${request.url}');
    return _innerClient!.send(request);
  }

  ObslyLogger.debug(
      '🎯 HTTP Request will be captured: ${request.method} ${request.url}');

  try {
    final response = await _innerClient!.send(request);
    metadata.stopwatch.stop();

    ObslyLogger.debug(
        'βœ… HTTP Response received: ${response.statusCode} for ${request.method} ${request.url}');

    http.StreamedResponse finalResponse = response;
    String? capturedBody;
    // Capture body only for error responses (4xx / 5xx)
    if (HTTPIntegration._isErrorStatusCode(response.statusCode)) {
      try {
        final bytes = await response.stream.toBytes();
        capturedBody = handleResponseBody(bytes, response.headers);
        // Recreate response with a fresh stream so callers can still read it
        finalResponse = http.StreamedResponse(
          Stream<List<int>>.fromIterable([bytes]),
          response.statusCode,
          contentLength: response.contentLength,
          request: response.request,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
        );
      } catch (e) {
        ObslyLogger.debug('Failed to capture error response body: $e');
      }
    }

    // Capture response event (use reservation from request start only)
    if (reservation != null) {
      _captureHTTPResponse(metadata, finalResponse, null,
          capturedBody: capturedBody, reservation: reservation);
    }

    return finalResponse;
  } catch (e) {
    metadata.stopwatch.stop();

    ObslyLogger.debug(
        '❌ HTTP Request failed: $e for ${request.method} ${request.url}');

    // Capture error (use reservation from request start only)
    if (reservation != null) {
      _captureHTTPResponse(metadata, null, e,
          capturedBody: null, reservation: reservation);
    }

    rethrow;
  }
}