From b7ec613ae2b1e6f074e1d6695b2d2762b4c0052c Mon Sep 17 00:00:00 2001 From: Anikate De <40452578+Anikate-De@users.noreply.github.com> Date: Mon, 24 Jun 2024 23:15:06 +0530 Subject: [PATCH] pkgs/ok_http: DevTools Networking Support. (#1242) --- pkgs/ok_http/CHANGELOG.md | 3 +- .../example/ok_http/RedirectInterceptor.kt | 19 +- .../integration_test/client_profile_test.dart | 288 ++++++++++++++++++ .../example/integration_test/client_test.dart | 44 ++- pkgs/ok_http/example/pubspec.yaml | 1 + pkgs/ok_http/jnigen.yaml | 1 + pkgs/ok_http/lib/src/jni/bindings.dart | 231 ++++++++++++-- pkgs/ok_http/lib/src/ok_http_client.dart | 117 +++++-- pkgs/ok_http/pubspec.yaml | 3 +- 9 files changed, 649 insertions(+), 58 deletions(-) create mode 100644 pkgs/ok_http/example/integration_test/client_profile_test.dart diff --git a/pkgs/ok_http/CHANGELOG.md b/pkgs/ok_http/CHANGELOG.md index f1c3a8b5d6..433fc4ec44 100644 --- a/pkgs/ok_http/CHANGELOG.md +++ b/pkgs/ok_http/CHANGELOG.md @@ -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. diff --git a/pkgs/ok_http/android/src/main/kotlin/com/example/ok_http/RedirectInterceptor.kt b/pkgs/ok_http/android/src/main/kotlin/com/example/ok_http/RedirectInterceptor.kt index 9f59e2fc3a..1aac8ea3ae 100644 --- a/pkgs/ok_http/android/src/main/kotlin/com/example/ok_http/RedirectInterceptor.kt +++ b/pkgs/ok_http/android/src/main/kotlin/com/example/ok_http/RedirectInterceptor.kt @@ -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) +} + + class RedirectInterceptor { companion object { @@ -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() @@ -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) diff --git a/pkgs/ok_http/example/integration_test/client_profile_test.dart b/pkgs/ok_http/example/integration_test/client_profile_test.dart new file mode 100644 index 0000000000..44975dd4b7 --- /dev/null +++ b/pkgs/ok_http/example/integration_test/client_profile_test.dart @@ -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(); + 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(); + 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); + }); + }); + }); +} diff --git a/pkgs/ok_http/example/integration_test/client_test.dart b/pkgs/ok_http/example/integration_test/client_test.dart index b167a8f329..a1d0e5d65b 100644 --- a/pkgs/ok_http/example/integration_test/client_test.dart +++ b/pkgs/ok_http/example/integration_test/client_test.dart @@ -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'; @@ -15,13 +16,40 @@ void main() async { Future 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; + } + }); }); } diff --git a/pkgs/ok_http/example/pubspec.yaml b/pkgs/ok_http/example/pubspec.yaml index de52329b4b..d452975f20 100644 --- a/pkgs/ok_http/example/pubspec.yaml +++ b/pkgs/ok_http/example/pubspec.yaml @@ -23,6 +23,7 @@ dev_dependencies: flutter_lints: ^3.0.0 http_client_conformance_tests: path: ../../http_client_conformance_tests/ + http_profile: ^0.1.0 integration_test: sdk: flutter test: ^1.23.1 diff --git a/pkgs/ok_http/jnigen.yaml b/pkgs/ok_http/jnigen.yaml index f4b43951ad..c9f06d183a 100644 --- a/pkgs/ok_http/jnigen.yaml +++ b/pkgs/ok_http/jnigen.yaml @@ -27,6 +27,7 @@ classes: - "okhttp3.ConnectionPool" - "okhttp3.Dispatcher" - "okhttp3.Cache" + - "com.example.ok_http.RedirectReceivedCallback" - "com.example.ok_http.RedirectInterceptor" - "com.example.ok_http.AsyncInputStreamReader" - "com.example.ok_http.DataCallback" diff --git a/pkgs/ok_http/lib/src/jni/bindings.dart b/pkgs/ok_http/lib/src/jni/bindings.dart index 8b76334e8c..05ab7b830e 100644 --- a/pkgs/ok_http/lib/src/jni/bindings.dart +++ b/pkgs/ok_http/lib/src/jni/bindings.dart @@ -1087,8 +1087,8 @@ class RequestBody_Companion extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Int32 )>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1211,8 +1211,8 @@ class RequestBody_Companion extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Int32 )>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1275,7 +1275,7 @@ class RequestBody_Companion extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64 + ffi.Int32 )>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1363,7 +1363,7 @@ class RequestBody_Companion extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64 + ffi.Int32 )>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1706,8 +1706,8 @@ class RequestBody extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Int32 )>)>>("globalEnv_CallStaticObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1830,8 +1830,8 @@ class RequestBody extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Int32 )>)>>("globalEnv_CallStaticObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1894,7 +1894,7 @@ class RequestBody extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64 + ffi.Int32 )>)>>("globalEnv_CallStaticObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -1982,7 +1982,7 @@ class RequestBody extends jni.JObject { ( ffi.Pointer, ffi.Pointer, - ffi.Int64 + ffi.Int32 )>)>>("globalEnv_CallStaticObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -2178,7 +2178,7 @@ class Response_Builder extends jni.JObject { static final _code = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Int32,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -2584,7 +2584,7 @@ class Response extends jni.JObject { ffi.Pointer, ffi.Pointer, ffi.Pointer, - ffi.Int64, + ffi.Int32, ffi.Pointer, ffi.Pointer, ffi.Pointer, @@ -3341,8 +3341,8 @@ class ResponseBody_BomAwareReader extends jni.JObject { ffi.VarArgs< ( ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Int32 )>)>>("globalEnv_CallIntMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, @@ -4539,7 +4539,7 @@ class OkHttpClient_Builder extends jni.JObject { static final _retryOnConnectionFailure = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Uint8,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -4590,7 +4590,7 @@ class OkHttpClient_Builder extends jni.JObject { static final _followRedirects = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Uint8,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -4613,7 +4613,7 @@ class OkHttpClient_Builder extends jni.JObject { static final _followSslRedirects = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Uint8,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -7571,7 +7571,7 @@ class Headers extends jni.JObject { static final _name = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Int32,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -7593,7 +7593,7 @@ class Headers extends jni.JObject { static final _value = ProtectedJniExtensions.lookup< ffi.NativeFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallObjectMethod") + ffi.VarArgs<(ffi.Int32,)>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -8188,7 +8188,7 @@ class ConnectionPool extends jni.JObject { jni.JMethodIDPtr, ffi.VarArgs< ( - ffi.Int64, + ffi.Int32, ffi.Int64, ffi.Pointer )>)>>("globalEnv_NewObject") @@ -8400,7 +8400,7 @@ class Dispatcher extends jni.JObject { jni.JThrowablePtr Function( ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallVoidMethod") + ffi.VarArgs<(ffi.Int32,)>)>>("globalEnv_CallVoidMethod") .asFunction< jni.JThrowablePtr Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -8448,7 +8448,7 @@ class Dispatcher extends jni.JObject { jni.JThrowablePtr Function( ffi.Pointer, jni.JMethodIDPtr, - ffi.VarArgs<(ffi.Int64,)>)>>("globalEnv_CallVoidMethod") + ffi.VarArgs<(ffi.Int32,)>)>>("globalEnv_CallVoidMethod") .asFunction< jni.JThrowablePtr Function( ffi.Pointer, jni.JMethodIDPtr, int)>(); @@ -9438,6 +9438,174 @@ final class $CacheType extends jni.JObjType { } } +/// from: com.example.ok_http.RedirectReceivedCallback +class RedirectReceivedCallback extends jni.JObject { + @override + late final jni.JObjType $type = type; + + RedirectReceivedCallback.fromReference( + jni.JReference reference, + ) : super.fromReference(reference); + + static final _class = + jni.JClass.forName(r"com/example/ok_http/RedirectReceivedCallback"); + + /// The type which includes information such as the signature of this class. + static const type = $RedirectReceivedCallbackType(); + static final _id_onRedirectReceived = _class.instanceMethodId( + r"onRedirectReceived", + r"(Lokhttp3/Response;Ljava/lang/String;)V", + ); + + static final _onRedirectReceived = ProtectedJniExtensions.lookup< + ffi.NativeFunction< + jni.JThrowablePtr Function( + ffi.Pointer, + jni.JMethodIDPtr, + ffi.VarArgs< + ( + ffi.Pointer, + ffi.Pointer + )>)>>("globalEnv_CallVoidMethod") + .asFunction< + jni.JThrowablePtr Function(ffi.Pointer, jni.JMethodIDPtr, + ffi.Pointer, ffi.Pointer)>(); + + /// from: public abstract void onRedirectReceived(okhttp3.Response response, java.lang.String string) + void onRedirectReceived( + Response response, + jni.JString string, + ) { + _onRedirectReceived( + reference.pointer, + _id_onRedirectReceived as jni.JMethodIDPtr, + response.reference.pointer, + string.reference.pointer) + .check(); + } + + /// Maps a specific port to the implemented interface. + static final Map _$impls = {}; + ReceivePort? _$p; + + static jni.JObjectPtr _$invoke( + int port, + jni.JObjectPtr descriptor, + jni.JObjectPtr args, + ) { + return _$invokeMethod( + port, + $MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final ffi.Pointer< + ffi.NativeFunction< + jni.JObjectPtr Function( + ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> + _$invokePointer = ffi.Pointer.fromFunction(_$invoke); + + static ffi.Pointer _$invokeMethod( + int $p, + $MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r"onRedirectReceived(Lokhttp3/Response;Ljava/lang/String;)V") { + _$impls[$p]!.onRedirectReceived( + $a[0].castTo(const $ResponseType(), releaseOriginal: true), + $a[1].castTo(const jni.JStringType(), releaseOriginal: true), + ); + return jni.nullptr; + } + } catch (e) { + return ProtectedJniExtensions.newDartException(e.toString()); + } + return jni.nullptr; + } + + factory RedirectReceivedCallback.implement( + $RedirectReceivedCallbackImpl $impl, + ) { + final $p = ReceivePort(); + final $x = RedirectReceivedCallback.fromReference( + ProtectedJniExtensions.newPortProxy( + r"com.example.ok_http.RedirectReceivedCallback", + $p, + _$invokePointer, + ), + ).._$p = $p; + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + $p.listen(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = $MethodInvocation.fromMessage($m as List); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + ProtectedJniExtensions.returnResult($i.result, $r); + }); + return $x; + } +} + +abstract interface class $RedirectReceivedCallbackImpl { + factory $RedirectReceivedCallbackImpl({ + required void Function(Response response, jni.JString string) + onRedirectReceived, + }) = _$RedirectReceivedCallbackImpl; + + void onRedirectReceived(Response response, jni.JString string); +} + +class _$RedirectReceivedCallbackImpl implements $RedirectReceivedCallbackImpl { + _$RedirectReceivedCallbackImpl({ + required void Function(Response response, jni.JString string) + onRedirectReceived, + }) : _onRedirectReceived = onRedirectReceived; + + final void Function(Response response, jni.JString string) + _onRedirectReceived; + + void onRedirectReceived(Response response, jni.JString string) { + return _onRedirectReceived(response, string); + } +} + +final class $RedirectReceivedCallbackType + extends jni.JObjType { + const $RedirectReceivedCallbackType(); + + @override + String get signature => r"Lcom/example/ok_http/RedirectReceivedCallback;"; + + @override + RedirectReceivedCallback fromReference(jni.JReference reference) => + RedirectReceivedCallback.fromReference(reference); + + @override + jni.JObjType get superType => const jni.JObjectType(); + + @override + final superCount = 1; + + @override + int get hashCode => ($RedirectReceivedCallbackType).hashCode; + + @override + bool operator ==(Object other) { + return other.runtimeType == ($RedirectReceivedCallbackType) && + other is $RedirectReceivedCallbackType; + } +} + /// from: com.example.ok_http.RedirectInterceptor$Companion class RedirectInterceptor_Companion extends jni.JObject { @override @@ -9454,7 +9622,7 @@ class RedirectInterceptor_Companion extends jni.JObject { static const type = $RedirectInterceptor_CompanionType(); static final _id_addRedirectInterceptor = _class.instanceMethodId( r"addRedirectInterceptor", - r"(Lokhttp3/OkHttpClient$Builder;IZ)Lokhttp3/OkHttpClient$Builder;", + r"(Lokhttp3/OkHttpClient$Builder;IZLcom/example/ok_http/RedirectReceivedCallback;)Lokhttp3/OkHttpClient$Builder;", ); static final _addRedirectInterceptor = ProtectedJniExtensions.lookup< @@ -9465,26 +9633,29 @@ class RedirectInterceptor_Companion extends jni.JObject { ffi.VarArgs< ( ffi.Pointer, - ffi.Int64, - ffi.Int64 + ffi.Int32, + ffi.Uint8, + ffi.Pointer )>)>>("globalEnv_CallObjectMethod") .asFunction< jni.JniResult Function(ffi.Pointer, jni.JMethodIDPtr, - ffi.Pointer, int, int)>(); + ffi.Pointer, int, int, ffi.Pointer)>(); - /// from: public final okhttp3.OkHttpClient$Builder addRedirectInterceptor(okhttp3.OkHttpClient$Builder builder, int i, boolean z) + /// from: public final okhttp3.OkHttpClient$Builder addRedirectInterceptor(okhttp3.OkHttpClient$Builder builder, int i, boolean z, com.example.ok_http.RedirectReceivedCallback redirectReceivedCallback) /// The returned object must be released after use, by calling the [release] method. OkHttpClient_Builder addRedirectInterceptor( OkHttpClient_Builder builder, int i, bool z, + RedirectReceivedCallback redirectReceivedCallback, ) { return _addRedirectInterceptor( reference.pointer, _id_addRedirectInterceptor as jni.JMethodIDPtr, builder.reference.pointer, i, - z ? 1 : 0) + z ? 1 : 0, + redirectReceivedCallback.reference.pointer) .object(const $OkHttpClient_BuilderType()); } diff --git a/pkgs/ok_http/lib/src/ok_http_client.dart b/pkgs/ok_http/lib/src/ok_http_client.dart index 3ab3799d62..33b1bfd5eb 100644 --- a/pkgs/ok_http/lib/src/ok_http_client.dart +++ b/pkgs/ok_http/lib/src/ok_http_client.dart @@ -16,6 +16,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:http/http.dart'; +import 'package:http_profile/http_profile.dart'; import 'package:jni/jni.dart'; import 'jni/bindings.dart' as bindings; @@ -78,6 +79,22 @@ class OkHttpClient extends BaseClient { _isClosed = true; } + HttpClientRequestProfile? _createProfile(BaseRequest request) => + HttpClientRequestProfile.profile( + requestStartTime: DateTime.now(), + requestMethod: request.method, + requestUri: request.url.toString()); + + void addProfileError(HttpClientRequestProfile? profile, Exception error) { + if (profile != null) { + if (profile.requestData.endTime == null) { + profile.requestData.closeWithError(error.toString()); + } else { + profile.responseData.closeWithError(error.toString()); + } + } + } + @override Future send(BaseRequest request) async { if (_isClosed) { @@ -85,6 +102,25 @@ class OkHttpClient extends BaseClient { 'HTTP request failed. Client is already closed.', request.url); } + final profile = _createProfile(request); + profile?.connectionInfo = { + 'package': 'package:ok_http', + 'client': 'OkHttpClient', + }; + + profile?.requestData + ?..contentLength = request.contentLength + ..followRedirects = request.followRedirects + ..headersCommaValues = request.headers + ..maxRedirects = request.maxRedirects; + + if (profile != null && request.contentLength != null) { + profile.requestData.headersListValues = { + 'Content-Length': ['${request.contentLength}'], + ...profile.requestData.headers! + }; + } + var requestUrl = request.url.toString(); var requestHeaders = request.headers; var requestMethod = request.method; @@ -92,6 +128,9 @@ class OkHttpClient extends BaseClient { var maxRedirects = request.maxRedirects; var followRedirects = request.followRedirects; + profile?.requestData.bodySink.add(requestBody); + var profileRespClosed = false; + final responseCompleter = Completer(); var reqBuilder = bindings.Request_Builder().url1(requestUrl.toJString()); @@ -123,9 +162,20 @@ class OkHttpClient extends BaseClient { // (Since OkHttp sets a hard limit of 20 redirects.) // https://github.com/square/okhttp/blob/54238b4c713080c3fd32fb1a070fb5d6814c9a09/okhttp/src/main/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt#L350 final reqConfiguredClient = bindings.RedirectInterceptor.Companion - .addRedirectInterceptor(_client.newBuilder().followRedirects(false), - maxRedirects, followRedirects) - .build(); + .addRedirectInterceptor( + _client.newBuilder().followRedirects(false), + maxRedirects, + followRedirects, bindings.RedirectReceivedCallback.implement( + bindings.$RedirectReceivedCallbackImpl( + onRedirectReceived: (response, newLocation) { + profile?.responseData.addRedirect(HttpProfileRedirectData( + statusCode: response.code(), + method: + response.request().method().toDartString(releaseOriginal: true), + location: newLocation.toDartString(releaseOriginal: true), + )); + }, + ))).build(); // `enqueue()` schedules the request to be executed in the future. // https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/enqueue.html @@ -161,19 +211,30 @@ class OkHttpClient extends BaseClient { responseBodyByteStream, bindings.DataCallback.implement( bindings.$DataCallbackImpl( - onDataRead: (JArray data) { - respBodyStreamController.sink.add(data.toUint8List()); + onDataRead: (JArray bytesRead) { + var data = bytesRead.toUint8List(); + + respBodyStreamController.sink.add(data); + profile?.responseData.bodySink.add(data); }, - onFinished: () async { + onFinished: () { reader.shutdown(); - await respBodyStreamController.sink.close(); + respBodyStreamController.sink.close(); + if (!profileRespClosed) { + profile?.responseData.close(); + profileRespClosed = true; + } }, - onError: (iOException) async { - respBodyStreamController.sink.addError( - ClientException(iOException.toString(), request.url)); + onError: (iOException) { + var exception = + ClientException(iOException.toString(), request.url); + + respBodyStreamController.sink.addError(exception); + addProfileError(profile, exception); + profileRespClosed = true; reader.shutdown(); - await respBodyStreamController.sink.close(); + respBodyStreamController.sink.close(); }, ), )); @@ -188,15 +249,26 @@ class OkHttpClient extends BaseClient { contentLength: contentLength, isRedirect: response.isRedirect(), )); + + profile?.requestData.close(); + profile?.responseData + ?..contentLength = contentLength + ..headersCommaValues = responseHeaders + ..isRedirect = response.isRedirect() + ..reasonPhrase = + response.message().toDartString(releaseOriginal: true) + ..startTime = DateTime.now() + ..statusCode = response.code(); }, onFailure: (bindings.Call call, JObject ioException) { - if (ioException.toString().contains('Redirect limit exceeded')) { - responseCompleter.completeError( - ClientException('Redirect limit exceeded', request.url)); - return; + var msg = ioException.toString(); + if (msg.contains('Redirect limit exceeded')) { + msg = 'Redirect limit exceeded'; } - responseCompleter.completeError( - ClientException(ioException.toString(), request.url)); + var exception = ClientException(msg, request.url); + responseCompleter.completeError(exception); + addProfileError(profile, exception); + profileRespClosed = true; }, ))); @@ -204,6 +276,17 @@ class OkHttpClient extends BaseClient { } } +/// A test-only class that makes the [HttpClientRequestProfile] data available. +class OkHttpClientWithProfile extends OkHttpClient { + HttpClientRequestProfile? profile; + + @override + HttpClientRequestProfile? _createProfile(BaseRequest request) => + profile = super._createProfile(request); + + OkHttpClientWithProfile() : super(); +} + extension on Uint8List { JArray toJArray() => JArray(jbyte.type, length)..setRange(0, length, this); diff --git a/pkgs/ok_http/pubspec.yaml b/pkgs/ok_http/pubspec.yaml index 8b217d23d4..2ea7a6a665 100644 --- a/pkgs/ok_http/pubspec.yaml +++ b/pkgs/ok_http/pubspec.yaml @@ -7,12 +7,13 @@ publish_to: none environment: sdk: ^3.4.0 - flutter: '>=3.22.0' + flutter: ">=3.22.0" dependencies: flutter: sdk: flutter http: ^1.2.1 + http_profile: ^0.1.0 jni: ^0.9.2 plugin_platform_interface: ^2.0.2