1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io';
8import 'dart:typed_data';
9
10export 'dart:io' show HttpClientResponse;
11export 'dart:typed_data' show Uint8List;
12
13/// Signature for getting notified when chunks of bytes are received while
14/// consolidating the bytes of an [HttpClientResponse] into a [Uint8List].
15///
16/// The `cumulative` parameter will contain the total number of bytes received
17/// thus far. If the response has been gzipped, this number will be the number
18/// of compressed bytes that have been received _across the wire_.
19///
20/// The `total` parameter will contain the _expected_ total number of bytes to
21/// be received across the wire (extracted from the value of the
22/// `Content-Length` HTTP response header), or null if the size of the response
23/// body is not known in advance (this is common for HTTP chunked transfer
24/// encoding, which itself is common when a large amount of data is being
25/// returned to the client and the total size of the response may not be known
26/// until the request has been fully processed).
27///
28/// This is used in [consolidateHttpClientResponseBytes].
29typedef BytesReceivedCallback = void Function(int cumulative, int? total);
30
31/// Efficiently converts the response body of an [HttpClientResponse] into a
32/// [Uint8List].
33///
34/// The future returned will forward any error emitted by `response`.
35///
36/// The `onBytesReceived` callback, if specified, will be invoked for every
37/// chunk of bytes that is received while consolidating the response bytes.
38/// If the callback throws an error, processing of the response will halt, and
39/// the returned future will complete with the error that was thrown by the
40/// callback. For more information on how to interpret the parameters to the
41/// callback, see the documentation on [BytesReceivedCallback].
42///
43/// If the `response` is gzipped and the `autoUncompress` parameter is true,
44/// this will automatically un-compress the bytes in the returned list if it
45/// hasn't already been done via [HttpClient.autoUncompress]. To get compressed
46/// bytes from this method (assuming the response is sending compressed bytes),
47/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
48/// parameter to false.
49Future<Uint8List> consolidateHttpClientResponseBytes(
50 HttpClientResponse response, {
51 bool autoUncompress = true,
52 BytesReceivedCallback? onBytesReceived,
53}) {
54 final Completer<Uint8List> completer = Completer<Uint8List>.sync();
55
56 final _OutputBuffer output = _OutputBuffer();
57 ByteConversionSink sink = output;
58 int? expectedContentLength = response.contentLength;
59 if (expectedContentLength == -1) {
60 expectedContentLength = null;
61 }
62 switch (response.compressionState) {
63 case HttpClientResponseCompressionState.compressed:
64 if (autoUncompress) {
65 // We need to un-compress the bytes as they come in.
66 sink = gzip.decoder.startChunkedConversion(output);
67 }
68 case HttpClientResponseCompressionState.decompressed:
69 // response.contentLength will not match our bytes stream, so we declare
70 // that we don't know the expected content length.
71 expectedContentLength = null;
72 case HttpClientResponseCompressionState.notCompressed:
73 // Fall-through.
74 break;
75 }
76
77 int bytesReceived = 0;
78 late final StreamSubscription<List<int>> subscription;
79 subscription = response.listen((List<int> chunk) {
80 sink.add(chunk);
81 if (onBytesReceived != null) {
82 bytesReceived += chunk.length;
83 try {
84 onBytesReceived(bytesReceived, expectedContentLength);
85 } catch (error, stackTrace) {
86 completer.completeError(error, stackTrace);
87 subscription.cancel();
88 return;
89 }
90 }
91 }, onDone: () {
92 sink.close();
93 completer.complete(output.bytes);
94 }, onError: completer.completeError, cancelOnError: true);
95
96 return completer.future;
97}
98
99class _OutputBuffer extends ByteConversionSinkBase {
100 List<List<int>>? _chunks = <List<int>>[];
101 int _contentLength = 0;
102 Uint8List? _bytes;
103
104 @override
105 void add(List<int> chunk) {
106 assert(_bytes == null);
107 _chunks!.add(chunk);
108 _contentLength += chunk.length;
109 }
110
111 @override
112 void close() {
113 if (_bytes != null) {
114 // We've already been closed; this is a no-op
115 return;
116 }
117 _bytes = Uint8List(_contentLength);
118 int offset = 0;
119 for (final List<int> chunk in _chunks!) {
120 _bytes!.setRange(offset, offset + chunk.length, chunk);
121 offset += chunk.length;
122 }
123 _chunks = null;
124 }
125
126 Uint8List get bytes {
127 assert(_bytes != null);
128 return _bytes!;
129 }
130}
131