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 {
  final stopwatch = Stopwatch()..start();
  final spanId = Utils.generateHex(16);

  final traceId = Utils.resolveTraceId();
  final customSpanContext = Utils.buildCustomSpanNetworkContext();

  // Get options from global storage
  final options = CxFlutterPlugin.globalOptions;
  final shouldAddTraceParent = options != null &&
      Utils.shouldAddTraceParent(request.url.toString(), options);

  if (shouldAddTraceParent) {
    request.headers['traceparent'] = generateTraceParent(traceId, spanId);
  }

  // Extract request payload before the stream is consumed by _inner.send.
  final requestPayload = _extractRequestPayload(request);

  try {
    final response = await _inner.send(request);
    stopwatch.stop();

    // Buffer as raw bytes to preserve the original content exactly.
    // This is non-lossy for binary and text responses alike.
    final bodyBytes = await response.stream.toBytes();

    // Only decode text-based responses for telemetry, matching the Android SDK's
    // isTextBasedContentType guard. Binary content (images, compressed data, etc.)
    // is never decoded, avoiding garbled payload strings and corruption.
    final contentType = response.headers['content-type'];
    final responseBodyText = _isTextContentType(contentType)
        ? utf8.decode(bodyBytes, allowMalformed: true)
        : null;

    final captureContext = Utils.buildCaptureContext(
      url: request.url.toString(),
      rules: options?.networkCaptureConfig ?? [],
      reqHeaders: Map<String, String>.from(request.headers),
      resHeaders: Map<String, String>.from(response.headers),
      requestPayload: requestPayload,
      responsePayload: responseBodyText,
    );

    // Fire-and-forget: telemetry must not delay the response back to the caller.
    CxFlutterPlugin.setNetworkRequestContext({
      'url': request.url.toString(),
      'host': request.url.host,
      'method': request.method,
      'status_code': response.statusCode,
      'status_text': response.reasonPhrase ?? '',
      'duration': stopwatch.elapsedMilliseconds,
      'http_response_body_size': bodyBytes.length,
      'fragments': request.url.path,
      'schema': request.url.scheme,
      ...customSpanContext,
      ...captureContext,
      if (shouldAddTraceParent) 'traceId': traceId,
      if (shouldAddTraceParent) 'spanId': spanId,
    }).catchError((Object e) {
      debugPrint('[CxHttpClient] setNetworkRequestContext failed: $e');
      return null;
    });

    // Return the response with the original bytes intact.
    return http.StreamedResponse(
      Stream.fromIterable([bodyBytes]),
      response.statusCode,
      contentLength: response.contentLength,
      request: response.request,
      headers: response.headers,
      isRedirect: response.isRedirect,
      persistentConnection: response.persistentConnection,
      reasonPhrase: response.reasonPhrase,
    );
  } catch (e) {
    stopwatch.stop();
    final captureContext = Utils.buildCaptureContext(
      url: request.url.toString(),
      rules: options?.networkCaptureConfig ?? [],
      reqHeaders: Map<String, String>.from(request.headers),
      resHeaders: null,
      requestPayload: requestPayload,
      responsePayload: null,
    );
    CxFlutterPlugin.setNetworkRequestContext({
      'url': request.url.toString(),
      'host': request.url.host,
      'method': request.method,
      'status_code': 0,
      'status_text': '',
      'duration': stopwatch.elapsedMilliseconds,
      'http_response_body_size': 0,
      'fragments': request.url.path,
      'schema': request.url.scheme,
      ...customSpanContext,
      ...captureContext,
      'error_message': e.toString(),
      if (shouldAddTraceParent) 'traceId': traceId,
      if (shouldAddTraceParent) 'spanId': spanId,
    }).catchError((Object err) {
      debugPrint('[CxHttpClient] setNetworkRequestContext failed: $err');
      return null;
    });
    rethrow;
  }
}