• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / box-java-sdk-gen / #549

15 Sep 2025 01:34PM UTC coverage: 36.587% (+0.03%) from 36.559%
#549

Pull #447

github

web-flow
Merge 1db8d3e87 into 58a40fe53
Pull Request #447: feat: Add proxy support (box/box-codegen#822)

9 of 71 new or added lines in 4 files covered. (12.68%)

3 existing lines in 1 file now uncovered.

18376 of 50226 relevant lines covered (36.59%)

0.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.11
/src/main/java/com/box/sdkgen/networking/boxnetworkclient/BoxNetworkClient.java
1
package com.box.sdkgen.networking.boxnetworkclient;
2

3
import static com.box.sdkgen.box.BoxConstants.USER_AGENT_HEADER;
4
import static com.box.sdkgen.box.BoxConstants.X_BOX_UA_HEADER;
5
import static com.box.sdkgen.internal.utils.UtilsManager.readByteStream;
6
import static com.box.sdkgen.serialization.json.JsonManager.jsonToSerializedData;
7
import static com.box.sdkgen.serialization.json.JsonManager.sdToJson;
8
import static com.box.sdkgen.serialization.json.JsonManager.sdToUrlParams;
9
import static java.util.Collections.singletonList;
10
import static okhttp3.ConnectionSpec.MODERN_TLS;
11

12
import com.box.sdkgen.box.errors.BoxAPIError;
13
import com.box.sdkgen.box.errors.BoxSDKError;
14
import com.box.sdkgen.internal.logging.DataSanitizer;
15
import com.box.sdkgen.networking.fetchoptions.FetchOptions;
16
import com.box.sdkgen.networking.fetchoptions.MultipartItem;
17
import com.box.sdkgen.networking.fetchoptions.ResponseFormat;
18
import com.box.sdkgen.networking.fetchresponse.FetchResponse;
19
import com.box.sdkgen.networking.network.NetworkSession;
20
import com.box.sdkgen.networking.networkclient.NetworkClient;
21
import com.box.sdkgen.networking.proxyconfig.ProxyConfig;
22
import com.fasterxml.jackson.databind.JsonNode;
23
import java.io.IOException;
24
import java.net.InetSocketAddress;
25
import java.net.Proxy;
26
import java.net.URI;
27
import java.nio.charset.StandardCharsets;
28
import java.util.Locale;
29
import java.util.Map;
30
import java.util.Objects;
31
import java.util.Optional;
32
import java.util.TreeMap;
33
import java.util.concurrent.TimeUnit;
34
import java.util.stream.Collectors;
35
import okhttp3.Call;
36
import okhttp3.Credentials;
37
import okhttp3.Headers;
38
import okhttp3.HttpUrl;
39
import okhttp3.MediaType;
40
import okhttp3.MultipartBody;
41
import okhttp3.OkHttpClient;
42
import okhttp3.Request;
43
import okhttp3.RequestBody;
44
import okhttp3.Response;
45
import okio.BufferedSink;
46
import okio.Okio;
47
import okio.Source;
48

49
public class BoxNetworkClient implements NetworkClient {
50

51
  private static final int BASE_TIMEOUT = 1;
52
  private static final double RANDOM_FACTOR = 0.5;
53
  private static final int DEFAULT_HTTP_PORT = 80;
54
  private static final int DEFAULT_HTTPS_PORT = 443;
55

56
  protected OkHttpClient httpClient;
57

58
  public BoxNetworkClient(OkHttpClient httpClient) {
×
59
    this.httpClient = httpClient;
×
60
  }
×
61

62
  public BoxNetworkClient() {
1✔
63
    OkHttpClient.Builder builder =
1✔
64
        new OkHttpClient.Builder()
65
            .followSslRedirects(true)
1✔
66
            .followRedirects(false)
1✔
67
            .connectionSpecs(singletonList(MODERN_TLS));
1✔
68
    httpClient = builder.build();
1✔
69
  }
1✔
70

71
  public OkHttpClient getHttpClient() {
72
    return httpClient;
×
73
  }
74

75
  public BoxNetworkClient withProxy(ProxyConfig config) {
NEW
76
    URI uri = URI.create(config.getUrl());
×
NEW
77
    String host = Objects.requireNonNull(uri.getHost(), "Invalid Proxy URL");
×
78

NEW
79
    String scheme =
×
NEW
80
        Optional.ofNullable(uri.getScheme())
×
NEW
81
            .filter(schema -> schema.startsWith("http"))
×
NEW
82
            .orElseThrow(() -> new IllegalArgumentException("Invalid Proxy URL: " + uri));
×
83

NEW
84
    int port =
×
NEW
85
        (uri.getPort() != -1)
×
NEW
86
            ? uri.getPort()
×
NEW
87
            : ("https".equalsIgnoreCase(scheme) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT);
×
88

NEW
89
    OkHttpClient.Builder clientBuilder =
×
90
        httpClient
NEW
91
            .newBuilder()
×
NEW
92
            .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)));
