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 | |
5 | import 'dart:async'; |
6 | import 'dart:convert'; |
7 | import 'dart:io'; |
8 | import 'dart:typed_data'; |
9 | |
10 | export 'dart:io' show HttpClientResponse; |
11 | export '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]. |
29 | typedef 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. |
49 | Future<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 | |
99 | class _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 | |