send method
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;
}
}