• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

mizosoft / methanol / #605

09 Oct 2025 12:32PM UTC coverage: 88.705% (+0.1%) from 88.592%
#605

push

github

mizosoft
Use a specific interceptor implementation instead of a new interface

2374 of 2876 branches covered (82.55%)

Branch coverage included in aggregate %.

144 of 182 new or added lines in 3 files covered. (79.12%)

52 existing lines in 4 files now uncovered.

7843 of 8642 relevant lines covered (90.75%)

0.91 hits per line

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

83.98
/methanol/src/main/java/com/github/mizosoft/methanol/ResponseBuilder.java
1
/*
2
 * Copyright (c) 2024 Moataz Hussein
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining a copy
5
 * of this software and associated documentation files (the "Software"), to deal
6
 * in the Software without restriction, including without limitation the rights
7
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
 * copies of the Software, and to permit persons to whom the Software is
9
 * furnished to do so, subject to the following conditions:
10
 *
11
 * The above copyright notice and this permission notice shall be included in all
12
 * copies or substantial portions of the Software.
13
 *
14
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
 * SOFTWARE.
21
 */
22

23
package com.github.mizosoft.methanol;
24

25
import static com.github.mizosoft.methanol.internal.Validate.castNonNull;
26
import static com.github.mizosoft.methanol.internal.Validate.requireArgument;
27
import static com.github.mizosoft.methanol.internal.Validate.requireState;
28
import static java.util.Objects.requireNonNull;
29

30
import com.github.mizosoft.methanol.CacheAwareResponse.CacheStatus;
31
import com.github.mizosoft.methanol.internal.extensions.HeadersBuilder;
32
import com.google.errorprone.annotations.CanIgnoreReturnValue;
33
import java.net.HttpURLConnection;
34
import java.net.URI;
35
import java.net.http.HttpClient.Version;
36
import java.net.http.HttpHeaders;
37
import java.net.http.HttpRequest;
38
import java.net.http.HttpResponse;
39
import java.time.Instant;
40
import java.util.List;
41
import java.util.Optional;
42
import java.util.function.BiPredicate;
43
import java.util.function.Consumer;
44
import java.util.function.Supplier;
45
import javax.net.ssl.SSLSession;
46
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
47
import org.checkerframework.checker.nullness.qual.NonNull;
48
import org.checkerframework.checker.nullness.qual.Nullable;
49

