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 {
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;
}
}