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

box / box-java-sdk / #4498

26 Feb 2025 11:16AM UTC coverage: 72.059% (+0.05%) from 72.007%
#4498

push

github

web-flow
fix: Improve logging for API Request and API Response (#1295)

41 of 43 new or added lines in 4 files covered. (95.35%)

2 existing lines in 2 files now uncovered.

8214 of 11399 relevant lines covered (72.06%)

0.72 hits per line

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

89.36
/src/main/java/com/box/sdk/BoxAPIResponse.java
1
package com.box.sdk;
2

3
import static com.box.sdk.StandardCharsets.UTF_8;
4
import static com.box.sdk.http.ContentType.APPLICATION_JSON;
5
import static java.lang.String.format;
6

7
import com.eclipsesource.json.Json;
8
import com.eclipsesource.json.ParseException;
9
import java.io.ByteArrayInputStream;
10
import java.io.Closeable;
11
import java.io.IOException;
12
import java.io.InputStream;
13
import java.io.InputStreamReader;
14
import java.net.HttpURLConnection;
15
import java.util.List;
16
import java.util.Map;
17
import java.util.Objects;
18
import java.util.Optional;
19
import java.util.TreeMap;
20
import okhttp3.MediaType;
21
import okhttp3.Response;
22
import okhttp3.ResponseBody;
23

24
/**
25
 * Used to read HTTP responses from the Box API.
26
 *
27
 * <p>
28
 * All responses from the REST API are read using this class or one of its subclasses. This class wraps {@link
29
 * HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific
30
 * to Box's API. When a response is contructed, it will throw a {@link BoxAPIException} if the response from the API
31
 * was an error. Therefore every BoxAPIResponse instance is guaranteed to represent a successful response.
32
 * </p>
33
 *
34
 * <p>
35
 * This class usually isn't instantiated directly, but is instead returned after calling {@link BoxAPIRequest#send}.
36
 * </p>
37
 */
38
public class BoxAPIResponse implements Closeable {
39
    private static final int BUFFER_SIZE = 8192;
40
    private static final BoxLogger LOGGER = BoxLogger.defaultLogger();
1✔
41
    private final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
1✔
42
    private final long contentLength;
43
    private final String contentType;
44
    private final String requestMethod;
45
    private final String requestUrl;
46
    private int responseCode;
47
    private String bodyString;
48

49
    /**
50
     * The raw InputStream is the stream returned directly from HttpURLConnection.getInputStream(). We need to keep
51
     * track of this stream in case we need to access it after wrapping it inside another stream.
52
     */
53
    private InputStream rawInputStream;
54

55
    /**
56
     * The regular InputStream is the stream that will be returned by getBody(). This stream might be a GZIPInputStream
57
     * or a ProgressInputStream (or both) that wrap the raw InputStream.
58
     */
59
    private InputStream inputStream;
60

61
    /**
62
     * Constructs an empty BoxAPIResponse without an associated HttpURLConnection.
63
     */
64
    public BoxAPIResponse() {
1✔
65
        this.contentLength = 0;
1✔
66
        this.contentType = null;
1✔
67
        this.requestMethod = null;
1✔
68
        this.requestUrl = null;
1✔
69
    }
1✔
70

71
    /**
72
     * Constructs a BoxAPIResponse with a http response code and response headers.
73
     *
74
     * @param responseCode http response code
75
     * @param headers      map of headers
76
     */
77
    public BoxAPIResponse(
78
        int responseCode, String requestMethod, String requestUrl, Map<String, List<String>> headers
79
    ) {
80
        this(responseCode, requestMethod, requestUrl, headers, null, null, 0);
1✔
81
    }
1✔
82

83
    public BoxAPIResponse(int responseCode,
84
                          String requestMethod,
85
                          String requestUrl,
86
                          Map<String, List<String>> headers,
87
                          String bodyString,
88
                          String contentType) {
89
        this(responseCode, requestMethod, requestUrl, headers, null, contentType, 0, bodyString);
1✔
90
    }
1✔
91

92
    public BoxAPIResponse(int code,
93
                          String requestMethod,
94
                          String requestUrl,
95
                          Map<String, List<String>> headers,
96
                          InputStream body,
97
                          String contentType,
98
                          long contentLength
99
    ) {
100
        this(code, requestMethod, requestUrl, headers, body, contentType, contentLength, null);
1✔
101
    }
1✔
102

103
    public BoxAPIResponse(int code,
104
                          String requestMethod,
105
                          String requestUrl,
106
                          Map<String, List<String>> headers,
107
                          InputStream body,
108
                          String contentType,
109
                          long contentLength,
110
                          String bodyString
111
    ) {
1✔
112
        this.responseCode = code;
1✔
113
        this.requestMethod = requestMethod;
1✔
114
        this.requestUrl = requestUrl;
1✔
115
        if (headers != null) {
1✔
116
            this.headers.putAll(headers);
1✔
117
        }
118
        this.rawInputStream = body;
1✔
119
        this.contentType = contentType;
1✔
120
        this.contentLength = contentLength;
1✔
121
        this.bodyString = bodyString;
1✔
122
        if (body != null) {
1✔
123
            storeBodyResponse(body);
1✔
124
        }
125
        if (isSuccess(responseCode)) {
1✔
126
            this.logResponse();
1✔
127
        } else {
128
            this.logErrorResponse(this.responseCode);
×
129
            throw new BoxAPIResponseException("The API returned an error code", responseCode, null, headers);
×
130
        }
131
    }
1✔
132

133
    private static boolean isSuccess(int responseCode) {
134
        return responseCode >= 200 && responseCode < 400;
1✔
135
    }
136

137
    static BoxAPIResponse toBoxResponse(Response response) {
138
        if (!response.isSuccessful() && !response.isRedirect()) {
1✔
139
            throw new BoxAPIResponseException(
1✔
140
                "The API returned an error code",
141
                response.code(),
1✔
142
                Optional.ofNullable(response.body()).map(body -> {
1✔
143
                    try {
144
                        return body.string();
1✔
145
                    } catch (IOException e) {
×
146
                        throw new RuntimeException(e);
×
147
                    }
148
                }).orElse("Body was null"),
1✔
149
                response.headers().toMultimap()
1✔
150
            );
151
        }
152
        ResponseBody responseBody = response.body();
1✔
153
        if (responseBody.contentType() == null) {
1✔
154
            try {
155
                return emptyContentResponse(response);
1✔
156
            } finally {
157
                responseBody.close();
1✔
158
            }
159
        }
160
        if (responseBody != null && responseBody.contentType() != null) {
1✔
161
            if (responseBody.contentType().toString().contains(APPLICATION_JSON)) {
1✔
162
                if (responseBody.contentLength() == 0) {
1✔
163
                    return emptyContentResponse(response);
1✔
164
                }
165
                String bodyAsString = "";
1✔
166
                try {
167
                    bodyAsString = responseBody.string();
1✔
168
                    return new BoxJSONResponse(response.code(),
1✔
169
                        response.request().method(),
1✔
170
                        response.request().url().toString(),
1✔
171
                        response.headers().toMultimap(),
1✔
172
                        Json.parse(bodyAsString).asObject()
1✔
173
                    );
174
                } catch (ParseException e) {
1✔
175
                    throw new BoxAPIException(format("Error parsing JSON:\n%s", bodyAsString), e);
1✔
176
                } catch (IOException e) {
1✔
177
                    throw new RuntimeException("Error getting response to string", e);
1✔
178
                } finally {
179
                    responseBody.close();
1✔
180
                }
181
            }
182
        }
183
        return new BoxAPIResponse(response.code(),
1✔
184
            response.request().method(),
1✔
185
            response.request().url().toString(),
1✔
186
            response.headers().toMultimap(),
1✔
187
            responseBody.byteStream(),
1✔
188
            Optional.ofNullable(responseBody.contentType()).map(MediaType::toString).orElse(null),
1✔
189
            responseBody.contentLength()
1✔
190
        );
191
    }
192

193
    private static BoxAPIResponse emptyContentResponse(Response response) {
194
        return new BoxAPIResponse(response.code(),
1✔
195
            response.request().method(),
1✔
196
            response.request().url().toString(),
1✔
197
            response.headers().toMultimap()
1✔
198
        );
199
    }
200

201
    private void storeBodyResponse(InputStream body) {
202
        try {
203
            if (contentType != null && body != null && contentType.contains(APPLICATION_JSON) && body.available() > 0) {
1✔
204
                InputStreamReader reader = new InputStreamReader(this.getBody(), UTF_8);
1✔
205
                StringBuilder builder = new StringBuilder();
1✔
206
                char[] buffer = new char[BUFFER_SIZE];
1✔
207

208
                int read = reader.read(buffer, 0, BUFFER_SIZE);
1✔
209
                while (read != -1) {
1✔
210
                    builder.append(buffer, 0, read);
1✔
211
                    read = reader.read(buffer, 0, BUFFER_SIZE);
1✔
212
                }
213
                reader.close();
1✔
214
                this.disconnect();
1✔
215
                bodyString = builder.toString();
1✔
216
                rawInputStream = new ByteArrayInputStream(bodyString.getBytes(UTF_8));
1✔
217
            }
NEW
218
        } catch (IOException e) {
×
NEW
219
            throw new RuntimeException("Cannot read body stream", e);
×
220
        }
1✔
221
    }
1✔
222

223
    /**
224
     * Gets the response code returned by the API.
225
     *
226
     * @return the response code returned by the API.
227
     */
228
    public int getResponseCode() {
229
        return this.responseCode;
1✔
230
    }
231

232
    /**
233
     * Gets the length of this response's body as indicated by the "Content-Length" header.
234
     *
235
     * @return the length of the response's body.
236
     */
237
    public long getContentLength() {
238
        return this.contentLength;
1✔
239
    }
240

241
    /**
242
     * Gets the value of the given header field.
243
     *
244
     * @param fieldName name of the header field.
245
     * @return value of the header.
246
     */
247
    public String getHeaderField(String fieldName) {
248
        return Optional.ofNullable(this.headers.get(fieldName)).map((l) -> l.get(0)).orElse("");
1✔
249
    }
250

251
    /**
252
     * Gets an InputStream for reading this response's body.
253
     *
254
     * @return an InputStream for reading the response's body.
255
     */
256
    public InputStream getBody() {
257
        return this.getBody(null);
1✔
258
    }
259

260
    /**
261
     * Gets an InputStream for reading this response's body which will report its read progress to a ProgressListener.
262
     *
263
     * @param listener a listener for monitoring the read progress of the body.
264
     * @return an InputStream for reading the response's body.
265
     */
266
    public InputStream getBody(ProgressListener listener) {
267
        if (this.inputStream == null) {
1✔
268
            if (listener == null) {
1✔
269
                this.inputStream = this.rawInputStream;
1✔
270
            } else {
271
                this.inputStream = new ProgressInputStream(this.rawInputStream, listener, this.getContentLength());
1✔
272
            }
273
        }
274
        return this.inputStream;
1✔
275
    }
276

277
    /**
278
     * Disconnects this response from the server and frees up any network resources. The body of this response can no
279
     * longer be read after it has been disconnected.
280
     */
281
    public void disconnect() {
282
        this.close();
1✔
283
    }
1✔
284

285
    /**
286
     * @return A Map containg headers on this Box API Response.
287
     */
288
    public Map<String, List<String>> getHeaders() {
289
        return this.headers;
1✔
290
    }
291

292
    @Override
293
    public String toString() {
294
        String lineSeparator = System.getProperty("line.separator");
1✔
295
        StringBuilder builder = new StringBuilder();
1✔
296
        builder.append("Response")
1✔
297
            .append(lineSeparator)
1✔
298
            .append(this.requestMethod)
1✔
299
            .append(' ')
1✔
300
            .append(this.requestUrl)
1✔
301
            .append(' ')
1✔
302
            .append(this.responseCode)
1✔
303
            .append(lineSeparator)
1✔
304
            .append(contentType != null ? "Content-Type: " + contentType + lineSeparator : "")
1✔
305
            .append(headers.isEmpty() ? "" : "Headers:" + lineSeparator);
1✔
306
        headers.entrySet()
1✔
307
            .stream()
1✔
308
            .filter(Objects::nonNull)
1✔
309
            .forEach(e -> builder.append(format("%s: [%s]%s", e.getKey().toLowerCase(), e.getValue(), lineSeparator)));
1✔
310

311
        String bodyString = this.bodyToString();
1✔
312
        if (bodyString != null && !bodyString.equals("")) {
1✔
313
            String sanitizedBodyString = contentType.equals(APPLICATION_JSON)
1✔
314
                ? BoxSensitiveDataSanitizer.sanitizeJsonBody(Json.parse(bodyString).asObject()).toString()
1✔
315
                : bodyString;
316
            builder.append("Body:").append(lineSeparator).append(sanitizedBodyString);
1✔
317
        }
318

319
        return builder.toString().trim();
1✔
320
    }
321

322
    @Override
323
    public void close() {
324
        try {
325
            if (this.inputStream == null && this.rawInputStream != null) {
1✔
326
                this.rawInputStream.close();
×
327
            }
328
            if (this.inputStream != null) {
1✔
329
                this.inputStream.close();
1✔
330
            }
331
        } catch (IOException e) {
×
332
            throw new BoxAPIException(
×
333
                "Couldn't finish closing the connection to the Box API due to a network error or "
334
                    + "because the stream was already closed.", e
335
            );
336
        }
1✔
337
    }
1✔
338

339
    /**
340
     * Returns a string representation of this response's body. This method is used when logging this response's body.
341
     * By default, it returns an empty string (to avoid accidentally logging binary data) unless the response contained
342
     * an error message or content type is application/json.
343
     *
344
     * @return a string representation of this response's body.
345
     */
346
    protected String bodyToString() {
347
        return this.bodyString;
1✔
348
    }
349

350
    private void logResponse() {
351
        if (LOGGER.isDebugEnabled()) {
1✔
352
            LOGGER.debug(this.toString());
×
353
        }
354
    }
1✔
355

356
    private void logErrorResponse(int responseCode) {
357
        if (responseCode < 500 && LOGGER.isWarnEnabled()) {
×
358
            LOGGER.warn(this.toString());
×
359
        }
360
        if (responseCode >= 500 && LOGGER.isErrorEnabled()) {
×
361
            LOGGER.error(this.toString());
×
362
        }
363
    }
×
364

365
    protected String getRequestMethod() {
366
        return requestMethod;
1✔
367
    }
368

369
    protected String getRequestUrl() {
370
        return requestUrl;
1✔
371
    }
372
}
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