Skip to content

Commit f8d5bf8

Browse files
authored
[pkgs/ok_http] Add functionality to accept and configure redirects. (#1230)
1 parent 93ff4a9 commit f8d5bf8

File tree

6 files changed

+267
-1
lines changed

6 files changed

+267
-1
lines changed

pkgs/ok_http/android/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ rootProject.allprojects {
2626
}
2727

2828
apply plugin: "com.android.library"
29+
apply plugin: 'kotlin-android'
2930

3031
android {
3132
if (project.android.hasProperty("namespace")) {
3233
namespace = "com.example.ok_http"
3334
}
35+
36+
kotlinOptions {
37+
jvmTarget = '1.8'
38+
}
39+
40+
sourceSets {
41+
main.java.srcDirs += 'src/main/kotlin'
42+
}
3443

3544
// Bumping the plugin compileSdk version requires all clients of this plugin
3645
// to bump the version in their app.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
// To cause a request failure [with a suitable message] due to too many redirects,
6+
// we need to throw an IOException. This cannot be done using Dart JNI bindings,
7+
// which lead to a deadlock and eventually a `java.net.SocketTimeoutException`.
8+
// https://github.com/dart-lang/native/issues/561
9+
10+
package com.example.ok_http
11+
12+
import okhttp3.Interceptor
13+
import okhttp3.OkHttpClient
14+
import java.io.IOException
15+
16+
class RedirectInterceptor {
17+
companion object {
18+
19+
/**
20+
* Adds a redirect interceptor to the OkHttpClient.Builder
21+
*
22+
* @param clientBuilder The `OkHttpClient.Builder` to add the interceptor to
23+
* @param maxRedirects The maximum number of redirects to follow
24+
* @param followRedirects Whether to follow redirects
25+
*
26+
* @return OkHttpClient.Builder
27+
*/
28+
fun addRedirectInterceptor(
29+
clientBuilder: OkHttpClient.Builder, maxRedirects: Int, followRedirects: Boolean
30+
): OkHttpClient.Builder {
31+
return clientBuilder.addInterceptor(Interceptor { chain ->
32+
var req = chain.request()
33+
var response = chain.proceed(req)
34+
var redirectCount = 0
35+
36+
while (response.isRedirect && followRedirects) {
37+
if (redirectCount >= maxRedirects) {
38+
throw IOException("Redirect limit exceeded")
39+
}
40+
41+
val location = response.header("location") ?: break
42+
req = req.newBuilder().url(location).build()
43+
response.close()
44+
response = chain.proceed(req)
45+
redirectCount++
46+
}
47+
48+
response
49+
})
50+
}
51+
}
52+
}

pkgs/ok_http/example/integration_test/client_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ Future<void> testConformance() async {
2222
testResponseHeaders(OkHttpClient(), supportsFoldedHeaders: false);
2323
testResponseStatusLine(OkHttpClient());
2424
testCompressedResponseBody(OkHttpClient());
25+
testRedirect(OkHttpClient());
2526
testServerErrors(OkHttpClient());
2627
testClose(OkHttpClient.new);
2728
testIsolate(OkHttpClient.new);
29+
testRequestCookies(OkHttpClient(), canSendCookieHeaders: true);
2830
testResponseCookies(OkHttpClient(), canReceiveSetCookieHeaders: true);
2931
});
3032
}

pkgs/ok_http/jnigen.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ classes:
2727
- "okhttp3.ConnectionPool"
2828
- "okhttp3.Dispatcher"
2929
- "okhttp3.Cache"
30+
- "com.example.ok_http.RedirectInterceptor"
3031

3132
# Exclude the deprecated methods listed below
3233
# They cause syntax errors during the `dart format` step of JNIGen.

