Skip to content

Commit 58a5462

Browse files
JavaClient can stream the HTTP response body (dart-lang#1005)
1 parent df1f625 commit 58a5462

File tree

3 files changed

+127
-48
lines changed

3 files changed

+127
-48
lines changed

pkgs/java_http/lib/src/java_client.dart

+124-47
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:io';
67
import 'dart:isolate';
78
import 'dart:typed_data';
89

10+
import 'package:async/async.dart';
911
import 'package:http/http.dart';
1012
import 'package:jni/jni.dart';
1113
import 'package:path/path.dart';
@@ -39,51 +41,118 @@ class JavaClient extends BaseClient {
3941
// See https://github.com/dart-lang/http/pull/980#discussion_r1253700470.
4042
_initJVM();
4143

44+
final receivePort = ReceivePort();
45+
final events = StreamQueue<dynamic>(receivePort);
46+
4247
// We can't send a StreamedRequest to another Isolate.
4348
// But we can send Map<String, String>, String, UInt8List, Uri.
44-
final requestBody = await request.finalize().toBytes();
45-
final requestHeaders = request.headers;
46-
final requestMethod = request.method;
47-
final requestUrl = request.url;
48-
49-
final (statusCode, reasonPhrase, responseHeaders, responseBody) =
50-
await Isolate.run(() async {
51-
final httpUrlConnection = URL
52-
.ctor3(requestUrl.toString().toJString())
53-
.openConnection()
54-
.castTo(HttpURLConnection.type, deleteOriginal: true);
55-
56-
requestHeaders.forEach((headerName, headerValue) {
57-
httpUrlConnection.setRequestProperty(
58-
headerName.toJString(), headerValue.toJString());
59-
});
60-
61-
httpUrlConnection.setRequestMethod(requestMethod.toJString());
62-
_setRequestBody(httpUrlConnection, requestBody);
63-
64-
final statusCode = _statusCode(requestUrl, httpUrlConnection);
65-
final reasonPhrase = _reasonPhrase(httpUrlConnection);
66-
final responseHeaders = _responseHeaders(httpUrlConnection);
67-
final responseBody = _responseBody(httpUrlConnection);
68-
69-
httpUrlConnection.disconnect();
49+
final isolateRequest = (
50+
sendPort: receivePort.sendPort,
51+
url: request.url,
52+
method: request.method,
53+
headers: request.headers,
54+
body: await request.finalize().toBytes(),
55+
);
56+
57+
// Could create a new class to hold the data for the isolate instead
58+
// of using a record.
59+
await Isolate.spawn(_isolateMethod, isolateRequest);
60+
61+
final statusCodeEvent = await events.next;
62+
late int statusCode;
63+
if (statusCodeEvent is ClientException) {
64+
// Do we need to close the ReceivePort here as well?
65+
receivePort.close();
66+
throw statusCodeEvent;
67+
} else {
68+
statusCode = statusCodeEvent as int;
69+
}
7070

71-
return (
72-
statusCode,
73-
reasonPhrase,
74-
responseHeaders,
75-
responseBody,
76-
);
77-
});
71+
final reasonPhrase = await events.next as String?;
72+
final responseHeaders = await events.next as Map<String, String>;
73+
74+
Stream<List<int>> responseBodyStream(Stream<dynamic> events) async* {
75+
try {
76+
await for (final event in events) {
77+
if (event is List<int>) {
78+
yield event;
79+
} else if (event is ClientException) {
80+
throw event;
81+
} else if (event == null) {
82+
return;
83+
}
84+
}
85+
} finally {
86+
// TODO: Should we kill the isolate here?
87+
receivePort.close();
88+
}
89+
}
7890

79-
return StreamedResponse(Stream.value(responseBody), statusCode,
80-
contentLength:
81-
_contentLengthHeader(request, responseHeaders, responseBody.length),
91+
return StreamedResponse(responseBodyStream(events.rest), statusCode,
92+
contentLength: _parseContentLengthHeader(request.url, responseHeaders),
8293
request: request,
8394
headers: responseHeaders,
8495
reasonPhrase: reasonPhrase);
8596
}
8697

98+
// TODO: Rename _isolateMethod to something more descriptive.
99+
void _isolateMethod(
100+
({
101+
SendPort sendPort,
102+
Uint8List body,
103+
Map<String, String> headers,
104+
String method,
105+
Uri url,
106+
}) request,
107+
) {
108+
final httpUrlConnection = URL
109+
.ctor3(request.url.toString().toJString())
110+
.openConnection()
111+
.castTo(HttpURLConnection.type, deleteOriginal: true);
112+
113+
request.headers.forEach((headerName, headerValue) {
114+
httpUrlConnection.setRequestProperty(
115+
headerName.toJString(), headerValue.toJString());
116+
});
117+
118+
httpUrlConnection.setRequestMethod(request.method.toJString());
119+
_setRequestBody(httpUrlConnection, request.body);
120+
121+
try {
122+
final statusCode = _statusCode(request.url, httpUrlConnection);
123+
request.sendPort.send(statusCode);
124+
} on ClientException catch (e) {
125+
request.sendPort.send(e);
126+
httpUrlConnection.disconnect();
127+
return;
128+
}
129+
130+
final reasonPhrase = _reasonPhrase(httpUrlConnection);
131+
request.sendPort.send(reasonPhrase);
132+
133+
final responseHeaders = _responseHeaders(httpUrlConnection);
134+
request.sendPort.send(responseHeaders);
135+
136+
// TODO: Throws a ClientException if the content length header is invalid.
137+
// I think we need to send the ClientException over the SendPort.
138+
final contentLengthHeader = _parseContentLengthHeader(
139+
request.url,
140+
responseHeaders,
141+
);
142+
143+
_responseBody(
144+
request.url,
145+
httpUrlConnection,
146+
request.sendPort,
147+
contentLengthHeader,
148+
);
149+
150+
httpUrlConnection.disconnect();
151+
152+
// Signals to the receiving isolate that we are done sending events.
153+
request.sendPort.send(null);
154+
}
155+
87156
void _setRequestBody(
88157
HttpURLConnection httpUrlConnection,
89158
Uint8List requestBody,
@@ -142,42 +211,50 @@ class JavaClient extends BaseClient {
142211
return headers.map((key, value) => MapEntry(key, value.join(',')));
143212
}
144213

145-
int? _contentLengthHeader(
146-
BaseRequest request, Map<String, String> headers, int bodyLength) {
214+
int? _parseContentLengthHeader(
215+
Uri requestUrl,
216+
Map<String, String> headers,
217+
) {
147218
int? contentLength;
148219
switch (headers['content-length']) {
149220
case final contentLengthHeader?
150221
when !_digitRegex.hasMatch(contentLengthHeader):
151222
throw ClientException(
152223
'Invalid content-length header [$contentLengthHeader].',
153-
request.url,
224+
requestUrl,
154225
);
155226
case final contentLengthHeader?:
156227
contentLength = int.parse(contentLengthHeader);
157-
if (bodyLength < contentLength) {
158-
throw ClientException('Unexpected end of body', request.url);
159-
}
160228
}
161229

162230
return contentLength;
163231
}
164232

165-
Uint8List _responseBody(HttpURLConnection httpUrlConnection) {
233+
void _responseBody(
234+
Uri requestUrl,
235+
HttpURLConnection httpUrlConnection,
236+
SendPort sendPort,
237+
int? expectedBodyLength,
238+
) {
166239
final responseCode = httpUrlConnection.getResponseCode();
167240

168241
final inputStream = (responseCode >= 200 && responseCode <= 299)
169242
? httpUrlConnection.getInputStream()
170243
: httpUrlConnection.getErrorStream();
171244

172-
final bytes = <int>[];
173245
int byte;
246+
var actualBodyLength = 0;
247+
// TODO: inputStream.read() could throw IOException.
174248
while ((byte = inputStream.read()) != -1) {
175-
bytes.add(byte);
249+
sendPort.send([byte]);
250+
actualBodyLength++;
176251
}
177252

178-
inputStream.close();
253+
if (expectedBodyLength != null && actualBodyLength < expectedBodyLength) {
254+
sendPort.send(ClientException('Unexpected end of body', requestUrl));
255+
}
179256

180-
return Uint8List.fromList(bytes);
257+
inputStream.close();
181258
}
182259
}
183260

pkgs/java_http/pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ environment:
88
sdk: ^3.0.0
99

1010
dependencies:
11+
async: ^2.11.0
1112
http: '>=0.13.4 <2.0.0'
1213
jni: ^0.5.0
1314
path: ^1.8.0

pkgs/java_http/test/java_client_test.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import 'package:test/test.dart';
99
void main() {
1010
group('java_http client conformance tests', () {
1111
testIsolate(JavaClient.new);
12-
testResponseBody(JavaClient(), canStreamResponseBody: false);
12+
testResponseBody(JavaClient());
13+
testResponseBodyStreamed(JavaClient());
1314
testResponseHeaders(JavaClient());
1415
testRequestBody(JavaClient());
1516
testRequestHeaders(JavaClient());

0 commit comments

Comments
 (0)