×
93

NEW
94
    String username = config.getUsername();
×
NEW
95
    String password = config.getPassword();
×
NEW
96
    if (username != null && !username.trim().isEmpty() && password != null) {
×
NEW
97
      String basic = Credentials.basic(username, password, StandardCharsets.UTF_8);
×
NEW
98
      clientBuilder.proxyAuthenticator(
×
99
          (route, resp) ->
NEW
100
              resp.request().newBuilder().header("Proxy-Authorization", basic).build());
×
101
    }
NEW
102
    return new BoxNetworkClient(clientBuilder.build());
×
103
  }
104

105
  public FetchResponse fetch(FetchOptions options) {
106
    NetworkSession networkSession =
1✔
107
        options.getNetworkSession() == null ? new NetworkSession() : options.getNetworkSession();
1✔
108

109
    FetchOptions fetchOptions =
1✔
110
        networkSession.getInterceptors().stream()
1✔
111
            .reduce(
1✔
112
                options,
113
                (modifiedOptions, interceptor) -> interceptor.beforeRequest(modifiedOptions),
1✔
114
                (o1, o2) -> o2);
×
115

116
    boolean authenticationNeeded = false;
1✔
117
    Request request;
118
    FetchResponse fetchResponse = new FetchResponse.Builder(0, new TreeMap<>()).build();
1✔
119
    Exception exceptionThrown = null;
1✔
120

121
    int attemptNumber = 1;
1✔
122
    int numberOfRetriesOnException = 0;
1✔
123
    int attemptForRetry = 0;
1✔
124
    boolean shouldRetry = false;
1✔
125

126
    while (true) {
127
      request = prepareRequest(fetchOptions, authenticationNeeded, networkSession);
1✔
128

129
      Response response = null;
1✔
130
      String rawResponseBody = null;
1✔
131

132
      try {
133
        response = executeOnClient(request);
1✔
134

135
        Map<String, String> headersMap =
1✔
136
            response.headers().toMultimap().entrySet().stream()
1✔
137
                .collect(
1✔
138
                    Collectors.toMap(
1✔
139
                        Map.Entry::getKey,
140
                        e -> e.getValue().get(0),
1✔
141
                        (existing, replacement) -> existing,
×
142
                        () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
1✔
143

144
        String responseUrl =
1✔
145
            response.networkResponse() != null
1✔
146
                ? response.networkResponse().request().url().toString()
1✔
147
                : response.request().url().toString();
1✔
148

149
        attemptForRetry = attemptNumber;
1✔
150

151
        if (Objects.equals(
1✔
152
            fetchOptions.getResponseFormat().getEnumValue(), ResponseFormat.BINARY)) {
1✔
153
          fetchResponse =
1✔
154
              new FetchResponse.Builder(response.code(), headersMap)
1✔
155
                  .content(response.body().byteStream())
1✔
156
                  .url(responseUrl)
1✔
157
                  .build();
1✔
158
        } else {
159
          rawResponseBody = response.body() != null ? response.body().string() : null;
1✔
160
          fetchResponse =
1✔
161
              new FetchResponse.Builder(response.code(), headersMap)
1✔
162
                  .data(readJsonFromRawBody(rawResponseBody))
1✔
163
                  .url(responseUrl)
1✔
164
                  .build();
1✔
165
        }
166

167
        fetchResponse =
1✔
168
            networkSession.getInterceptors().stream()
1✔
169
                .reduce(
1✔
170
                    fetchResponse,
171
                    (modifiedResponse, interceptor) -> interceptor.afterRequest(modifiedResponse),
1✔
172
                    (o1, o2) -> o2);
×
173

174
      } catch (Exception e) {
1✔
175
        exceptionThrown = e;
1✔
176
        numberOfRetriesOnException++;
1✔
177
        attemptForRetry = numberOfRetriesOnException;
1✔
178
        if (response != null) {
1✔
179
          response.close();
×
180
        }
181
      }
1✔
182

183
      shouldRetry =
1✔
184
          networkSession
185
              .getRetryStrategy()
1✔
186
              .shouldRetry(fetchOptions, fetchResponse, attemptForRetry);
1✔
187

188
      if (shouldRetry) {
1✔
189
        double retryDelay =
1✔
190
            networkSession
191
                .getRetryStrategy()
1✔
192
                .retryAfter(fetchOptions, fetchResponse, attemptForRetry);
1✔
193
        if (retryDelay > 0) {
1✔
194
          try {
195
            TimeUnit.SECONDS.sleep((long) retryDelay);
1✔
196
          } catch (InterruptedException ie) {
×
197
            Thread.currentThread().interrupt();
×
198
            throw new BoxSDKError("Retry interrupted", ie);
×
199
          }
1✔
200
        }
201
        attemptNumber++;
1✔
202
        continue;
1✔
203
      }
204

205
      if (fetchResponse.getStatus() >= 300
1✔
206
          && fetchResponse.getStatus() < 400
1✔
207
          && fetchOptions.followRedirects) {
1✔
208
        if (!fetchResponse.getHeaders().containsKey("Location")) {
1✔
209
          throw new BoxSDKError(
1✔
210
              "Redirect response missing Location header for " + fetchOptions.getUrl());
1✔
211
        }
212
        URI originalUri = URI.create(fetchOptions.getUrl());
1✔
213
        URI redirectUri = URI.create(fetchResponse.getHeaders().get("Location"));
1✔
214
        boolean sameOrigin =
1✔
215
            originalUri.getHost().equals(redirectUri.getHost())
1✔
216
                && originalUri.getPort() == redirectUri.getPort()
1✔
217
                && originalUri.getScheme().equals(redirectUri.getScheme());
1✔
218
        return fetch(
1✔
219
            new FetchOptions.Builder(fetchResponse.getHeaders().get("Location"), "GET")
1✔
220
                .responseFormat(fetchOptions.getResponseFormat())
1✔
221
                .auth(sameOrigin ? fetchOptions.getAuth() : null)
1✔
222
                .networkSession(networkSession)
1✔
223
                .build());
1✔
224
      }
225

226
      if (fetchResponse.getStatus() >= 200 && fetchResponse.getStatus() < 400) {
1✔
227
        return fetchResponse;
1✔
228
      }
229

230
      throwOnUnsuccessfulResponse(
1✔
231
          request,
232
          fetchResponse,
233
          rawResponseBody,
234
          exceptionThrown,
235
          networkSession.getDataSanitizer());
1✔
236
    }
×
237
  }
238

239
  private static Request prepareRequest(
240
      FetchOptions options, boolean reauthenticate, NetworkSession networkSession) {
241
    Request.Builder requestBuilder = new Request.Builder().url(options.getUrl());
1✔
242
    Headers headers = prepareHeaders(options, reauthenticate, networkSession);
1✔
243
    HttpUrl url = prepareUrl(options);
1✔
244
    RequestBody body = prepareRequestBody(options);
1✔
245

246
    requestBuilder.headers(headers);
1✔
247
    requestBuilder.url(url);
1✔
248
    requestBuilder.method(options.getMethod().toUpperCase(Locale.ROOT), body);
1✔
249
    return requestBuilder.build();
1✔
250
  }
251

252
  private static Headers prepareHeaders(
253
      FetchOptions options, boolean reauthenticate, NetworkSession networkSession) {
254
    Headers.Builder headersBuilder = new Headers.Builder();
1✔
255

256
    networkSession.getAdditionalHeaders().forEach(headersBuilder::add);
1✔
257

258
    if (options.getHeaders() != null) {
1✔
259
      options.getHeaders().forEach(headersBuilder::add);
1✔
260
    }
261
    if (options.getAuth() != null) {
1✔
262
      if (reauthenticate) {
1✔
263
        options.getAuth().refreshToken(networkSession);
×
264
      }
265
      headersBuilder.add(
1✔
266
          "Authorization", options.getAuth().retrieveAuthorizationHeader(networkSession));
1✔
267
    }
268
    headersBuilder.add("User-Agent", USER_AGENT_HEADER);
1✔
269
    headersBuilder.add("X-Box-UA", X_BOX_UA_HEADER);
1✔
270
    return headersBuilder.build();
1✔
271
  }
272

273
  private static HttpUrl prepareUrl(FetchOptions options) {
274

275
    HttpUrl baseUrl = HttpUrl.parse(options.getUrl());
1✔
276
    if (baseUrl == null) {
1✔
277
      throw new IllegalArgumentException("Invalid URL " + options.getUrl());
×
278
    }
279
    HttpUrl.Builder urlBuilder = baseUrl.newBuilder();
1✔
280
    if (options.getParams() != null) {
1✔
281
      options.getParams().forEach(urlBuilder::addQueryParameter);
1✔
282
    }
283
    return urlBuilder.build();
1✔
284
  }
285

286
  private static RequestBody prepareRequestBody(FetchOptions options) {
287
    if (options.getMethod().equalsIgnoreCase("GET")) {
1✔
288
      return null;
1✔
289
    }
290
    String contentType = options.getContentType();
1✔
291
    MediaType mediaType = MediaType.parse(contentType);
1✔
292
    switch (contentType) {
1✔
293
      case "application/json":
294
      case "application/json-patch+json":
295
        return options.getData() != null
1✔
296
            ? RequestBody.create(sdToJson(options.getData()), mediaType)
1✔
297
            : RequestBody.create("", mediaType);
1✔
298
      case "application/x-www-form-urlencoded":
299
        return options.getData() != null
1✔
300
            ? RequestBody.create(sdToUrlParams(options.getData()), mediaType)
1✔
301
            : RequestBody.create("", mediaType);
×
302
      case "multipart/form-data":
303
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
1✔
304
        for (MultipartItem part : options.multipartData) {
1✔
305
          if (part.getData() != null) {
1✔
306
            bodyBuilder.addFormDataPart(part.getPartName(), sdToJson(part.getData()));
1✔
307
          } else {
308
            bodyBuilder.addFormDataPart(
1✔
309
                part.getPartName(),
1✔
310
                part.getFileName() != null ? part.getFileName() : "file",
1✔
311
                createMultipartRequestBody(part));
1✔
312
          }
313
        }
1✔
314
        return bodyBuilder.build();
1✔
315
      case "application/octet-stream":
316
        return RequestBody.create(readByteStream(options.getFileStream()), mediaType);
1✔
317
      default:
318
        throw new IllegalArgumentException("Unsupported content type " + contentType);
×
319
    }
320
  }
321

322
  protected Call createNewCall(Request request) {
323
    return this.httpClient.newCall(request);
1✔
324
  }
325

326
  private Response executeOnClient(Request request) throws IOException {
327
    return createNewCall(request).execute();
1✔
328
  }
329

330
  private static JsonNode readJsonFromRawBody(String rawResponseBody) {
331
    if (rawResponseBody == null) {
1✔
332
      return null;
×
333
    }
334

335
    try {
336
      return jsonToSerializedData(rawResponseBody);
1✔
337
    } catch (Exception e) {
1✔
338
      return null;
1✔
339
    }
340
  }
341

342
  private static void throwOnUnsuccessfulResponse(
343
      Request request,
344
      FetchResponse fetchResponse,
345
      String rawResponseBody,
346
      Exception exceptionThrown,
347
      DataSanitizer dataSanitizer) {
348
    if (fetchResponse.getStatus() == 0 && exceptionThrown != null) {
1✔
349
      throw new BoxSDKError(exceptionThrown.getMessage(), exceptionThrown);
1✔
350
    }
351
    try {
352
      throw BoxAPIError.fromAPICall(request, fetchResponse, rawResponseBody, dataSanitizer);
1✔
353
    } finally {
354
      try {
355
        if (fetchResponse.getContent() != null) {
1✔
356
          fetchResponse.getContent().close();
1✔
357
        }
358
      } catch (IOException ignored) {
×
359
      }
1✔
360
    }
361
  }
362

363
  private static int getRetryAfterTimeInSeconds(int attemptNumber, String retryAfterHeader) {
364

365
    if (retryAfterHeader != null) {
×
366
      return Integer.parseInt(retryAfterHeader);
×
367
    }
368

369
    double minWindow = 1 - RANDOM_FACTOR;
×
370
    double maxWindow = 1 + RANDOM_FACTOR;
×
371
    double jitter = (Math.random() * (maxWindow - minWindow)) + minWindow;
×
372
    return (int) (Math.pow(2, attemptNumber) * BASE_TIMEOUT * jitter);
×
373
  }
374

375
  public static RequestBody createMultipartRequestBody(MultipartItem part) {
376
    return new RequestBody() {
1✔
377
      @Override
378
      public MediaType contentType() {
379
        if (part.contentType != null) {
1✔
380
          return MediaType.parse(part.contentType);
1✔
381
        }
382
        return MediaType.parse("application/octet-stream");
1✔
383
      }
384

385
      @Override
386
      public void writeTo(BufferedSink sink) throws IOException {
387
        try (Source source = Okio.source(part.getFileStream())) {
1✔
388
          sink.writeAll(source);
1✔
389
        }
390
      }
1✔
391
    };
392
  }
393
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc