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] Add functionality to accept and configure redirects. #1230

Merged
merged 4 commits into from
Jun 11, 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
9 changes: 9 additions & 0 deletions pkgs/ok_http/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,20 @@ rootProject.allprojects {
}

apply plugin: "com.android.library"
apply plugin: 'kotlin-android'

android {
if (project.android.hasProperty("namespace")) {
namespace = "com.example.ok_http"
}

kotlinOptions {
jvmTarget = '1.8'
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

// Bumping the plugin compileSdk version requires all clients of this plugin
// to bump the version in their app.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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.

// To cause a request failure [with a suitable message] due to too many redirects,
// we need to throw an IOException. This cannot be done using Dart JNI bindings,
// which lead to a deadlock and eventually a `java.net.SocketTimeoutException`.
// https://github.com/dart-lang/native/issues/561
Anikate-De marked this conversation as resolved.
Show resolved Hide resolved

package com.example.ok_http

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

class RedirectInterceptor {
companion object {

/**
* Adds a redirect interceptor to the OkHttpClient.Builder
*
* @param clientBuilder The `OkHttpClient.Builder` to add the interceptor to
* @param maxRedirects The maximum number of redirects to follow
* @param followRedirects Whether to follow redirects
*
* @return OkHttpClient.Builder
*/
fun addRedirectInterceptor(
clientBuilder: OkHttpClient.Builder, maxRedirects: Int, followRedirects: Boolean
): OkHttpClient.Builder {
return clientBuilder.addInterceptor(Interceptor { chain ->
var req = chain.request()
var response = chain.proceed(req)
var redirectCount = 0

while (response.isRedirect && followRedirects) {
if (redirectCount >= maxRedirects) {
throw IOException("Redirect limit exceeded")
}

val location = response.header("location") ?: break
req = req.newBuilder().url(location).build()
response.close()
response = chain.proceed(req)
redirectCount++
}

response
})
}
}
}
2 changes: 2 additions & 0 deletions pkgs/ok_http/example/integration_test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ Future<void> testConformance() async {
testResponseHeaders(OkHttpClient(), supportsFoldedHeaders: false);
testResponseStatusLine(OkHttpClient());
testCompressedResponseBody(OkHttpClient());
testRedirect(OkHttpClient());
testServerErrors(OkHttpClient());
testClose(OkHttpClient.new);
testIsolate(OkHttpClient.new);
testRequestCookies(OkHttpClient(), canSendCookieHeaders: true);
testResponseCookies(OkHttpClient(), canReceiveSetCookieHeaders: true);
});
}
1 change: 1 addition & 0 deletions pkgs/ok_http/jnigen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ classes:
- "okhttp3.ConnectionPool"
- "okhttp3.Dispatcher"
- "okhttp3.Cache"
- "com.example.ok_http.RedirectInterceptor"

# Exclude the deprecated methods listed below
# They cause syntax errors during the `dart format` step of JNIGen.
Expand Down
181 changes: 181 additions & 0 deletions pkgs/ok_http/lib/src/jni/bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9437,3 +9437,184 @@ final class $CacheType extends jni.JObjType<Cache> {
return other.runtimeType == ($CacheType) && other is $CacheType;
}
}

/// from: com.example.ok_http.RedirectInterceptor$Companion
class RedirectInterceptor_Companion extends jni.JObject {
@override
late final jni.JObjType<RedirectInterceptor_Companion> $type = type;

RedirectInterceptor_Companion.fromReference(
jni.JReference reference,
) : super.fromReference(reference);

static final _class =
jni.JClass.forName(r"com/example/ok_http/RedirectInterceptor$Companion");

/// The type which includes information such as the signature of this class.
static const type = $RedirectInterceptor_CompanionType();
static final _id_addRedirectInterceptor = _class.instanceMethodId(
r"addRedirectInterceptor",
r"(Lokhttp3/OkHttpClient$Builder;IZ)Lokhttp3/OkHttpClient$Builder;",
);

static final _addRedirectInterceptor = ProtectedJniExtensions.lookup<
ffi.NativeFunction<
jni.JniResult Function(
ffi.Pointer<ffi.Void>,
jni.JMethodIDPtr,
ffi.VarArgs<
(
ffi.Pointer<ffi.Void>,
ffi.Int64,
ffi.Int64
)>)>>("globalEnv_CallObjectMethod")
.asFunction<
jni.JniResult Function(ffi.Pointer<ffi.Void>, jni.JMethodIDPtr,
ffi.Pointer<ffi.Void>, int, int)>();

/// from: public final okhttp3.OkHttpClient$Builder addRedirectInterceptor(okhttp3.OkHttpClient$Builder builder, int i, boolean z)
/// The returned object must be released after use, by calling the [release] method.
OkHttpClient_Builder addRedirectInterceptor(
OkHttpClient_Builder builder,
int i,
bool z,
) {
return _addRedirectInterceptor(
reference.pointer,
_id_addRedirectInterceptor as jni.JMethodIDPtr,
builder.reference.pointer,
i,
z ? 1 : 0)
.object(const $OkHttpClient_BuilderType());
}

static final _id_new0 = _class.constructorId(
r"(Lkotlin/jvm/internal/DefaultConstructorMarker;)V",
);

static final _new0 = ProtectedJniExtensions.lookup<
ffi.NativeFunction<
jni.JniResult Function(
ffi.Pointer<ffi.Void>,
jni.JMethodIDPtr,
ffi.VarArgs<(ffi.Pointer<ffi.Void>,)>)>>(
"globalEnv_NewObject")
.asFunction<
jni.JniResult Function(ffi.Pointer<ffi.Void>, jni.JMethodIDPtr,
ffi.Pointer<ffi.Void>)>();

/// from: public void <init>(kotlin.jvm.internal.DefaultConstructorMarker defaultConstructorMarker)
/// The returned object must be released after use, by calling the [release] method.
factory RedirectInterceptor_Companion(
jni.JObject defaultConstructorMarker,
) {
return RedirectInterceptor_Companion.fromReference(_new0(
_class.reference.pointer,
_id_new0 as jni.JMethodIDPtr,
defaultConstructorMarker.reference.pointer)
.reference);
}
}