50
/** A builder of {@link HttpResponse} instances. */
51
public final class ResponseBuilder<T> implements HeadersAccumulator<ResponseBuilder<T>> {
52
  private final HeadersBuilder headersBuilder = new HeadersBuilder();
1✔
53
  private int statusCode = HttpURLConnection.HTTP_OK;
1✔
54
  private @MonotonicNonNull URI uri;
55
  private @MonotonicNonNull Version version;
56
  private @MonotonicNonNull HttpRequest request;
57
  private @MonotonicNonNull Instant timeRequestSent;
58
  private @MonotonicNonNull Instant timeResponseReceived;
59
  private @Nullable Object body;
60
  private @Nullable SSLSession sslSession;
61
  private @Nullable HttpResponse<T> previousResponse;
62
  private @Nullable TrackedResponse<?> networkResponse;
63
  private @Nullable TrackedResponse<?> cacheResponse;
64
  private @MonotonicNonNull CacheStatus cacheStatus;
65

66
  private ResponseBuilder() {}
1✔
67

68
  @CanIgnoreReturnValue
69
  public ResponseBuilder<T> apply(Consumer<? super ResponseBuilder<T>> mutator) {
70
    mutator.accept(this);
1✔
71
    return this;
1✔
72
  }
73

74
  @CanIgnoreReturnValue
75
  public ResponseBuilder<T> statusCode(int statusCode) {
76
    requireArgument(statusCode >= 0, "Negative status code: %d", statusCode);
1!
77
    this.statusCode = statusCode;
1✔
78
    return this;
1✔
79
  }
80

81
  @CanIgnoreReturnValue
82
  public ResponseBuilder<T> uri(URI uri) {
83
    this.uri = requireNonNull(uri);
1✔
84
    return this;
1✔
85
  }
86

87
  @CanIgnoreReturnValue
88
  public ResponseBuilder<T> uri(String uri) {
89
    return uri(URI.create(uri));
×
90
  }
91

92
  @CanIgnoreReturnValue
93
  public ResponseBuilder<T> version(Version version) {
94
    this.version = requireNonNull(version);
1✔
95
    return this;
1✔
96
  }
97

98
  @Override
99
  @CanIgnoreReturnValue
100
  public ResponseBuilder<T> header(String name, String value) {
101
    headersBuilder.add(name, value);
1✔
102
    return this;
1✔
103
  }
104

105
  @Override
106
  @CanIgnoreReturnValue
107
  public ResponseBuilder<T> headers(String... headers) {
108
    headersBuilder.addAll(headers);
×
109
    return this;
×
110
  }
111

112
  @Override
113
  @CanIgnoreReturnValue
114
  public ResponseBuilder<T> headers(HttpHeaders headers) {
115
    headersBuilder.addAll(headers);
1✔
116
    return this;
1✔
117
  }
118

119
  @Override
120
  @CanIgnoreReturnValue
121
  public ResponseBuilder<T> setHeader(String name, String value) {
122
    headersBuilder.set(name, value);
1✔
123
    return this;
1✔
124
  }
125

126
  @Override
127
  public ResponseBuilder<T> setHeader(String name, List<String> values) {
128
    headersBuilder.set(name, values);
×
129
    return this;
×
130
  }
131

132
  @Override
133
  public ResponseBuilder<T> setHeaderIfAbsent(String name, String value) {
134
    headersBuilder.setIfAbsent(name, value);
×
135
    return this;
×
136
  }
137

138
  @Override
139
  public ResponseBuilder<T> setHeaderIfAbsent(String name, List<String> values) {
140
    headersBuilder.setIfAbsent(name, values);
×
141
    return this;
×
142
  }
143

144
  @Override
145
  @CanIgnoreReturnValue
146
  public ResponseBuilder<T> removeHeaders() {
147
    headersBuilder.clear();
1✔
148
    return this;
1✔
149
  }
150

151
  @Override
152
  @CanIgnoreReturnValue
153
  public ResponseBuilder<T> removeHeader(String name) {
154
    headersBuilder.remove(name);
1✔
155
    return this;
1✔
156
  }
157

158
  @Override
159
  @CanIgnoreReturnValue
160
  public ResponseBuilder<T> removeHeadersIf(BiPredicate<String, String> filter) {
161
    headersBuilder.removeIf(filter);
×
162
    return this;
×
163
  }
164

165
  @CanIgnoreReturnValue
166
  public ResponseBuilder<T> request(HttpRequest request) {
167
    this.request = requireNonNull(request);
1✔
168
    return this;
1✔
169
  }
170

171
  @CanIgnoreReturnValue
172
  public ResponseBuilder<T> timeRequestSent(Instant timeRequestSent) {
173
    this.timeRequestSent = requireNonNull(timeRequestSent);
1✔
174
    return this;
1✔
175
  }
176

177
  @CanIgnoreReturnValue
178
  public ResponseBuilder<T> timeResponseReceived(Instant timeResponseReceived) {
179
    this.timeResponseReceived = requireNonNull(timeResponseReceived);
1✔
180
    return this;
1✔
181
  }
182

183
  @CanIgnoreReturnValue
184
  @SuppressWarnings("unchecked")
185
  public <U> ResponseBuilder<U> body(@Nullable U body) {
186
    this.body = body;
1✔
187
    return (ResponseBuilder<U>) this;
1✔
188
  }
189

190
  @CanIgnoreReturnValue
191
  public ResponseBuilder<T> dropBody() {
192
    return body(null);
1✔
193
  }
194

195
  @CanIgnoreReturnValue
196
  public ResponseBuilder<T> sslSession(@Nullable SSLSession sslSession) {
197
    this.sslSession = sslSession;
1✔
198
    return this;
1✔
199
  }
200

201
  @CanIgnoreReturnValue
202
  public ResponseBuilder<T> previousResponse(@Nullable HttpResponse<T> previousResponse) {
203
    this.previousResponse = previousResponse;
1✔
204
    return this;
1✔
205
  }
206

207
  @CanIgnoreReturnValue
208
  public ResponseBuilder<T> networkResponse(@Nullable TrackedResponse<?> networkResponse) {
209
    this.networkResponse = networkResponse;
1✔
210
    return this;
1✔
211
  }
212

213
  @CanIgnoreReturnValue
214
  public ResponseBuilder<T> cacheResponse(@Nullable TrackedResponse<?> cacheResponse) {
215
    this.cacheResponse = cacheResponse;
1✔
216
    return this;
1✔
217
  }
218

219
  @CanIgnoreReturnValue
220
  public ResponseBuilder<T> cacheStatus(CacheStatus cacheStatus) {
221
    this.cacheStatus = requireNonNull(cacheStatus);
1✔
222
    return this;
1✔
223
  }
224

225
  @SuppressWarnings("unchecked")
226
  public HttpResponse<T> build() {
227
    if (cacheStatus != null) {
1✔
228
      return buildCacheAwareResponse();
1✔
229
    }
230
    if (timeRequestSent != null || timeResponseReceived != null) {
1!
231
      return buildTrackedResponse();
1✔
232
    }
233
    return new HttpResponseImpl<>(
1✔
234
        statusCode,
235
        ensureSetOrElseGet(uri, () -> request != null ? request.uri() : null, "uri"),
1!
236
        ensureSetOrElseGet(
1✔
237
            version,
238
            () -> request != null ? request.version().orElse(Version.HTTP_1_1) : Version.HTTP_1_1,
1!
239
            "version"),
240
        headersBuilder.build(),
1✔
241
        ensureSet(request, "request"),
1✔
242
        (T) body,
243
        sslSession,
244
        previousResponse);
245
  }
246

247
  @SuppressWarnings("unchecked")
248
  public TrackedResponse<T> buildTrackedResponse() {
249
    if (cacheStatus != null) {
1✔
250
      return buildCacheAwareResponse();
1✔
251
    }
252
    return new TrackedResponseImpl<>(
1✔
253
        statusCode,
254
        ensureSetOrElseGet(uri, () -> request != null ? request.uri() : null, "uri"),
1!
255
        ensureSetOrElseGet(
1✔
256
            version,
UNCOV
257
            () -> request != null ? request.version().orElse(Version.HTTP_1_1) : Version.HTTP_1_1,
×
258
            "version"),
259
        headersBuilder.build(),
1✔
260
        ensureSet(request, "request"),
1✔
261
        (T) body,
262
        sslSession,
263
        previousResponse,
264
        ensureSet(timeRequestSent, "timeRequestSent"),
1✔
265
        ensureSet(timeResponseReceived, "timeResponseReceived"));
1✔
266
  }
267

268
  @SuppressWarnings("unchecked")
269
  public CacheAwareResponse<T> buildCacheAwareResponse() {
270
    return new CacheAwareResponseImpl<>(
1✔
271
        statusCode,
272
        ensureSetOrElseGet(uri, () -> request != null ? request.uri() : null, "uri"),
1!
273
        ensureSetOrElseGet(
1✔
274
            version,
UNCOV
275
            () -> request != null ? request.version().orElse(Version.HTTP_1_1) : Version.HTTP_1_1,
×
276
            "version"),
277
        headersBuilder.build(),
1✔
278
        ensureSet(request, "request"),
1✔
279
        (T) body,
280
        sslSession,
281
        previousResponse,
282
        ensureSet(timeRequestSent, "timeRequestSent"),
1✔
283
        ensureSet(timeResponseReceived, "timeResponseReceived"),
1✔
284
        networkResponse,
285
        cacheResponse,
286
        ensureSet(cacheStatus, "cacheStatus"));
1✔
287
  }
288

289
  @CanIgnoreReturnValue
290
  private ResponseBuilder<T> headers(HttpHeaders headers, boolean bypassHeaderValidation) {
291
    if (bypassHeaderValidation) {
1!
292
      headersBuilder.addAllLenient(headers);
1✔
293
    } else {
UNCOV
294
      headersBuilder.addAll(headers);
×
295
    }
296
    return this;
1✔
297
  }
298

299
  /** Returns a new {@code ResponseBuilder}. */
300
  public static <T> ResponseBuilder<T> create() {
301
    return new ResponseBuilder<>();
1✔
302
  }
303

304
  /** Returns a new {@code ResponseBuilder} that copies the given response's state. */
305
  public static <T> ResponseBuilder<T> from(HttpResponse<T> response) {
306
    var builder =
1✔
307
        new ResponseBuilder<>()
308
            .statusCode(response.statusCode())
1✔
309
            .uri(response.uri())
1✔
310
            .version(response.version())
1✔
311
            .headers(response.headers(), isTrusted(response))
1✔
312
            .request(response.request())
1✔
313
            .body(response.body());
1✔
314
    response.previousResponse().ifPresent(builder::previousResponse);
1✔
315
    response.sslSession().ifPresent(builder::sslSession);
1✔
316
    if (response instanceof TrackedResponse<?>) {
1✔
317
      var trackedResponse = ((TrackedResponse<?>) response);
1✔
318
      builder
1✔
319
          .timeRequestSent(trackedResponse.timeRequestSent())
1✔
320
          .timeResponseReceived(trackedResponse.timeResponseReceived());
1✔
321
    }
322
    if (response instanceof CacheAwareResponse<?>) {
1✔
323
      var cacheAwareResponse = (CacheAwareResponse<?>) response;
1✔
324
      builder
1✔
325
          .networkResponse(cacheAwareResponse.networkResponse().orElse(null))
1✔
326
          .cacheResponse(cacheAwareResponse.cacheResponse().orElse(null))
1✔
327
          .cacheStatus(cacheAwareResponse.cacheStatus());
1✔
328
    }
329
    return builder;
1✔
330
  }
331

332
  private static boolean isTrusted(HttpResponse<?> response) {
333
    return response instanceof HttpResponseImpl
1✔
334
        || "java.net.http".equals(response.getClass().getModule().getName());
1!
335
  }
336

337
  private static <T> @NonNull T ensureSet(@Nullable T property, String name) {
338
    requireState(property != null, "%s is required", name);
1!
339
    return castNonNull(property);
1✔
340
  }
341

342
  private static <T> @NonNull T ensureSetOrElseGet(
343
      @Nullable T property, Supplier<T> fallback, String name) {
344
    return ensureSet(property != null ? property : fallback.get(), name);
1✔
345
  }
346

347
  private static class HttpResponseImpl<T> implements HttpResponse<T> {
348
    private final int statusCode;
349
    private final URI uri;
350
    private final Version version;
351
    private final HttpHeaders headers;
352
    private final HttpRequest request;
353
    private final @Nullable T body;
354
    private final @Nullable SSLSession sslSession;
355
    private final @Nullable HttpResponse<T> previousResponse;
356

357
    HttpResponseImpl(
358
        int statusCode,
359
        URI uri,
360
        Version version,
361
        HttpHeaders headers,
362
        HttpRequest request,
363
        @Nullable T body,
364
        @Nullable SSLSession sslSession,
365
        @Nullable HttpResponse<T> previousResponse) {
1✔
366
      this.statusCode = statusCode;
1✔
367
      this.uri = uri;
1✔
368
      this.version = version;
1✔
369
      this.headers = headers;
1✔
370
      this.request = request;
1✔
371
      this.body = body;
1✔
372
      this.sslSession = sslSession;
1✔
373
      this.previousResponse = previousResponse;
1✔
374
    }
1✔
375

376
    @Override
377
    public int statusCode() {
378
      return statusCode;
1✔
379
    }
380

381
    @Override
382
    public HttpRequest request() {
383
      return request;
1✔
384
    }
385

386
    @Override
387
    public Optional<HttpResponse<T>> previousResponse() {
388
      return Optional.ofNullable(previousResponse);
1✔
389
    }
390

391
    @Override
392
    public HttpHeaders headers() {
393
      return headers;
1✔
394
    }
395

396
    @Override
397
    @SuppressWarnings("NullAway")
398
    public T body() {
399
      return body;
1✔
400
    }
401

402
    @Override
403
    public Optional<SSLSession> sslSession() {
404
      return Optional.ofNullable(sslSession);
1✔
405
    }
406

407
    @Override
408
    public URI uri() {
409
      return uri;
1✔
410
    }
411

412
    @Override
413
    public Version version() {
414
      return version;
1✔
415
    }
416

417
    @Override
418
    public String toString() {
419
      return '(' + request.method() + " " + request.uri() + ") " + statusCode;
1✔
420
    }
421
  }
422

423
  private static class TrackedResponseImpl<T> extends HttpResponseImpl<T>
424
      implements TrackedResponse<T> {
425
    private final Instant timeRequestSent;
426
    private final Instant timeResponseReceived;
427

428
    TrackedResponseImpl(
429
        int statusCode,
430
        URI uri,
431
        Version version,
432
        HttpHeaders headers,
433
        HttpRequest request,
434
        @Nullable T body,
435
        @Nullable SSLSession sslSession,
436
        @Nullable HttpResponse<T> previousResponse,
437
        Instant timeRequestSent,
438
        Instant timeResponseReceived) {
439
      super(statusCode, uri, version, headers, request, body, sslSession, previousResponse);
1✔
440
      this.timeRequestSent = timeRequestSent;
1✔
441
      this.timeResponseReceived = timeResponseReceived;
1✔
442
    }
1✔
443

444
    @Override
445
    public Instant timeRequestSent() {
446
      return timeRequestSent;
1✔
447
    }
448

449
    @Override
450
    public Instant timeResponseReceived() {
451
      return timeResponseReceived;
1✔
452
    }
453
  }
454

455
  private static final class CacheAwareResponseImpl<T> extends TrackedResponseImpl<T>
456
      implements CacheAwareResponse<T> {
457
    private final @Nullable TrackedResponse<?> networkResponse;
458
    private final @Nullable TrackedResponse<?> cacheResponse;
459
    private final CacheStatus cacheStatus;
460

461
    CacheAwareResponseImpl(
462
        int statusCode,
463
        URI uri,
464
        Version version,
465
        HttpHeaders headers,
466
        HttpRequest request,
467
        @Nullable T body,
468
        @Nullable SSLSession sslSession,
469
        @Nullable HttpResponse<T> previousResponse,
470
        Instant timeRequestSent,
471
        Instant timeResponseReceived,
472
        @Nullable TrackedResponse<?> networkResponse,
473
        @Nullable TrackedResponse<?> cacheResponse,
474
        CacheStatus cacheStatus) {
475
      super(
1✔
476
          statusCode,
477
          uri,
478
          version,
479
          headers,
480
          request,
481
          body,
482
          sslSession,
483
          previousResponse,
484
          timeRequestSent,
485
          timeResponseReceived);
486
      this.networkResponse = networkResponse;
1✔
487
      this.cacheResponse = cacheResponse;
1✔
488
      this.cacheStatus = cacheStatus;
1✔
489
    }
1✔
490

491
    @Override
492
    public Optional<TrackedResponse<?>> networkResponse() {
493
      return Optional.ofNullable(networkResponse);
1✔
494
    }
495

496
    @Override
497
    public Optional<TrackedResponse<?>> cacheResponse() {
498
      return Optional.ofNullable(cacheResponse);
1✔
499
    }
500

501
    @Override
502
    public CacheStatus cacheStatus() {
503
      return cacheStatus;
1✔
504
    }
505
  }
506
}
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

© 2026 Coveralls, Inc