pkgs/ok_http/lib/src/jni/bindings.dart

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9437,3 +9437,184 @@ final class $CacheType extends jni.JObjType<Cache> {
94379437
return other.runtimeType == ($CacheType) && other is $CacheType;
94389438
}
94399439
}
9440+
9441+
/// from: com.example.ok_http.RedirectInterceptor$Companion
9442+
class RedirectInterceptor_Companion extends jni.JObject {
9443+
@override
9444+
late final jni.JObjType<RedirectInterceptor_Companion> $type = type;
9445+
9446+
RedirectInterceptor_Companion.fromReference(
9447+
jni.JReference reference,
9448+
) : super.fromReference(reference);
9449+
9450+
static final _class =
9451+
jni.JClass.forName(r"com/example/ok_http/RedirectInterceptor$Companion");
9452+
9453+
/// The type which includes information such as the signature of this class.
9454+
static const type = $RedirectInterceptor_CompanionType();
9455+
static final _id_addRedirectInterceptor = _class.instanceMethodId(
9456+
r"addRedirectInterceptor",
9457+
r"(Lokhttp3/OkHttpClient$Builder;IZ)Lokhttp3/OkHttpClient$Builder;",
9458+
);
9459+
9460+
static final _addRedirectInterceptor = ProtectedJniExtensions.lookup<
9461+
ffi.NativeFunction<
9462+
jni.JniResult Function(
9463+
ffi.Pointer<ffi.Void>,
9464+
jni.JMethodIDPtr,
9465+
ffi.VarArgs<
9466+
(
9467+
ffi.Pointer<ffi.Void>,
9468+
ffi.Int64,
9469+
ffi.Int64
9470+
)>)>>("globalEnv_CallObjectMethod")
9471+
.asFunction<
9472+
jni.JniResult Function(ffi.Pointer<ffi.Void>, jni.JMethodIDPtr,
9473+
ffi.Pointer<ffi.Void>, int, int)>();
9474+
9475+
/// from: public final okhttp3.OkHttpClient$Builder addRedirectInterceptor(okhttp3.OkHttpClient$Builder builder, int i, boolean z)
9476+
/// The returned object must be released after use, by calling the [release] method.
9477+
OkHttpClient_Builder addRedirectInterceptor(
9478+
OkHttpClient_Builder builder,
9479+
int i,
9480+
bool z,
9481+
) {
9482+
return _addRedirectInterceptor(
9483+
reference.pointer,
9484+
_id_addRedirectInterceptor as jni.JMethodIDPtr,
9485+
builder.reference.pointer,
9486+
i,
9487+
z ? 1 : 0)
9488+
.object(const $OkHttpClient_BuilderType());
9489+
}
9490+
9491+
static final _id_new0 = _class.constructorId(
9492+
r"(Lkotlin/jvm/internal/DefaultConstructorMarker;)V",
9493+
);
9494+
9495+
static final _new0 = ProtectedJniExtensions.lookup<
9496+
ffi.NativeFunction<
9497+
jni.JniResult Function(
9498+
ffi.Pointer<ffi.Void>,
9499+
jni.JMethodIDPtr,
9500+
ffi.VarArgs<(ffi.Pointer<ffi.Void>,)>)>>(
9501+
"globalEnv_NewObject")
9502+
.asFunction<
9503+
jni.JniResult Function(ffi.Pointer<ffi.Void>, jni.JMethodIDPtr,
9504+
ffi.Pointer<ffi.Void>)>();
9505+
9506+
/// from: public void <init>(kotlin.jvm.internal.DefaultConstructorMarker defaultConstructorMarker)
9507+
/// The returned object must be released after use, by calling the [release] method.
9508+
factory RedirectInterceptor_Companion(
9509+
jni.JObject defaultConstructorMarker,
9510+
) {
9511+
return RedirectInterceptor_Companion.fromReference(_new0(
9512+
_class.reference.pointer,
9513+
_id_new0 as jni.JMethodIDPtr,
9514+
defaultConstructorMarker.reference.pointer)
9515+
.reference);
9516+
}
9517+
}
9518+
9519+
final class $RedirectInterceptor_CompanionType
9520+
extends jni.JObjType<RedirectInterceptor_Companion> {
9521+
const $RedirectInterceptor_CompanionType();
9522+
9523+
@override
9524+
String get signature =>
9525+
r"Lcom/example/ok_http/RedirectInterceptor$Companion;";
9526+
9527+
@override
9528+
RedirectInterceptor_Companion fromReference(jni.JReference reference) =>
9529+
RedirectInterceptor_Companion.fromReference(reference);
9530+
9531+
@override
9532+
jni.JObjType get superType => const jni.JObjectType();
9533+
9534+
@override
9535+
final superCount = 1;
9536+
9537+
@override
9538+
int get hashCode => ($RedirectInterceptor_CompanionType).hashCode;
9539+
9540+
@override
9541+
bool operator ==(Object other) {
9542+
return other.runtimeType == ($RedirectInterceptor_CompanionType) &&
9543+
other is $RedirectInterceptor_CompanionType;
9544+
}
9545+
}
9546+
9547+
/// from: com.example.ok_http.RedirectInterceptor
9548+
class RedirectInterceptor extends jni.JObject {
9549+
@override
9550+
late final jni.JObjType<RedirectInterceptor> $type = type;
9551+
9552+
RedirectInterceptor.fromReference(
9553+
jni.JReference reference,
9554+
) : super.fromReference(reference);
9555+
9556+
static final _class =
9557+
jni.JClass.forName(r"com/example/ok_http/RedirectInterceptor");
9558+
9559+
/// The type which includes information such as the signature of this class.
9560+
static const type = $RedirectInterceptorType();
9561+
static final _id_Companion = _class.staticFieldId(
9562+
r"Companion",
9563+
r"Lcom/example/ok_http/RedirectInterceptor$Companion;",
9564+
);
9565+
9566+
/// from: static public final com.example.ok_http.RedirectInterceptor$Companion Companion
9567+
/// The returned object must be released after use, by calling the [release] method.
9568+
static RedirectInterceptor_Companion get Companion =>
9569+
_id_Companion.get(_class, const $RedirectInterceptor_CompanionType());
9570+
9571+
static final _id_new0 = _class.constructorId(
9572+
r"()V",
9573+
);
9574+
9575+
static final _new0 = ProtectedJniExtensions.lookup<
9576+
ffi.NativeFunction<
9577+
jni.JniResult Function(
9578+
ffi.Pointer<ffi.Void>,
9579+
jni.JMethodIDPtr,
9580+
)>>("globalEnv_NewObject")
9581+
.asFunction<
9582+
jni.JniResult Function(
9583+
ffi.Pointer<ffi.Void>,
9584+
jni.JMethodIDPtr,
9585+
)>();
9586+
9587+
/// from: public void <init>()
9588+
/// The returned object must be released after use, by calling the [release] method.
9589+
factory RedirectInterceptor() {
9590+
return RedirectInterceptor.fromReference(
9591+
_new0(_class.reference.pointer, _id_new0 as jni.JMethodIDPtr)
9592+
.reference);
9593+
}
9594+
}
9595+
9596+
final class $RedirectInterceptorType extends jni.JObjType<RedirectInterceptor> {
9597+
const $RedirectInterceptorType();
9598+
9599+
@override
9600+
String get signature => r"Lcom/example/ok_http/RedirectInterceptor;";
9601+
9602+
@override
9603+
RedirectInterceptor fromReference(jni.JReference reference) =>
9604+
RedirectInterceptor.fromReference(reference);
9605+
9606+
@override
9607+
jni.JObjType get superType => const jni.JObjectType();
9608+
9609+
@override
9610+
final superCount = 1;
9611+
9612+
@override
9613+
int get hashCode => ($RedirectInterceptorType).hashCode;
9614+
9615+
@override
9616+
bool operator ==(Object other) {
9617+
return other.runtimeType == ($RedirectInterceptorType) &&
9618+
other is $RedirectInterceptorType;
9619+
}
9620+
}