final class $RedirectInterceptor_CompanionType
extends jni.JObjType<RedirectInterceptor_Companion> {
const $RedirectInterceptor_CompanionType();

@override
String get signature =>
r"Lcom/example/ok_http/RedirectInterceptor$Companion;";

@override
RedirectInterceptor_Companion fromReference(jni.JReference reference) =>
RedirectInterceptor_Companion.fromReference(reference);

@override
jni.JObjType get superType => const jni.JObjectType();

@override
final superCount = 1;

@override
int get hashCode => ($RedirectInterceptor_CompanionType).hashCode;

@override
bool operator ==(Object other) {
return other.runtimeType == ($RedirectInterceptor_CompanionType) &&
other is $RedirectInterceptor_CompanionType;
}
}

/// from: com.example.ok_http.RedirectInterceptor
class RedirectInterceptor extends jni.JObject {
@override
late final jni.JObjType<RedirectInterceptor> $type = type;

RedirectInterceptor.fromReference(
jni.JReference reference,
) : super.fromReference(reference);

static final _class =
jni.JClass.forName(r"com/example/ok_http/RedirectInterceptor");

/// The type which includes information such as the signature of this class.
static const type = $RedirectInterceptorType();
static final _id_Companion = _class.staticFieldId(
r"Companion",
r"Lcom/example/ok_http/RedirectInterceptor$Companion;",
);

/// from: static public final com.example.ok_http.RedirectInterceptor$Companion Companion
/// The returned object must be released after use, by calling the [release] method.
static RedirectInterceptor_Companion get Companion =>
_id_Companion.get(_class, const $RedirectInterceptor_CompanionType());

static final _id_new0 = _class.constructorId(
r"()V",
);

static final _new0 = ProtectedJniExtensions.lookup<
ffi.NativeFunction<
jni.JniResult Function(
ffi.Pointer<ffi.Void>,
jni.JMethodIDPtr,
)>>("globalEnv_NewObject")
.asFunction<
jni.JniResult Function(
ffi.Pointer<ffi.Void>,
jni.JMethodIDPtr,
)>();

/// from: public void <init>()
/// The returned object must be released after use, by calling the [release] method.
factory RedirectInterceptor() {
return RedirectInterceptor.fromReference(
_new0(_class.reference.pointer, _id_new0 as jni.JMethodIDPtr)
.reference);
}
}

final class $RedirectInterceptorType extends jni.JObjType<RedirectInterceptor> {
const $RedirectInterceptorType();

@override
String get signature => r"Lcom/example/ok_http/RedirectInterceptor;";

@override
RedirectInterceptor fromReference(jni.JReference reference) =>
RedirectInterceptor.fromReference(reference);

@override
jni.JObjType get superType => const jni.JObjectType();

@override
final superCount = 1;

@override
int get hashCode => ($RedirectInterceptorType).hashCode;

@override
bool operator ==(Object other) {
return other.runtimeType == ($RedirectInterceptorType) &&
other is $RedirectInterceptorType;
}
}
23 changes: 22 additions & 1 deletion pkgs/ok_http/lib/src/ok_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class OkHttpClient extends BaseClient {
var requestHeaders = request.headers;
var requestMethod = request.method;
var requestBody = await request.finalize().toBytes();
var maxRedirects = request.maxRedirects;
var followRedirects = request.followRedirects;

final responseCompleter = Completer<StreamedResponse>();

Expand All @@ -112,9 +114,22 @@ class OkHttpClient extends BaseClient {
okReqBody,
);

// To configure the client per-request, we create a new client with the
// builder associated with `_client`.
// They share the same connection pool and dispatcher.
// https://square.github.io/okhttp/recipes/#per-call-configuration-kt-java
//
// `followRedirects` is set to `false` to handle redirects manually.
// (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();

// `enqueue()` schedules the request to be executed in the future.
// https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/enqueue.html
_client
reqConfiguredClient
.newCall(reqBuilder.build())
.enqueue(bindings.Callback.implement(bindings.$CallbackImpl(
onResponse: (bindings.Call call, bindings.Response response) {
Expand Down Expand Up @@ -159,9 +174,15 @@ class OkHttpClient extends BaseClient {
headers: responseHeaders,
request: request,
contentLength: contentLength,
isRedirect: response.isRedirect(),
));
},
onFailure: (bindings.Call call, JObject ioException) {
if (ioException.toString().contains('Redirect limit exceeded')) {
responseCompleter.completeError(
ClientException('Redirect limit exceeded', request.url));
return;
}
responseCompleter.completeError(
ClientException(ioException.toString(), request.url));
},
Expand Down