11package io .getstream .chat .java .services .framework ;
22
33import okhttp3 .Request ;
4+ import okhttp3 .ResponseBody ;
45import org .jetbrains .annotations .NotNull ;
6+ import retrofit2 .Retrofit ;
57
68import java .io .IOException ;
9+ import java .lang .annotation .Annotation ;
10+ import java .lang .reflect .Type ;
711
812/**
913 * Wrapper for Retrofit {@code Call} objects that injects user authentication tokens.
1014 * <p>
11- * This class delegates all {@code Call} operations to an underlying call while ensuring
12- * that the {@link UserToken} is attached to the request as a typed tag. The token can
13- * then be retrieved by OkHttp interceptors for adding authorization headers.
15+ * This class creates new OkHttp calls using the tagged request to ensure the {@link UserToken}
16+ * is properly attached and available to interceptors for adding authorization headers.
1417 * </p>
1518 *
1619 * @param <T> the response body type
1720 * @see UserToken
18- * @see UserTokenCallRewriter
1921 */
2022class UserCall <T > implements retrofit2 .Call <T > {
2123 private final retrofit2 .Call <T > delegate ;
2224 private final UserToken token ;
25+ private final Retrofit retrofit ;
26+ private final Type responseType ;
27+ private volatile boolean executed ;
28+ private volatile okhttp3 .Call rawCall ;
2329
2430 /**
2531 * Constructs a new UserCall that wraps the provided call with token injection.
2632 *
27- * @param delegate the underlying Retrofit call
33+ * @param delegate the underlying Retrofit call (used for request template)
2834 * @param token the user token to inject
35+ * @param retrofit the Retrofit instance for creating calls and parsing responses
36+ * @param responseType the actual response type for proper deserialization
2937 */
30- UserCall (retrofit2 .Call <T > delegate , UserToken token ) {
38+ UserCall (retrofit2 .Call <T > delegate , UserToken token , Retrofit retrofit , Type responseType ) {
3139 this .delegate = delegate ;
3240 this .token = token ;
41+ this .retrofit = retrofit ;
42+ this .responseType = responseType ;
3343 }
3444
3545 /**
36- * Executes the HTTP request synchronously.
46+ * Creates an OkHttp call with the tagged request.
47+ */
48+ private okhttp3 .Call createRawCall () {
49+ return retrofit .callFactory ().newCall (request ());
50+ }
51+
52+ /**
53+ * Executes the HTTP request synchronously using a new call with the tagged request.
3754 *
3855 * @return the response
3956 * @throws IOException if the request fails
4057 */
4158 @ Override
4259 public @ NotNull retrofit2 .Response <T > execute () throws IOException {
43- return delegate .execute ();
60+ okhttp3 .Call call ;
61+ synchronized (this ) {
62+ if (executed ) throw new IllegalStateException ("Already executed." );
63+ executed = true ;
64+ rawCall = createRawCall ();
65+ call = rawCall ;
66+ }
67+
68+ okhttp3 .Response rawResponse = call .execute ();
69+ return parseResponse (rawResponse );
4470 }
4571
4672 /**
47- * Asynchronously sends the request and notifies the callback of its response .
73+ * Asynchronously sends the request using a new call with the tagged request .
4874 *
4975 * @param callback the callback to notify when the response arrives
5076 */
5177 @ Override
5278 public void enqueue (@ NotNull retrofit2 .Callback <T > callback ) {
53- delegate .enqueue (callback );
79+ okhttp3 .Call call ;
80+ synchronized (this ) {
81+ if (executed ) throw new IllegalStateException ("Already executed." );
82+ executed = true ;
83+ rawCall = createRawCall ();
84+ call = rawCall ;
85+ }
86+
87+ call .enqueue (new okhttp3 .Callback () {
88+ @ Override
89+ public void onResponse (@ NotNull okhttp3 .Call call , @ NotNull okhttp3 .Response rawResponse ) {
90+ retrofit2 .Response <T > response ;
91+ try {
92+ response = parseResponse (rawResponse );
93+ } catch (Throwable t ) {
94+ callFailure (t );
95+ return ;
96+ }
97+ callSuccess (response );
98+ }
99+
100+ @ Override
101+ public void onFailure (@ NotNull okhttp3 .Call call , @ NotNull IOException e ) {
102+ callFailure (e );
103+ }
104+
105+ private void callSuccess (retrofit2 .Response <T > response ) {
106+ try {
107+ callback .onResponse (UserCall .this , response );
108+ } catch (Throwable t ) {
109+ t .printStackTrace ();
110+ }
111+ }
112+
113+ private void callFailure (Throwable t ) {
114+ try {
115+ callback .onFailure (UserCall .this , t );
116+ } catch (Throwable t2 ) {
117+ t2 .printStackTrace ();
118+ }
119+ }
120+ });
121+ }
122+
123+ /**
124+ * Parses the raw OkHttp response into a Retrofit response using Retrofit's converters.
125+ * Based on Retrofit's OkHttpCall.parseResponse() implementation.
126+ */
127+ @ SuppressWarnings ("unchecked" )
128+ private retrofit2 .Response <T > parseResponse (okhttp3 .Response rawResponse ) throws IOException {
129+ ResponseBody rawBody = rawResponse .body ();
130+
131+ // Remove the body's source (the only stateful object) so we can pass the response along
132+ rawResponse = rawResponse .newBuilder ()
133+ .body (new NoContentResponseBody (rawBody .contentType (), rawBody .contentLength ()))
134+ .build ();
135+
136+ int code = rawResponse .code ();
137+
138+ if (code < 200 || code >= 300 ) {
139+ try {
140+ // Buffer the entire body to avoid future I/O
141+ ResponseBody bufferedBody = bufferResponseBody (rawBody );
142+ return retrofit2 .Response .error (bufferedBody , rawResponse );
143+ } finally {
144+ rawBody .close ();
145+ }
146+ }
147+
148+ if (code == 204 || code == 205 ) {
149+ rawBody .close ();
150+ return retrofit2 .Response .success (null , rawResponse );
151+ }
152+
153+ // Success response - parse body using Retrofit's converter
154+ try {
155+ retrofit2 .Converter <ResponseBody , T > converter =
156+ (retrofit2 .Converter <ResponseBody , T >) retrofit .responseBodyConverter (
157+ responseType , new Annotation [0 ]);
158+
159+ T body = converter .convert (rawBody );
160+ return retrofit2 .Response .success (body , rawResponse );
161+ } catch (RuntimeException e ) {
162+ rawBody .close ();
163+ throw e ;
164+ }
165+ }
166+
167+ /**
168+ * Buffers the response body to avoid future I/O operations.
169+ */
170+ private static ResponseBody bufferResponseBody (ResponseBody body ) throws IOException {
171+ okio .Buffer buffer = new okio .Buffer ();
172+ body .source ().readAll (buffer );
173+ return ResponseBody .create (buffer .readByteArray (), body .contentType ());
174+ }
175+
176+ /**
177+ * A response body that returns empty content, used to prevent reading stateful sources.
178+ */
179+ private static final class NoContentResponseBody extends ResponseBody {
180+ private final okhttp3 .MediaType contentType ;
181+ private final long contentLength ;
182+
183+ NoContentResponseBody (okhttp3 .MediaType contentType , long contentLength ) {
184+ this .contentType = contentType ;
185+ this .contentLength = contentLength ;
186+ }
187+
188+ @ Override
189+ public okhttp3 .MediaType contentType () {
190+ return contentType ;
191+ }
192+
193+ @ Override
194+ public long contentLength () {
195+ return contentLength ;
196+ }
197+
198+ @ Override
199+ public okio .BufferedSource source () {
200+ throw new IllegalStateException ("Cannot read raw response body of a converted body." );
201+ }
54202 }
55203
56204 /**
@@ -60,15 +208,17 @@ public void enqueue(@NotNull retrofit2.Callback<T> callback) {
60208 */
61209 @ Override
62210 public boolean isExecuted () {
63- return delegate . isExecuted () ;
211+ return executed ;
64212 }
65213
66214 /**
67215 * Cancels the request, if possible.
68216 */
69217 @ Override
70218 public void cancel () {
71- delegate .cancel ();
219+ if (rawCall != null ) {
220+ rawCall .cancel ();
221+ }
72222 }
73223
74224 /**
@@ -78,7 +228,7 @@ public void cancel() {
78228 */
79229 @ Override
80230 public boolean isCanceled () {
81- return delegate .isCanceled ();
231+ return rawCall != null && rawCall .isCanceled ();
82232 }
83233
84234 /**
@@ -91,7 +241,7 @@ public boolean isCanceled() {
91241 */
92242 @ Override
93243 public @ NotNull retrofit2 .Call <T > clone () {
94- return new UserCall <>(delegate .clone (), token );
244+ return new UserCall <>(delegate .clone (), token , retrofit , responseType );
95245 }
96246
97247 /**
@@ -118,6 +268,6 @@ public boolean isCanceled() {
118268 */
119269 @ Override
120270 public @ NotNull okio .Timeout timeout () {
121- return delegate .timeout ();
271+ return rawCall != null ? rawCall .timeout () : okio . Timeout . NONE ;
122272 }
123273}
0 commit comments