Skip to content

Commit b7ec613

Browse files
authored
pkgs/ok_http: DevTools Networking Support. (#1242)
1 parent 4d8e7ef commit b7ec613

File tree

9 files changed

+649
-58
lines changed

9 files changed

+649
-58
lines changed

pkgs/ok_http/CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## 0.1.0-wip
22

33
- Implementation of [`BaseClient`](https://pub.dev/documentation/http/latest/http/BaseClient-class.html) and `send()` method using [`enqueue()` API](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/enqueue.html)
4-
- `ok_http` can now send asynchronous requests
4+
- `ok_http` can now send asynchronous requests and stream response bodies.
5+
- Add [DevTools Network View](https://docs.flutter.dev/tools/devtools/network) support.

pkgs/ok_http/android/src/main/kotlin/com/example/ok_http/RedirectInterceptor.kt

+18-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,19 @@ package com.example.ok_http
1111

1212
import okhttp3.Interceptor
1313
import okhttp3.OkHttpClient
14+
import okhttp3.Response
1415
import java.io.IOException
1516

17+
/**
18+
* Callback interface utilized by the [RedirectInterceptor].
19+
*
20+
* Allows Dart code to operate upon the intermediate redirect responses.
21+
*/
22+
interface RedirectReceivedCallback {
23+
fun onRedirectReceived(response: Response, location: String)
24+
}
25+
26+
1627
class RedirectInterceptor {
1728
companion object {
1829

@@ -26,7 +37,10 @@ class RedirectInterceptor {
2637
* @return OkHttpClient.Builder
2738
*/
2839
fun addRedirectInterceptor(
29-
clientBuilder: OkHttpClient.Builder, maxRedirects: Int, followRedirects: Boolean
40+
clientBuilder: OkHttpClient.Builder,
41+
maxRedirects: Int,
42+
followRedirects: Boolean,
43+
redirectCallback: RedirectReceivedCallback,
3044
): OkHttpClient.Builder {
3145
return clientBuilder.addInterceptor(Interceptor { chain ->
3246
var req = chain.request()
@@ -39,6 +53,9 @@ class RedirectInterceptor {
3953
}
4054

4155
val location = response.header("location") ?: break
56+
57+
redirectCallback.onRedirectReceived(response, location)
58+
4259
req = req.newBuilder().url(location).build()
4360
response.close()
4461
response = chain.proceed(req)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:http/http.dart';
10+
import 'package:http_profile/http_profile.dart';
11+
import 'package:integration_test/integration_test.dart';
12+
import 'package:ok_http/ok_http.dart';
13+
14+
void main() {
15+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
16+
17+
group('profile', () {
18+
final profilingEnabled = HttpClientRequestProfile.profilingEnabled;
19+
20+
setUpAll(() {
21+
HttpClientRequestProfile.profilingEnabled = true;
22+
});
23+
24+
tearDownAll(() {
25+
HttpClientRequestProfile.profilingEnabled = profilingEnabled;
26+
});
27+
28+
group('POST', () {
29+
late HttpServer successServer;
30+
late Uri successServerUri;
31+
late HttpClientRequestProfile profile;
32+
33+
setUpAll(() async {
34+
successServer = (await HttpServer.bind('localhost', 0))
35+
..listen((request) async {
36+
await request.drain<void>();
37+
request.response.headers.set('Content-Type', 'text/plain');
38+
request.response.headers.set('Content-Length', '11');
39+
request.response.write('Hello World');
40+
await request.response.close();
41+
});
42+
successServerUri = Uri.http('localhost:${successServer.port}');
43+
final client = OkHttpClientWithProfile();
44+
await client.post(successServerUri,
45+
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
46+
profile = client.profile!;
47+
});
48+
tearDownAll(() {
49+
successServer.close();
50+
});
51+
52+
test('profile attributes', () {
53+
expect(profile.events, isEmpty);
54+
expect(profile.requestMethod, 'POST');
55+
expect(profile.requestUri, successServerUri.toString());
56+
expect(
57+
profile.connectionInfo, containsPair('package', 'package:ok_http'));
58+
});
59+
60+
test('request attributes', () {
61+
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
62+
expect(profile.requestData.contentLength, 2);
63+
expect(profile.requestData.endTime, isNotNull);
64+
expect(profile.requestData.error, isNull);
65+
expect(
66+
profile.requestData.headers, containsPair('Content-Length', ['2']));
67+
expect(profile.requestData.headers,
68+
containsPair('Content-Type', ['text/plain; charset=utf-8']));
69+
expect(profile.requestData.persistentConnection, isNull);
70+
expect(profile.requestData.proxyDetails, isNull);
71+
expect(profile.requestData.startTime, isNotNull);
72+
});
73+
74+
test('response attributes', () {
75+
expect(profile.responseData.bodyBytes, 'Hello World'.codeUnits);
76+
expect(profile.responseData.compressionState, isNull);
77+
expect(profile.responseData.contentLength, 11);
78+
expect(profile.responseData.endTime, isNotNull);
79+
expect(profile.responseData.error, isNull);
80+
expect(profile.responseData.headers,
81+
containsPair('content-type', ['text/plain']));
82+
expect(profile.responseData.headers,
83+
containsPair('content-length', ['11']));
84+
expect(profile.responseData.isRedirect, false);
85+
expect(profile.responseData.persistentConnection, isNull);
86+
expect(profile.responseData.reasonPhrase, 'OK');
87+
expect(profile.responseData.redirects, isEmpty);
88+
expect(profile.responseData.startTime, isNotNull);
89+
expect(profile.responseData.statusCode, 200);
90+
});
91+
});
92+
93+
group('failed POST request', () {
94+
late HttpClientRequestProfile profile;
95+
96+
setUpAll(() async {
97+
final client = OkHttpClientWithProfile();
98+
try {
99+
await client.post(Uri.http('thisisnotahost'),
100+
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
101+
fail('expected exception');
102+
} on ClientException {
103+
// Expected exception.
104+
}
105+
profile = client.profile!;
106+
});
107+
108+
test('profile attributes', () {
109+
expect(profile.events, isEmpty);
110+
expect(profile.requestMethod, 'POST');
111+
expect(profile.requestUri, 'http://thisisnotahost');
112+
expect(
113+
profile.connectionInfo, containsPair('package', 'package:ok_http'));
114+
});
115+
116+
test('request attributes', () {
117+
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
118+
expect(profile.requestData.contentLength, 2);
119+
expect(profile.requestData.endTime, isNotNull);
120+
expect(profile.requestData.error, startsWith('ClientException:'));
121+
expect(
122+
profile.requestData.headers, containsPair('Content-Length', ['2']));
123+
expect(profile.requestData.headers,
124+
containsPair('Content-Type', ['text/plain; charset=utf-8']));
125+
expect(profile.requestData.persistentConnection, isNull);
126+
expect(profile.requestData.proxyDetails, isNull);
127+
expect(profile.requestData.startTime, isNotNull);
128+
});
129+
130+
test('response attributes', () {
131+
expect(profile.responseData.bodyBytes, isEmpty);
132+
expect(profile.responseData.compressionState, isNull);
133+
expect(profile.responseData.contentLength, isNull);
134+
expect(profile.responseData.endTime, isNull);
135+
expect(profile.responseData.error, isNull);
136+
expect(profile.responseData.headers, isNull);
137+
expect(profile.responseData.isRedirect, isNull);
138+
expect(profile.responseData.persistentConnection, isNull);
139+
expect(profile.responseData.reasonPhrase, isNull);
140+
expect(profile.responseData.redirects, isEmpty);
141+
expect(profile.responseData.startTime, isNull);
142+
expect(profile.responseData.statusCode, isNull);
143+
});
144+
});
145+
146+
group('failed POST response', () {
147+
late HttpServer successServer;
148+
late Uri successServerUri;
149+
late HttpClientRequestProfile profile;
150+
151+
setUpAll(() async {
152+
successServer = (await HttpServer.bind('localhost', 0))
153+
..listen((request) async {
154+
await request.drain<void>();
155+
request.response.headers.set('Content-Type', 'text/plain');
156+
request.response.headers.set('Content-Length', '11');
157+
final socket = await request.response.detachSocket();
158+
await socket.close();
159+
});
160+
successServerUri = Uri.http('localhost:${successServer.port}');
161+
final client = OkHttpClientWithProfile();
162+
163+
try {
164+
await client.post(successServerUri,
165+
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
166+
fail('expected exception');
167+
} on ClientException {
168+
// Expected exception.
169+
}
170+
profile = client.profile!;
171+
});
172+
tearDownAll(() {
173+
successServer.close();
174+
});
175+
176+
test('profile attributes', () {
177+
expect(profile.events, isEmpty);
178+
expect(profile.requestMethod, 'POST');
179+
expect(profile.requestUri, successServerUri.toString());
180+
expect(
181+
profile.connectionInfo, containsPair('package', 'package:ok_http'));
182+
});
183+
184+
test('request attributes', () {
185+
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
186+
expect(profile.requestData.contentLength, 2);
187+
expect(profile.requestData.endTime, isNotNull);
188+
expect(profile.requestData.error, isNull);
189+
expect(
190+
profile.requestData.headers, containsPair('Content-Length', ['2']));
191+
expect(profile.requestData.headers,
192+
containsPair('Content-Type', ['text/plain; charset=utf-8']));
193+
expect(profile.requestData.persistentConnection, isNull);
194+
expect(profile.requestData.proxyDetails, isNull);
195+
expect(profile.requestData.startTime, isNotNull);
196+
});
197+
198+
test('response attributes', () {
199+
expect(profile.responseData.bodyBytes, isEmpty);
200+
expect(profile.responseData.compressionState, isNull);
201+
expect(profile.responseData.contentLength, 11);
202+
expect(profile.responseData.endTime, isNotNull);
203+
expect(profile.responseData.error, startsWith('ClientException:'));
204+
expect(profile.responseData.headers,
205+
containsPair('content-type', ['text/plain']));
206+
expect(profile.responseData.headers,
207+
containsPair('content-length', ['11']));
208+
expect(profile.responseData.isRedirect, false);
209+
expect(profile.responseData.persistentConnection, isNull);
210+
expect(profile.responseData.reasonPhrase, 'OK');
211+
expect(profile.responseData.redirects, isEmpty);
212+
expect(profile.responseData.startTime, isNotNull);
213+
expect(profile.responseData.statusCode, 200);
214+
});
215+
});
216+
217+
group('redirects', () {
218+
late HttpServer successServer;
219+
late Uri successServerUri;
220+
late HttpClientRequestProfile profile;
221+
222+
setUpAll(() async {
223+
successServer = (await HttpServer.bind('localhost', 0))
224+
..listen((request) async {
225+
if (request.requestedUri.pathSegments.isEmpty) {
226+
unawaited(request.response.close());
227+
} else {
228+
final n = int.parse(request.requestedUri.pathSegments.last);
229+
final nextPath = n - 1 == 0 ? '' : '${n - 1}';
230+
unawaited(request.response
231+
.redirect(successServerUri.replace(path: '/$nextPath')));
232+
}
233+
});
234+
successServerUri = Uri.http('localhost:${successServer.port}');
235+
});
236+
tearDownAll(() {
237+
successServer.close();
238+
});
239+
240+
test('no redirects', () async {
241+
final client = OkHttpClientWithProfile();
242+
await client.get(successServerUri);
243+
profile = client.profile!;
244+
245+
expect(profile.responseData.redirects, isEmpty);
246+
});
247+
248+
test('follow redirects', () async {
249+
final client = OkHttpClientWithProfile();
250+
await client.send(Request('GET', successServerUri.replace(path: '/3'))
251+
..followRedirects = true
252+
..maxRedirects = 4);
253+
profile = client.profile!;
254+
255+
expect(profile.requestData.followRedirects, true);
256+
expect(profile.requestData.maxRedirects, 4);
257+
expect(profile.responseData.isRedirect, false);
258+
259+
expect(profile.responseData.redirects, [
260+
HttpProfileRedirectData(
261+
statusCode: 302,
262+
method: 'GET',
263+
location: successServerUri.replace(path: '/2').toString()),
264+
HttpProfileRedirectData(
265+
statusCode: 302,
266+
method: 'GET',
267+
location: successServerUri.replace(path: '/1').toString()),
268+
HttpProfileRedirectData(
269+
statusCode: 302,
270+
method: 'GET',
271+
location: successServerUri.replace(path: '/').toString(),
272+
)
273+
]);
274+
});
275+
276+
test('no follow redirects', () async {
277+
final client = OkHttpClientWithProfile();
278+
await client.send(Request('GET', successServerUri.replace(path: '/3'))
279+
..followRedirects = false);
280+
profile = client.profile!;
281+
282+
expect(profile.requestData.followRedirects, false);
283+
expect(profile.responseData.isRedirect, true);
284+
expect(profile.responseData.redirects, isEmpty);
285+
});
286+
});
287+
});
288+
}

pkgs/ok_http/example/integration_test/client_test.dart

+36-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:http_client_conformance_tests/http_client_conformance_tests.dart';
6+
import 'package:http_profile/http_profile.dart';
67
import 'package:integration_test/integration_test.dart';
78
import 'package:ok_http/ok_http.dart';
89
import 'package:test/test.dart';
@@ -15,13 +16,40 @@ void main() async {
1516

1617
Future<void> testConformance() async {
1718
group('ok_http client', () {
18-
testAll(
19-
OkHttpClient.new,
20-
canStreamRequestBody: false,
21-
preservesMethodCase: true,
22-
supportsFoldedHeaders: false,
23-
canSendCookieHeaders: true,
24-
canReceiveSetCookieHeaders: true,
25-
);
19+
group('profile enabled', () {
20+
final profile = HttpClientRequestProfile.profilingEnabled;
21+
HttpClientRequestProfile.profilingEnabled = true;
22+
23+
try {
24+
testAll(
25+
OkHttpClient.new,
26+
canStreamRequestBody: false,
27+
preservesMethodCase: true,
28+
supportsFoldedHeaders: false,
29+
canSendCookieHeaders: true,
30+
canReceiveSetCookieHeaders: true,
31+
);
32+
} finally {
33+
HttpClientRequestProfile.profilingEnabled = profile;
34+
}
35+
});
36+
37+
group('profile disabled', () {
38+
final profile = HttpClientRequestProfile.profilingEnabled;
39+
HttpClientRequestProfile.profilingEnabled = false;
40+
41+
try {
42+
testAll(
43+
OkHttpClient.new,
44+
canStreamRequestBody: false,
45+
preservesMethodCase: true,
46+
supportsFoldedHeaders: false,
47+
canSendCookieHeaders: true,
48+
canReceiveSetCookieHeaders: true,
49+
);
50+
} finally {
51+
HttpClientRequestProfile.profilingEnabled = profile;
52+
}
53+
});
2654
});
2755
}

0 commit comments

Comments
 (0)