streamHttpRequest: replaced string buffering with chunk_expansion

This commit is contained in:
Manas Hejmadi
2025-07-04 02:31:42 +05:30
parent 2ab6de6a62
commit 6bcc855d06
3 changed files with 36 additions and 103 deletions

View File

@@ -68,11 +68,9 @@ class HttpResponseModel with _$HttpResponseModel {
}, response.headers); }, response.headers);
MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders);
//TODO: Review Effectiveness final body = (mediaType?.subtype == kSubTypeJson)
final body = decodeBytes( ? utf8.decode(response.bodyBytes)
response.bodyBytes, : response.body;
response.headers['content-type']!,
);
return HttpResponseModel( return HttpResponseModel(
statusCode: response.statusCode, statusCode: response.statusCode,

View File

@@ -27,8 +27,7 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest(
defaultUriScheme: defaultUriScheme, defaultUriScheme: defaultUriScheme,
noSSL: noSSL, noSSL: noSSL,
); );
final output = await stream.last; final output = await stream.first;
return (output?.$2, output?.$3, output?.$4); return (output?.$2, output?.$3, output?.$4);
// if (httpClientManager.wasRequestCancelled(requestId)) { // if (httpClientManager.wasRequestCancelled(requestId)) {
@@ -194,7 +193,7 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
StreamSubscription<List<int>?>? subscription; StreamSubscription<List<int>?>? subscription;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
cleanup() async { Future<void> cleanup() async {
stopwatch.stop(); stopwatch.stop();
await subscription?.cancel(); await subscription?.cancel();
httpClientManager.closeClient(requestId); httpClientManager.closeClient(requestId);
@@ -202,17 +201,22 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
controller.close(); controller.close();
} }
Future<void> handleError(dynamic error) async { Future<void> addCancelledMessage() async {
if (!controller.isClosed) {
controller.add((null, null, null, kMsgRequestCancelled));
}
httpClientManager.removeCancelledRequest(requestId);
await cleanup();
}
Future<void> addErrorMessage(dynamic error) async {
await Future.microtask(() {}); await Future.microtask(() {});
if (httpClientManager.wasRequestCancelled(requestId)) { if (httpClientManager.wasRequestCancelled(requestId)) {
if (!controller.isClosed) { await addCancelledMessage();
controller.add((null, null, null, kMsgRequestCancelled));
}
httpClientManager.removeCancelledRequest(requestId);
} else { } else {
controller.add((null, null, null, error.toString())); controller.add((null, null, null, error.toString()));
await cleanup();
} }
await cleanup();
} }
controller.onCancel = () async { controller.onCancel = () async {
@@ -221,11 +225,7 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
}; };
if (httpClientManager.wasRequestCancelled(requestId)) { if (httpClientManager.wasRequestCancelled(requestId)) {
if (!controller.isClosed) { await addCancelledMessage();
controller.add((null, null, null, kMsgRequestCancelled));
}
httpClientManager.removeCancelledRequest(requestId);
controller.close();
return controller.stream; return controller.stream;
} }
@@ -237,7 +237,7 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
); );
if (uri == null) { if (uri == null) {
await handleError(uriError ?? 'Invalid URL'); await addErrorMessage(uriError ?? 'Invalid URL');
return controller.stream; return controller.stream;
} }
@@ -248,7 +248,6 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
requestModel: requestModel, requestModel: requestModel,
apiType: apiType, apiType: apiType,
); );
//----------------- Response Handling ---------------------
HttpResponse getResponseFromBytes(List<int> bytes) { HttpResponse getResponseFromBytes(List<int> bytes) {
return HttpResponse.bytes( return HttpResponse.bytes(
@@ -262,54 +261,46 @@ Future<Stream<HttpStreamOutput>> streamHttpRequest(
); );
} }
final buffer = StringBuffer();
final contentType = final contentType =
streamedResponse.headers['content-type']?.toString() ?? ''; streamedResponse.headers['content-type']?.toString() ?? '';
final contentLength = final chunkList = <List<int>>[];
int.tryParse(streamedResponse.headers['content-length'] ?? '') ?? -1;
int receivedBytes = 0;
bool hasEmitted = false; bool hasEmitted = false;
subscription = streamedResponse.stream.listen( subscription = streamedResponse.stream.listen(
(bytes) { (bytes) async {
if (controller.isClosed) return; if (controller.isClosed) return;
if (httpClientManager.wasRequestCancelled(requestId)) {
return await addCancelledMessage();
}
final isStreaming = kStreamingResponseTypes.contains(contentType); final isStreaming = kStreamingResponseTypes.contains(contentType);
if (isStreaming) { if (isStreaming) {
//For Streaming responses, output every response
final response = getResponseFromBytes(bytes); final response = getResponseFromBytes(bytes);
controller.add((true, response, stopwatch.elapsed, null)); controller.add((true, response, stopwatch.elapsed, null));
return;
}
//For non Streaming events, add output to buffer
receivedBytes += bytes.length;
buffer.write(decodeBytes(bytes, contentType));
if (!hasEmitted &&
contentLength > 0 &&
receivedBytes >= contentLength &&
!controller.isClosed) {
final response = getResponseFromBytes(buffer.toString().codeUnits);
controller.add((false, response, stopwatch.elapsed, null));
hasEmitted = true; hasEmitted = true;
} else {
chunkList.add(bytes);
} }
}, },
onDone: () { onDone: () async {
//handle cases where response is larger than a TCP packet and cuts mid-way if (httpClientManager.wasRequestCancelled(requestId)) {
return await addCancelledMessage();
}
if (!hasEmitted && !controller.isClosed) { if (!hasEmitted && !controller.isClosed) {
final response = getResponseFromBytes(buffer.toString().codeUnits); final allBytes = chunkList.expand((x) => x).toList();
final response = getResponseFromBytes(allBytes);
final isStreaming = kStreamingResponseTypes.contains(contentType); final isStreaming = kStreamingResponseTypes.contains(contentType);
controller.add((isStreaming, response, stopwatch.elapsed, null)); controller.add((isStreaming, response, stopwatch.elapsed, null));
} }
cleanup(); await cleanup();
}, },
onError: handleError, onError: addErrorMessage,
); );
return controller.stream; return controller.stream;
} catch (e) { } catch (e) {
await handleError(e); await addErrorMessage(e);
return controller.stream; return controller.stream;
} }
} }
@@ -328,7 +319,7 @@ Future<http.StreamedResponse> makeStreamedRequest({
//----------------- Request Creation --------------------- //----------------- Request Creation ---------------------
//Handling HTTP Multipart Requests //Handling HTTP Multipart Requests
if (requestModel == APIType.rest && isMultipart && hasBody) { if (apiType == APIType.rest && isMultipart && hasBody) {
final multipart = http.MultipartRequest( final multipart = http.MultipartRequest(
requestModel.method.name.toUpperCase(), requestModel.method.name.toUpperCase(),
uri, uri,

View File

@@ -51,59 +51,3 @@ Future<http.Response> convertStreamedResponse(
return response; return response;
} }
// Stream<String?> streamTextResponse(
// http.StreamedResponse streamedResponse,
// ) async* {
// try {
// if (streamedResponse.statusCode != 200) {
// final errorText = await streamedResponse.stream.bytesToString();
// throw Exception('${streamedResponse.statusCode}\n$errorText');
// }
// final utf8Stream = streamedResponse.stream.transform(utf8.decoder);
// await for (final chunk in utf8Stream) {
// yield chunk;
// }
// } catch (e) {
// rethrow;
// }
// }
String getCharset(String contentType) {
final match = RegExp(
r'charset=([^\s;]+)',
caseSensitive: false,
).firstMatch(contentType);
return match?.group(1)?.toLowerCase() ?? 'utf-8'; // default to utf-8
}
String decodeBytes(List<int> bytes, String contentType) {
String _decodeUtf16(List<int> bytes, Endian endianness) {
final byteData = ByteData.sublistView(Uint8List.fromList(bytes));
final codeUnits = <int>[];
for (int i = 0; i + 1 < byteData.lengthInBytes; i += 2) {
codeUnits.add(byteData.getUint16(i, endianness));
}
return String.fromCharCodes(codeUnits);
}
final cSet = getCharset(contentType);
switch (cSet) {
case 'utf-8':
case 'utf8':
return utf8.decode(bytes, allowMalformed: true);
case 'utf-16':
case 'utf-16le':
return _decodeUtf16(bytes, Endian.little);
case 'utf-16be':
return _decodeUtf16(bytes, Endian.big);
case 'iso-8859-1':
case 'latin1':
return latin1.decode(bytes);
case 'us-ascii':
case 'ascii':
return ascii.decode(bytes);
default:
return utf8.decode(bytes, allowMalformed: true); //UTF8
}
}