pkgs/ok_http/lib/src/ok_http_client.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class OkHttpClient extends BaseClient {
8989
var requestHeaders = request.headers;
9090
var requestMethod = request.method;
9191
var requestBody = await request.finalize().toBytes();
92+
var maxRedirects = request.maxRedirects;
93+
var followRedirects = request.followRedirects;
9294

9395
final responseCompleter = Completer<StreamedResponse>();
9496

@@ -112,9 +114,22 @@ class OkHttpClient extends BaseClient {
112114
okReqBody,
113115
);
114116

117+
// To configure the client per-request, we create a new client with the
118+
// builder associated with `_client`.
119+
// They share the same connection pool and dispatcher.
120+
// https://square.github.io/okhttp/recipes/#per-call-configuration-kt-java
121+
//
122+
// `followRedirects` is set to `false` to handle redirects manually.
123+
// (Since OkHttp sets a hard limit of 20 redirects.)
124+
// https://github.com/square/okhttp/blob/54238b4c713080c3fd32fb1a070fb5d6814c9a09/okhttp/src/main/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt#L350
125+
final reqConfiguredClient = bindings.RedirectInterceptor.Companion
126+
.addRedirectInterceptor(_client.newBuilder().followRedirects(false),
127+
maxRedirects, followRedirects)
128+
.build();
129+
115130
// `enqueue()` schedules the request to be executed in the future.
116131
// https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/enqueue.html
117-
_client
132+
reqConfiguredClient
118133
.newCall(reqBuilder.build())
119134
.enqueue(bindings.Callback.implement(bindings.$CallbackImpl(
120135
onResponse: (bindings.Call call, bindings.Response response) {
@@ -159,9 +174,15 @@ class OkHttpClient extends BaseClient {
159174
headers: responseHeaders,
160175
request: request,
161176
contentLength: contentLength,
177+
isRedirect: response.isRedirect(),
162178
));
163179
},
164180
onFailure: (bindings.Call call, JObject ioException) {
181+
if (ioException.toString().contains('Redirect limit exceeded')) {
182+
responseCompleter.completeError(
183+
ClientException('Redirect limit exceeded', request.url));
184+
return;
185+
}
165186
responseCompleter.completeError(
166187
ClientException(ioException.toString(), request.url));
167188
},

0 commit comments

Comments
 (0)