Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkgs/ok_http: DevTools Networking Support. #1242

Merged
merged 6 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkgs/ok_http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## 0.1.0-wip

- 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)
- `ok_http` can now send asynchronous requests
- `ok_http` can now send asynchronous requests and stream response bodies.
- Add [DevTools Network View](https://docs.flutter.dev/tools/devtools/network) support.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,19 @@ package com.example.ok_http

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException

/**
* Callback interface utilized by the [RedirectInterceptor].
*
* Allows Dart code to operate upon the intermediate redirect responses.
*/
interface RedirectReceivedCallback {
fun onRedirectReceived(response: Response, location: String)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow-up PR, would it make sense to migrate the redirect logic to Dart? Actually, would it make sense to make interceptors available in Dart?

Taking a step back, maybe you'd want to expose OkHttpClient or a wrapper around that to Dart?

With that users can control caching, connection pools, DNS, etc.

I'm not sure what the best way to do that is though.

Copy link
Contributor Author

@Anikate-De Anikate-De Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About migrating the redirection logic to Dart, there are issues with threading while implementing Interceptor.
Hossein and I have been trying to work our way around this issue, but so far, RedirectInterceptor stands out as an example.


About exposing configurations to users, I had planned to provide high-level functions (and constructor parameters) within OkHttpClient allowing users to tweak the client according to their requirements.

For example, if a user wants to alter the timeout for a request, they can either pass the timeout duration in the constructor or in the function:

class OkHttpClient extends BaseClient {
  late bindings.OkHttpClient _client;
  OkHttpClient([Duration? timeoutDuration]) {
    // creates a client based on the timeout and assigns it to _client
  }
  void setTimeout(Duration timeoutDuration) {
    // uses a new builder from `_client` and edits its timeout
  }

But yes, a wrapper sounds good! We can use the architecture that CronetEngine has.

}


class RedirectInterceptor {
companion object {

Expand All @@ -26,7 +37,10 @@ class RedirectInterceptor {
* @return OkHttpClient.Builder
*/
fun addRedirectInterceptor(
clientBuilder: OkHttpClient.Builder, maxRedirects: Int, followRedirects: Boolean
clientBuilder: OkHttpClient.Builder,
maxRedirects: Int,
followRedirects: Boolean,
redirectCallback: RedirectReceivedCallback,
): OkHttpClient.Builder {
return clientBuilder.addInterceptor(Interceptor { chain ->
var req = chain.request()
Expand All @@ -39,6 +53,9 @@ class RedirectInterceptor {
}

val location = response.header("location") ?: break

redirectCallback.onRedirectReceived(response, location)

req = req.newBuilder().url(location).build()
response.close()
response = chain.proceed(req)
Expand Down
288 changes: 288 additions & 0 deletions pkgs/ok_http/example/integration_test/client_profile_test.dart
Anikate-De marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:http_profile/http_profile.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ok_http/ok_http.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('profile', () {
final profilingEnabled = HttpClientRequestProfile.profilingEnabled;

setUpAll(() {
HttpClientRequestProfile.profilingEnabled = true;
});

tearDownAll(() {
HttpClientRequestProfile.profilingEnabled = profilingEnabled;
});

group('POST', () {
late HttpServer successServer;
late Uri successServerUri;
late HttpClientRequestProfile profile;

setUpAll(() async {
successServer = (await HttpServer.bind('localhost', 0))
..listen((request) async {
await request.drain<void>();
request.response.headers.set('Content-Type', 'text/plain');
request.response.headers.set('Content-Length', '11');
request.response.write('Hello World');
await request.response.close();
});
successServerUri = Uri.http('localhost:${successServer.port}');
final client = OkHttpClientWithProfile();
await client.post(successServerUri,
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
profile = client.profile!;
});
tearDownAll(() {
successServer.close();
});

test('profile attributes', () {
expect(profile.events, isEmpty);
expect(profile.requestMethod, 'POST');
expect(profile.requestUri, successServerUri.toString());
expect(
profile.connectionInfo, containsPair('package', 'package:ok_http'));
});

test('request attributes', () {
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
expect(profile.requestData.contentLength, 2);
expect(profile.requestData.endTime, isNotNull);
expect(profile.requestData.error, isNull);
expect(
profile.requestData.headers, containsPair('Content-Length', ['2']));
expect(profile.requestData.headers,
containsPair('Content-Type', ['text/plain; charset=utf-8']));
expect(profile.requestData.persistentConnection, isNull);
expect(profile.requestData.proxyDetails, isNull);
expect(profile.requestData.startTime, isNotNull);
});

test('response attributes', () {
expect(profile.responseData.bodyBytes, 'Hello World'.codeUnits);
expect(profile.responseData.compressionState, isNull);
expect(profile.responseData.contentLength, 11);
expect(profile.responseData.endTime, isNotNull);
expect(profile.responseData.error, isNull);
expect(profile.responseData.headers,
containsPair('content-type', ['text/plain']));
expect(profile.responseData.headers,
containsPair('content-length', ['11']));
expect(profile.responseData.isRedirect, false);
expect(profile.responseData.persistentConnection, isNull);
expect(profile.responseData.reasonPhrase, 'OK');
expect(profile.responseData.redirects, isEmpty);
expect(profile.responseData.startTime, isNotNull);
expect(profile.responseData.statusCode, 200);
});
});

group('failed POST request', () {
late HttpClientRequestProfile profile;

setUpAll(() async {
final client = OkHttpClientWithProfile();
try {
await client.post(Uri.http('thisisnotahost'),
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
fail('expected exception');
} on ClientException {
// Expected exception.
}
profile = client.profile!;
});

test('profile attributes', () {
expect(profile.events, isEmpty);
expect(profile.requestMethod, 'POST');
expect(profile.requestUri, 'http://thisisnotahost');
expect(
profile.connectionInfo, containsPair('package', 'package:ok_http'));
});

test('request attributes', () {
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
expect(profile.requestData.contentLength, 2);
expect(profile.requestData.endTime, isNotNull);
expect(profile.requestData.error, startsWith('ClientException:'));
expect(
profile.requestData.headers, containsPair('Content-Length', ['2']));
expect(profile.requestData.headers,
containsPair('Content-Type', ['text/plain; charset=utf-8']));
expect(profile.requestData.persistentConnection, isNull);
expect(profile.requestData.proxyDetails, isNull);
expect(profile.requestData.startTime, isNotNull);
});

test('response attributes', () {
expect(profile.responseData.bodyBytes, isEmpty);
expect(profile.responseData.compressionState, isNull);
expect(profile.responseData.contentLength, isNull);
expect(profile.responseData.endTime, isNull);
expect(profile.responseData.error, isNull);
expect(profile.responseData.headers, isNull);
expect(profile.responseData.isRedirect, isNull);
expect(profile.responseData.persistentConnection, isNull);
expect(profile.responseData.reasonPhrase, isNull);
expect(profile.responseData.redirects, isEmpty);
expect(profile.responseData.startTime, isNull);
expect(profile.responseData.statusCode, isNull);
});
});

group('failed POST response', () {
late HttpServer successServer;
late Uri successServerUri;
late HttpClientRequestProfile profile;

setUpAll(() async {
successServer = (await HttpServer.bind('localhost', 0))
..listen((request) async {
await request.drain<void>();
request.response.headers.set('Content-Type', 'text/plain');
request.response.headers.set('Content-Length', '11');
final socket = await request.response.detachSocket();
await socket.close();
});
successServerUri = Uri.http('localhost:${successServer.port}');
final client = OkHttpClientWithProfile();

try {
await client.post(successServerUri,
headers: {'Content-Type': 'text/plain'}, body: 'Hi');
fail('expected exception');
} on ClientException {
// Expected exception.
}
profile = client.profile!;
});
tearDownAll(() {
successServer.close();
});

test('profile attributes', () {
expect(profile.events, isEmpty);
expect(profile.requestMethod, 'POST');
expect(profile.requestUri, successServerUri.toString());
expect(
profile.connectionInfo, containsPair('package', 'package:ok_http'));
});

test('request attributes', () {
expect(profile.requestData.bodyBytes, 'Hi'.codeUnits);
expect(profile.requestData.contentLength, 2);
expect(profile.requestData.endTime, isNotNull);
expect(profile.requestData.error, isNull);
expect(
profile.requestData.headers, containsPair('Content-Length', ['2']));
expect(profile.requestData.headers,
containsPair('Content-Type', ['text/plain; charset=utf-8']));
expect(profile.requestData.persistentConnection, isNull);
expect(profile.requestData.proxyDetails, isNull);
expect(profile.requestData.startTime, isNotNull);
});

test('response attributes', () {
expect(profile.responseData.bodyBytes, isEmpty);
expect(profile.responseData.compressionState, isNull);
expect(profile.responseData.contentLength, 11);
expect(profile.responseData.endTime, isNotNull);
expect(profile.responseData.error, startsWith('ClientException:'));
expect(profile.responseData.headers,
containsPair('content-type', ['text/plain']));
expect(profile.responseData.headers,
containsPair('content-length', ['11']));
expect(profile.responseData.isRedirect, false);
expect(profile.responseData.persistentConnection, isNull);
expect(profile.responseData.reasonPhrase, 'OK');
expect(profile.responseData.redirects, isEmpty);
expect(profile.responseData.startTime, isNotNull);
expect(profile.responseData.statusCode, 200);
});
});

group('redirects', () {
late HttpServer successServer;
late Uri successServerUri;
late HttpClientRequestProfile profile;

setUpAll(() async {
successServer = (await HttpServer.bind('localhost', 0))
..listen((request) async {
if (request.requestedUri.pathSegments.isEmpty) {
unawaited(request.response.close());
} else {
final n = int.parse(request.requestedUri.pathSegments.last);
final nextPath = n - 1 == 0 ? '' : '${n - 1}';
unawaited(request.response
.redirect(successServerUri.replace(path: '/$nextPath')));
}
});
successServerUri = Uri.http('localhost:${successServer.port}');
});
tearDownAll(() {
successServer.close();
});

test('no redirects', () async {
final client = OkHttpClientWithProfile();
await client.get(successServerUri);
profile = client.profile!;

expect(profile.responseData.redirects, isEmpty);
});

test('follow redirects', () async {
final client = OkHttpClientWithProfile();
await client.send(Request('GET', successServerUri.replace(path: '/3'))
..followRedirects = true
..maxRedirects = 4);
profile = client.profile!;

expect(profile.requestData.followRedirects, true);
expect(profile.requestData.maxRedirects, 4);
expect(profile.responseData.isRedirect, false);

expect(profile.responseData.redirects, [
HttpProfileRedirectData(
statusCode: 302,
method: 'GET',
location: successServerUri.replace(path: '/2').toString()),
HttpProfileRedirectData(
statusCode: 302,
method: 'GET',
location: successServerUri.replace(path: '/1').toString()),
HttpProfileRedirectData(
statusCode: 302,
method: 'GET',
location: successServerUri.replace(path: '/').toString(),
)
]);
});

test('no follow redirects', () async {
final client = OkHttpClientWithProfile();
await client.send(Request('GET', successServerUri.replace(path: '/3'))
..followRedirects = false);
profile = client.profile!;

expect(profile.requestData.followRedirects, false);
expect(profile.responseData.isRedirect, true);
expect(profile.responseData.redirects, isEmpty);
});
});
});
}
44 changes: 36 additions & 8 deletions pkgs/ok_http/example/integration_test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'package:http_client_conformance_tests/http_client_conformance_tests.dart';
import 'package:http_profile/http_profile.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ok_http/ok_http.dart';
import 'package:test/test.dart';
Expand All @@ -15,13 +16,40 @@ void main() async {

Future<void> testConformance() async {
group('ok_http client', () {
testAll(
OkHttpClient.new,
canStreamRequestBody: false,
preservesMethodCase: true,
supportsFoldedHeaders: false,
canSendCookieHeaders: true,
canReceiveSetCookieHeaders: true,
);
group('profile enabled', () {
final profile = HttpClientRequestProfile.profilingEnabled;
HttpClientRequestProfile.profilingEnabled = true;

try {
testAll(
OkHttpClient.new,
canStreamRequestBody: false,
preservesMethodCase: true,
supportsFoldedHeaders: false,
canSendCookieHeaders: true,
canReceiveSetCookieHeaders: true,
);
} finally {
HttpClientRequestProfile.profilingEnabled = profile;
}
});

group('profile disabled', () {
final profile = HttpClientRequestProfile.profilingEnabled;
HttpClientRequestProfile.profilingEnabled = false;

try {
testAll(
OkHttpClient.new,
canStreamRequestBody: false,
preservesMethodCase: true,
supportsFoldedHeaders: false,
canSendCookieHeaders: true,
canReceiveSetCookieHeaders: true,
);
} finally {
HttpClientRequestProfile.profilingEnabled = profile;
}
});
});
}
Loading