Skip to content

Commit 10e39f7

Browse files
committed
extract to UserTokenCallProxy
1 parent 4f98150 commit 10e39f7

File tree

2 files changed

+79
-25
lines changed

2 files changed

+79
-25
lines changed

src/main/java/io/getstream/chat/java/services/framework/DefaultClient.java

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -158,31 +158,7 @@ public <TService> TService create(Class<TService> svcClass) {
158158
return (TService) java.lang.reflect.Proxy.newProxyInstance(
159159
svcClass.getClassLoader(),
160160
new Class<?>[] { svcClass },
161-
(proxy, method, args) -> {
162-
Object result = method.invoke(service, args);
163-
164-
// If the result is a Call, wrap it to add the tag
165-
if (result instanceof retrofit2.Call) {
166-
retrofit2.Call<?> originalCall = (retrofit2.Call<?>) result;
167-
retrofit2.Call<?> clonedCall = originalCall.clone();
168-
169-
try {
170-
// Retrofit's OkHttpCall has a rawCall field
171-
var newRequest = originalCall.request().newBuilder().tag(UserToken.class, token).build();
172-
okhttp3.Call newOkHttpCall = okHttpClient.newCall(newRequest);
173-
174-
java.lang.reflect.Field rawCallField = clonedCall.getClass().getDeclaredField("rawCall");
175-
rawCallField.setAccessible(true);
176-
rawCallField.set(clonedCall, newOkHttpCall);
177-
178-
return clonedCall;
179-
} catch (Exception e) {
180-
throw new RuntimeException("Failed to modify call", e);
181-
}
182-
}
183-
184-
return result;
185-
}
161+
new UserTokenCallProxy(okHttpClient, service, token)
186162
);
187163
}
188164

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.getstream.chat.java.services.framework;
2+
3+
import okhttp3.Call;
4+
import okhttp3.Request;
5+
import org.jetbrains.annotations.NotNull;
6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.InvocationHandler;
8+
import java.lang.reflect.Method;
9+
10+
/**
11+
* Dynamic proxy that intercepts Retrofit service calls and injects UserToken
12+
* into the request by modifying the internal rawCall field via reflection.
13+
*
14+
* This approach allows per-call authentication without creating multiple OkHttpClient
15+
* instances, making it suitable for multi-tenant systems with thousands of users.
16+
*/
17+
class UserTokenCallProxy implements InvocationHandler {
18+
private static volatile Field rawCallField;
19+
20+
private final Call.Factory callFactory;
21+
private final Object delegate;
22+
private final UserToken token;
23+
24+
UserTokenCallProxy(@NotNull Call.Factory callFactory, @NotNull Object delegate, @NotNull UserToken token) {
25+
this.callFactory = callFactory;
26+
this.delegate = delegate;
27+
this.token = token;
28+
}
29+
30+
@Override
31+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
32+
Object result = method.invoke(delegate, args);
33+
34+
// If the result is a Retrofit Call, inject the user token
35+
if (result instanceof retrofit2.Call) {
36+
return injectTokenIntoCall((retrofit2.Call<?>) result);
37+
}
38+
39+
return result;
40+
}
41+
42+
private retrofit2.Call<?> injectTokenIntoCall(retrofit2.Call<?> originalCall) {
43+
retrofit2.Call<?> clonedCall = originalCall.clone();
44+
45+
try {
46+
// Cache field lookup for performance (double-checked locking)
47+
if (rawCallField == null) {
48+
synchronized (UserTokenCallProxy.class) {
49+
if (rawCallField == null) {
50+
rawCallField = clonedCall.getClass().getDeclaredField("rawCall");
51+
rawCallField.setAccessible(true);
52+
}
53+
}
54+
}
55+
56+
// Create new request with token tag
57+
Request newRequest = originalCall.request().newBuilder()
58+
.tag(UserToken.class, token)
59+
.build();
60+
61+
// Create new OkHttp call with modified request
62+
okhttp3.Call newOkHttpCall = callFactory.newCall(newRequest);
63+
64+
// Inject the new call into the cloned Retrofit call
65+
rawCallField.set(clonedCall, newOkHttpCall);
66+
67+
return clonedCall;
68+
} catch (NoSuchFieldException e) {
69+
// If Retrofit's internal structure changes, provide clear error message
70+
throw new RuntimeException(
71+
"Retrofit internal structure changed. Field 'rawCall' not found in " +
72+
clonedCall.getClass().getName() + ". Update client implementation.", e);
73+
} catch (IllegalAccessException e) {
74+
throw new RuntimeException("Failed to inject token into call", e);
75+
}
76+
}
77+
}
78+

0 commit comments

Comments
 (0)