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

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

87.35
/methanol/src/main/java/com/github/mizosoft/methanol/Methanol.java
1
/*
2
 * Copyright (c) 2025 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.Utils.requirePositiveDuration;
26
import static com.github.mizosoft.methanol.internal.Validate.castNonNull;
27
import static com.github.mizosoft.methanol.internal.Validate.requireArgument;
28
import static java.util.Objects.requireNonNull;
29
import static java.util.Objects.requireNonNullElse;
30

31
import com.github.mizosoft.methanol.BodyDecoder.Factory;
32
import com.github.mizosoft.methanol.Methanol.Interceptor.Chain;
33
import com.github.mizosoft.methanol.internal.Utils;
34
import com.github.mizosoft.methanol.internal.adapter.PayloadHandlerExecutor;
35
import com.github.mizosoft.methanol.internal.cache.RedirectingInterceptor;
36
import com.github.mizosoft.methanol.internal.concurrent.Delayer;
37
import com.github.mizosoft.methanol.internal.concurrent.SharedExecutors;
38
import com.github.mizosoft.methanol.internal.extensions.HeadersBuilder;
39
import com.github.mizosoft.methanol.internal.extensions.HttpResponsePublisher;
40
import com.github.mizosoft.methanol.internal.flow.FlowSupport;
41
import com.github.mizosoft.methanol.internal.function.Unchecked;
42
import com.google.errorprone.annotations.CanIgnoreReturnValue;
43
import com.google.errorprone.annotations.InlineMe;
44
import java.io.IOException;
45
import java.lang.System.Logger;
46
import java.lang.System.Logger.Level;
47
import java.lang.invoke.MethodHandle;
48
import java.lang.invoke.MethodHandles;
49
import java.lang.invoke.MethodType;
50
import java.net.Authenticator;
51
import java.net.CookieHandler;
52
import java.net.InetAddress;
53
import java.net.ProxySelector;
54
import java.net.URI;
55
import java.net.http.HttpClient;
56
import java.net.http.HttpHeaders;
57
import java.net.http.HttpRequest;
58
import java.net.http.HttpResponse;
59
import java.net.http.HttpResponse.BodyHandler;
60
import java.net.http.HttpResponse.BodySubscriber;
61
import java.net.http.HttpResponse.PushPromiseHandler;
62
import java.net.http.WebSocket;
63
import java.nio.ByteBuffer;
64
import java.time.Duration;
65
import java.util.ArrayList;
66
import java.util.Collections;
67
import java.util.List;
68
import java.util.Optional;
69
import java.util.concurrent.CancellationException;
70
import java.util.concurrent.CompletableFuture;
71
import java.util.concurrent.CompletionStage;
72
import java.util.concurrent.Executor;
73
import java.util.concurrent.Flow.Publisher;
74
import java.util.concurrent.Flow.Subscription;
75
import java.util.concurrent.ScheduledExecutorService;
76
import java.util.function.Consumer;
77
import java.util.function.Function;
78
import java.util.function.Predicate;
79
import java.util.function.Supplier;
80
import java.util.function.UnaryOperator;
81
import javax.net.ssl.SSLContext;
82
import javax.net.ssl.SSLParameters;
83
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
84
import org.checkerframework.checker.nullness.qual.Nullable;
85

86
/**
87
 * An {@code HttpClient} with interceptors, request decoration, HTTP caching and reactive
88
 * extensions.
89
 *
90
 * <p>In addition to implementing the {@link HttpClient} API, this class allows to:
91
 *
92
 * <ul>
93
 *   <li>Specify a {@link BaseBuilder#baseUri(URI) base URI}.
94
 *   <li>Specify a default {@link HttpRequest#timeout() request timeout}.
95
 *   <li>Specify a read timeout.
96
 *   <li>Add a set of default HTTP headers for inclusion in requests if absent.
97
 *   <li>Add an {@link HttpCache HTTP caching} layer.
98
 *   <li>{@link BaseBuilder#autoAcceptEncoding(boolean) Transparent} response decompression.
99
 *   <li>Intercept requests and responses going through this client.
100
 *   <li>Specify an {@link AdapterCodec} to automatically convert to/from request/response bodies.
101
 *   <li>Get {@code Publisher<HttpResponse<T>>} for asynchronous requests.
102
 * </ul>
103
 *
104
 * <p>A {@code Methanol} client relies on a standard {@code HttpClient} instance for sending
105
 * requests, referred to as its backend. You can obtain builders for {@code Methanol} using either
106
 * {@link #newBuilder()} or {@link #newBuilder(HttpClient)}. The latter takes a prebuilt backend,
107
 * while the former allows configuring a backend to be newly created each time {@link
108
 * BaseBuilder#build()} is invoked. Note that {@code HttpCaches} are not usable with a prebuilt
109
 * backend.
110
 */
111
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
112
public class Methanol extends HttpClient {
113
  private static final Logger logger = System.getLogger(Methanol.class.getName());
1✔
114

115
  private static final @Nullable MethodHandle SHUTDOWN; // Since Java 21.
116
  private static final @Nullable MethodHandle AWAIT_TERMINATION; // Since Java 21.
117
  private static final @Nullable MethodHandle IS_TERMINATED; // Since Java 21.
118
  private static final @Nullable MethodHandle SHUTDOWN_NOW; // Since Java 21.
119
  private static final @Nullable MethodHandle CLOSE; // Since Java 21.
120

121
  static {
122
    var lookup = MethodHandles.lookup();
1✔
123
    MethodHandle shutdown;
124
    try {
125
      shutdown =
1✔
126
          lookup.findVirtual(HttpClient.class, "shutdown", MethodType.methodType(void.class));
1✔
UNCOV
127
    } catch (NoSuchMethodException e) {
×
UNCOV
128
      shutdown = null;
×
129
    } catch (IllegalAccessException e) {
×
130
      throw new IllegalStateException(e);
×
131
    }
1✔
132

133
    if (shutdown == null) {
1!
UNCOV
134
      SHUTDOWN = null;
×
UNCOV
135
      AWAIT_TERMINATION = null;
×
UNCOV
136
      IS_TERMINATED = null;
×
UNCOV
137
      SHUTDOWN_NOW = null;
×
UNCOV
138
      CLOSE = null;
×
139
    } else {
140
      SHUTDOWN = shutdown;
1✔
141
      try {
142
        AWAIT_TERMINATION =
1✔
143
            lookup.findVirtual(
1✔
144
                HttpClient.class,
145
                "awaitTermination",
146
                MethodType.methodType(boolean.class, Duration.class));
1✔
147
        IS_TERMINATED =
1✔
148
            lookup.findVirtual(
1✔
149
                HttpClient.class, "isTerminated", MethodType.methodType(boolean.class));
1✔
150
        SHUTDOWN_NOW =
1✔
151
            lookup.findVirtual(HttpClient.class, "shutdownNow", MethodType.methodType(void.class));
1✔
152
        CLOSE = lookup.findVirtual(HttpClient.class, "close", MethodType.methodType(void.class));
1✔
UNCOV
153
      } catch (NoSuchMethodException | IllegalAccessException e) {
×
UNCOV
154
        throw new IllegalStateException(e);
×
155
      }
1✔
156
    }
157
  }
1✔
158

159
  private final HttpClient backend;
160
  private final Redirect redirectPolicy;
161
  private final HttpHeaders defaultHeaders;
162
  private final Optional<String> userAgent;
163
  private final Optional<URI> baseUri;
164
  private final Optional<Duration> headersTimeout;
165
  private final Optional<Duration> requestTimeout;
166
  private final Optional<Duration> readTimeout;
167
  private final Optional<AdapterCodec> adapterCodec;
168
  private final boolean autoAcceptEncoding;
169
  private final List<Interceptor> interceptors;
170
  private final List<Interceptor> backendInterceptors;
171
  private final List<HttpCache> caches;
172

173
  /** The complete list of interceptors invoked throughout the chain. */
174
  private final List<Interceptor> mergedInterceptors;
175

176
  private Methanol(BaseBuilder<?> builder) {
1✔
177
    backend = builder.buildBackend();
1✔
178
    redirectPolicy = requireNonNullElse(builder.redirectPolicy, backend.followRedirects());
1✔
179
    defaultHeaders = builder.defaultHeadersBuilder.build();
1✔
180
    userAgent = Optional.ofNullable(builder.userAgent);
1✔
181
    baseUri = Optional.ofNullable(builder.baseUri);
1✔
182
    headersTimeout = Optional.ofNullable(builder.headersTimeout);
1✔
183
    requestTimeout = Optional.ofNullable(builder.requestTimeout);
1✔
184
    readTimeout = Optional.ofNullable(builder.readTimeout);
1✔
185
    adapterCodec = Optional.ofNullable(builder.adapterCodec);
1✔
186
    autoAcceptEncoding = builder.autoAcceptEncoding;
1✔
187
    interceptors = List.copyOf(builder.interceptors);
1✔
188
    backendInterceptors = List.copyOf(builder.backendInterceptors);
1✔
189
    caches = builder.caches;
1✔
190

191
    var mergedInterceptors = new ArrayList<>(interceptors);
1✔
192
    mergedInterceptors.add(
1✔
193
        new RewritingInterceptor(
194
            baseUri, requestTimeout, adapterCodec, defaultHeaders, autoAcceptEncoding));
195
    headersTimeout.ifPresent(
1✔
196
        timeout ->
197
            mergedInterceptors.add(
1✔
198
                new HeadersTimeoutInterceptor(
199
                    timeout, castNonNull(builder.headersTimeoutDelayer))));
1✔
200
    readTimeout.ifPresent(
1✔
201
        timeout ->
202
            mergedInterceptors.add(
1✔
203
                new ReadTimeoutInterceptor(timeout, castNonNull(builder.readTimeoutDelayer))));
1✔
204
    if (!caches.isEmpty()) {
1✔
205
      mergedInterceptors.add(
1✔
206
          new RedirectingInterceptor(
207
              redirectPolicy, backend.executor().orElseGet(SharedExecutors::executor)));
1✔
208
    }
209
    caches.forEach(
1✔
210
        cache -> mergedInterceptors.add(cache.interceptor(implicitHeaderPredicateOf(backend))));
1✔
211

212
    mergedInterceptors.addAll(backendInterceptors);
1✔
213
    this.mergedInterceptors = Collections.unmodifiableList(mergedInterceptors);
1✔
214
  }
1✔
215

216
  private static Predicate<String> implicitHeaderPredicateOf(HttpClient client) {
217
    Predicate<String> predicate = name -> name.equalsIgnoreCase("Host");
1✔
218
    if (client.authenticator().isPresent()) {
1✔
219
      predicate =
1✔
220
          predicate.or(
1✔
221
              name ->
222
                  name.equalsIgnoreCase("Authorization")
1✔
223
                      || name.equalsIgnoreCase("Proxy-Authorization"));
1✔
224
    }
225
    if (client.cookieHandler().isPresent()) {
1✔
226
      predicate =
1✔
227
          predicate.or(name -> name.equalsIgnoreCase("Cookie") || name.equalsIgnoreCase("Cookie2"));
1✔
228
    }
229
    return predicate;
1✔
230
  }
231

232
  /**
233
   * Returns a {@code Publisher} for the {@code HttpResponse<T>} resulting from asynchronously
234
   * sending the given request.
235
   */
236
  public <T> Publisher<HttpResponse<T>> exchange(HttpRequest request, BodyHandler<T> bodyHandler) {
237
    return new HttpResponsePublisher<>(
1✔
238
        this, request, bodyHandler, null, executor().orElse(FlowSupport.SYNC_EXECUTOR));
1✔
239
  }
240

241
  /**
242
   * Returns a {@code Publisher} for the sequence of {@code HttpResponse<T>} resulting from
243
   * asynchronously sending the given request along with accepting incoming {@link
244
   * PushPromiseHandler push promises} using the given {@code Function}. The function accepts an
245
   * incoming push promise by returning a non-{@code null} {@code BodyHandler<T>} for handling the
246
   * pushed response body. If a {@code null} handler is returned, the push promise will be rejected.
247
   *
248
   * <p>Note that the published sequence has no specific order, and hence the main response is not
249
   * guaranteed to be the first and may appear anywhere in the sequence.
250
   */
251
  public <T> Publisher<HttpResponse<T>> exchange(
252
      HttpRequest request,
253
      BodyHandler<T> bodyHandler,
254
      Function<HttpRequest, @Nullable BodyHandler<T>> pushPromiseMapper) {
255
    return new HttpResponsePublisher<>(
1✔
256
        this,
257
        request,
258
        bodyHandler,
259
        pushPromiseMapper,
260
        executor().orElse(FlowSupport.SYNC_EXECUTOR));
1✔
261
  }
262

263
  /** Returns the underlying {@code HttpClient} used for sending requests. */
264
  public HttpClient underlyingClient() {
265
    return backend;
1✔
266
  }
267

268
  /** Returns this client's {@code User-Agent}. */
269
  public Optional<String> userAgent() {
270
    return userAgent;
1✔
271
  }
272

273
  /** Returns this client's base URI. */
274
  public Optional<URI> baseUri() {
275
    return baseUri;
1✔
276
  }
277

278
  /** Returns the default request timeout used when not set in an {@code HttpRequest}. */
279
  public Optional<Duration> requestTimeout() {
280
    return requestTimeout;
1✔
281
  }
282

283
  /** Returns the headers timeout. */
284
  public Optional<Duration> headersTimeout() {
285
    return headersTimeout;
1✔
286
  }
287

288
  /**
289
   * Returns the {@link MoreBodySubscribers#withReadTimeout(BodySubscriber, Duration) read timeout}
290
   * used for each request.
291
   */
292
  public Optional<Duration> readTimeout() {
293
    return readTimeout;
1✔
294
  }
295

296
  /**
297
   * Returns an immutable list of this client's {@link BaseBuilder#interceptor(Interceptor)
298
   * interceptors}.
299
   */
300
  public List<Interceptor> interceptors() {
301
    return interceptors;
1✔
302
  }
303

304
  /**
305
   * Returns an immutable list of this client's {@link BaseBuilder#backendInterceptor(Interceptor)
306
   * backend interceptors}.
307
   */
308
  public List<Interceptor> backendInterceptors() {
309
    return backendInterceptors;
1✔
310
  }
311

312
  /**
313
   * Returns the list of interceptors invoked after request decoration.
314
   *
315
   * @deprecated Use {@link #backendInterceptors()}
316
   */
317
  @Deprecated(since = "1.5.0")
318
  public List<Interceptor> postDecorationInterceptors() {
UNCOV
319
    return backendInterceptors;
×
320
  }
321

322
  /** Returns this client's default headers. */
323
  public HttpHeaders defaultHeaders() {
324
    return defaultHeaders;
1✔
325
  }
326

327
  /** Returns this client's {@link Builder#autoAcceptEncoding auto Accept-Encoding} setting. */
328
  public boolean autoAcceptEncoding() {
329
    return autoAcceptEncoding;
1✔
330
  }
331

332
  /** Returns this client's {@link HttpCache cache}. */
333
  public Optional<HttpCache> cache() {
334
    return caches.stream().findFirst();
1✔
335
  }
336

337
  public List<HttpCache> caches() {
338
    return caches;
1✔
339
  }
340

341
  public Optional<AdapterCodec> adapterCodec() {
342
    return adapterCodec;
1✔
343
  }
344

345
  @Override
346
  public Optional<CookieHandler> cookieHandler() {
347
    return backend.cookieHandler();
1✔
348
  }
349

350
  @Override
351
  public Optional<Duration> connectTimeout() {
352
    return backend.connectTimeout();
1✔
353
  }
354

355
  @Override
356
  public Redirect followRedirects() {
357
    return redirectPolicy;
1✔
358
  }
359

360
  @Override
361
  public Optional<ProxySelector> proxy() {
362
    return backend.proxy();
1✔
363
  }
364

365
  @Override
366
  public SSLContext sslContext() {
367
    return backend.sslContext();
1✔
368
  }
369

370
  @Override
371
  public SSLParameters sslParameters() {
UNCOV
372
    return backend.sslParameters();
×
373
  }
374

375
  @Override
376
  public Optional<Authenticator> authenticator() {
377
    return backend.authenticator();
1✔
378
  }
379

380
  @Override
381
  public Version version() {
382
    return backend.version();
1✔
383
  }
384

385
  @Override
386
  public Optional<Executor> executor() {
387
    return backend.executor();
1✔
388
  }
389

390
  @Override
391
  public WebSocket.Builder newWebSocketBuilder() {
392
    return backend.newWebSocketBuilder();
1✔
393
  }
394

395
  @Override
396
  public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> bodyHandler)
397
      throws IOException, InterruptedException {
398
    return new InterceptorChain<>(backend, bodyHandler, null, mergedInterceptors).forward(request);
1✔
399
  }
400

401
  @Override
402
  public <T> CompletableFuture<HttpResponse<T>> sendAsync(
403
      HttpRequest request, BodyHandler<T> bodyHandler) {
404
    return new InterceptorChain<>(backend, bodyHandler, null, mergedInterceptors)
1✔
405
        .forwardAsync(request);
1✔
406
  }
407

408
  @Override
409
  public <T> CompletableFuture<HttpResponse<T>> sendAsync(
410
      HttpRequest request,
411
      BodyHandler<T> bodyHandler,
412
      @Nullable PushPromiseHandler<T> pushPromiseHandler) {
413
    return new InterceptorChain<>(backend, bodyHandler, pushPromiseHandler, mergedInterceptors)
1✔
414
        .forwardAsync(request);
1✔
415
  }
416

417
  /**
418
   * {@link #send(HttpRequest, BodyHandler) Sends} the given request and converts the response body
419
   * into an object of the given type.
420
   */
421
  public <T> HttpResponse<T> send(HttpRequest request, Class<T> type)
422
      throws IOException, InterruptedException {
423
    return send(request, TypeRef.of(type));
1✔
424
  }
425

426
  /**
427
   * {@link #sendAsync(HttpRequest, BodyHandler) Asynchronously sends} the given request and
428
   * converts the response body into an object of the given type.
429
   */
430
  public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, Class<T> type) {
431
    return sendAsync(request, TypeRef.of(type));
1✔
432
  }
433

434
  /**
435
   * {@link #send(HttpRequest, BodyHandler) Sends} the given request and converts the response body
436
   * into an object of the given type.
437
   */
438
  public <T> HttpResponse<T> send(HttpRequest request, TypeRef<T> typeRef)
439
      throws IOException, InterruptedException {
440
    return new InterceptorChain<>(backend, handlerOf(request, typeRef), null, mergedInterceptors)
1✔
441
        .forward(request);
1✔
442
  }
443

444
  /**
445
   * {@link #sendAsync(HttpRequest, BodyHandler) Asynchronously sends} the given request and
446
   * converts the response body into an object of the given type.
447
   */
448
  public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, TypeRef<T> typeRef) {
449
    return new InterceptorChain<>(backend, handlerOf(request, typeRef), null, mergedInterceptors)
1✔
450
        .forwardAsync(request);
1✔
451
  }
452

453
  private <T> BodyHandler<T> handlerOf(HttpRequest request, TypeRef<T> typeRef) {
454
    var adapterCodec = MutableRequest.adapterCodecOf(request).or(() -> this.adapterCodec);
1✔
455
    var hints = TaggableRequest.hintsOf(request);
1✔
456
    var deferredValueTypeRef =
457
        typeRef.isParameterizedType() && typeRef.rawType() == Supplier.class
1✔
458
            ? typeRef
459
                .resolveSupertype(Supplier.class)
1✔
460
                .typeArgumentAt(0)
1✔
461
                .orElseThrow(AssertionError::new) // A parameterized type must contain a type arg.
1✔
462
            : null;
1✔
463
    if ((typeRef.isRawType() && typeRef.rawType() == ResponsePayload.class)
1✔
464
        || (deferredValueTypeRef != null
465
            && deferredValueTypeRef.isRawType()
1✔
466
            && deferredValueTypeRef.rawType() == ResponsePayload.class)) {
1!
467
      // Add ResponsePayload-specific hints (see BasicAdapter.Decoder).
468
      var hintsBuilder = hints.mutate();
1✔
469
      adapterCodec.ifPresent(codec -> hintsBuilder.put(AdapterCodec.class, codec));
1✔
470
      executor()
1✔
471
          .ifPresent(
1✔
472
              executor ->
UNCOV
473
                  hintsBuilder.put(
×
474
                      PayloadHandlerExecutor.class, new PayloadHandlerExecutor(executor)));
475
      hints = hintsBuilder.build();
1✔
476
    }
477

478
    var effectiveAdapterCodec = adapterCodec.orElseGet(AdapterCodec::installed);
1✔
479
    if (deferredValueTypeRef != null) {
1✔
480
      @SuppressWarnings("unchecked")
481
      var bodyHandler =
1✔
482
          (BodyHandler<T>) effectiveAdapterCodec.deferredHandlerOf(deferredValueTypeRef, hints);
1✔
483
      return bodyHandler;
1✔
484
    } else {
485
      return effectiveAdapterCodec.handlerOf(typeRef, hints);
1✔
486
    }
487
  }
488

489
  @CanIgnoreReturnValue
490
  private static URI validateUri(URI uri) {
491
    var scheme = uri.getScheme();
1✔
492
    requireArgument(scheme != null, "URI has no scheme: %s", uri);
1✔
493
    requireArgument(
1✔
494
        scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"),
1✔
495
        "Unsupported scheme: %s",
496
        scheme);
497
    requireArgument(uri.getHost() != null, "URI has no host: %s", uri);
1✔
498
    return uri;
1✔
499
  }
500

501
  private static <T> PushPromiseHandler<T> transformPushPromiseHandler(
502
      PushPromiseHandler<T> pushPromiseHandler,
503
      UnaryOperator<BodyHandler<T>> bodyHandlerTransformer,
504
      UnaryOperator<HttpResponse<T>> responseTransformer) {
505
    return (initialRequest, pushRequest, acceptor) ->
1✔
506
        pushPromiseHandler.applyPushPromise(
1✔
507
            initialRequest,
508
            pushRequest,
509
            acceptor
510
                .compose(bodyHandlerTransformer)
1✔
511
                .andThen(future -> future.thenApply(responseTransformer)));
1✔
512
  }
513

514
  /** Returns a new {@link Methanol.Builder}. */
515
  public static Builder newBuilder() {
516
    return Builder.create();
1✔
517
  }
518

519
  /** Returns a new {@link Methanol.WithClientBuilder} with a prebuilt backend. */
520
  public static WithClientBuilder newBuilder(HttpClient backend) {
521
    return new WithClientBuilder(backend);
1✔
522
  }
523

524
  /** Creates a default {@code Methanol} instance. */
525
  public static Methanol create() {
526
    return newBuilder().build();
1✔
527
  }
528

529
  private static final class MethanolForJava21AndLater extends Methanol {
530
    MethanolForJava21AndLater(BaseBuilder<?> builder) {
531
      super(builder);
1✔
532
    }
1✔
533

534
    // @Override
535
    @SuppressWarnings({"Since15", "UnusedMethod", "EffectivelyPrivate"})
536
    public void shutdown() {
537
      try {
538
        castNonNull(SHUTDOWN).invokeExact(underlyingClient());
1✔
539
      } catch (Throwable e) {
1✔
UNCOV
540
        Unchecked.propagateIfUnchecked(e);
×
UNCOV
541
        throw new RuntimeException(e);
×
542
      }
1✔
543
    }
1✔
544

545
    // @Override
546
    @SuppressWarnings({"Since15", "UnusedMethod", "EffectivelyPrivate"})
547
    public boolean awaitTermination(Duration duration) throws InterruptedException {
548
      try {
549
        return (boolean) castNonNull(AWAIT_TERMINATION).invokeExact(underlyingClient(), duration);
1✔
550
      } catch (Throwable e) {
1✔
551
        Unchecked.propagateIfUnchecked(e);
1✔
552
        if (e instanceof InterruptedException) {
1!
553
          throw (InterruptedException) e;
1✔
554
        }
UNCOV
555
        throw new RuntimeException(e);
×
556
      }
557
    }
558

559
    // @Override
560
    @SuppressWarnings({"Since15", "UnusedMethod", "EffectivelyPrivate"})
561
    public boolean isTerminated() {
562
      try {
563
        return (boolean) castNonNull(IS_TERMINATED).invokeExact(underlyingClient());
1✔
564
      } catch (Throwable e) {
1✔
UNCOV
565
        Unchecked.propagateIfUnchecked(e);
×
UNCOV
566
        throw new RuntimeException(e);
×
567
      }
568
    }
569

570
    // @Override
571
    @SuppressWarnings({"Since15", "UnusedMethod", "EffectivelyPrivate"})
572
    public void shutdownNow() {
573
      try {
574
        castNonNull(SHUTDOWN_NOW).invokeExact(underlyingClient());
1✔
575
      } catch (Throwable e) {
1✔
UNCOV
576
        Unchecked.propagateIfUnchecked(e);
×
UNCOV
577
        throw new RuntimeException(e);
×
578
      }
1✔
579
    }
1✔
580

581
    // @Override
582
    @SuppressWarnings({"UnusedMethod", "EffectivelyPrivate"})
583
    public void close() {
584
      try {
585
        castNonNull(CLOSE).invokeExact(underlyingClient());
×
UNCOV
586
      } catch (Throwable e) {
×
UNCOV
587
        Unchecked.propagateIfUnchecked(e);
×
UNCOV
588
        throw new RuntimeException(e);
×
UNCOV
589
      }
×
UNCOV
590
    }
×
591
  }
592

593
  /** An object that intercepts requests before being sent and responses before being returned. */
594
  public interface Interceptor {
595

596
    /**
597
     * Intercepts given request and returns the resulting response, usually by forwarding to the
598
     * given chain.
599
     */
600
    <T> HttpResponse<T> intercept(HttpRequest request, Chain<T> chain)
601
        throws IOException, InterruptedException;
602

603
    /**
604
     * Intercepts the given request and returns a {@code CompletableFuture} for the resulting
605
     * response, usually by forwarding to the given chain.
606
     */
607
    <T> CompletableFuture<HttpResponse<T>> interceptAsync(HttpRequest request, Chain<T> chain);
608

609
    /** Returns an interceptor that forwards the request after applying the given operator. */
610
    static Interceptor create(Function<HttpRequest, HttpRequest> operator) {
611
      requireNonNull(operator);
1✔
612
      return new Interceptor() {
1✔
613
        @Override
614
        public <T> HttpResponse<T> intercept(HttpRequest request, Chain<T> chain)
615
            throws IOException, InterruptedException {
616
          return chain.forward(operator.apply(request));
1✔
617
        }
618

619
        @Override
620
        public <T> CompletableFuture<HttpResponse<T>> interceptAsync(
621
            HttpRequest request, Chain<T> chain) {
622
          return chain.forwardAsync(operator.apply(request));
1✔
623
        }
624
      };
625
    }
626

627
    /**
628
     * An object that gives interceptors the ability to relay requests to sibling interceptors, till
629
     * eventually being sent by the client's backend.
630
     *
631
     * @param <T> the response body type
632
     */
633
    interface Chain<T> {
634

635
      /** Returns the {@code BodyHandler} this chain uses for handling the response. */
636
      BodyHandler<T> bodyHandler();
637

638
      /** Returns the {@code PushPromiseHandler} this chain uses for handling push promises. */
639
      Optional<PushPromiseHandler<T>> pushPromiseHandler();
640

641
      /** Returns a new chain that uses the given {@code BodyHandler}. */
642
      Chain<T> withBodyHandler(BodyHandler<T> bodyHandler);
643

644
      /** Returns a new chain that uses the given {@code PushPromiseHandler}. */
645
      Chain<T> withPushPromiseHandler(@Nullable PushPromiseHandler<T> pushPromiseHandler);
646

647
      /** Returns a new chain that uses given handlers, possibly targeting another response type. */
648
      default <U> Chain<U> with(
649
          BodyHandler<U> bodyHandler, @Nullable PushPromiseHandler<U> pushPromiseHandler) {
UNCOV
650
        throw new UnsupportedOperationException();
×
651
      }
652

653
      /** Returns a new chain after applying the given function to this chain's body handler. */
654
      default Chain<T> with(UnaryOperator<BodyHandler<T>> bodyHandlerTransformer) {
655
        return withBodyHandler(bodyHandlerTransformer.apply(bodyHandler()));
1✔
656
      }
657

658
      /**
659
       * Returns a new chain after applying the given functions to this chain's body and push
660
       * promise handlers, and only to the latter if a push promise handler is present.
661
       */
662
      default Chain<T> with(
663
          UnaryOperator<BodyHandler<T>> bodyHandlerTransformer,
664
          UnaryOperator<PushPromiseHandler<T>> pushPromiseHandlerTransformer) {
665
        return with(
1✔
666
            bodyHandlerTransformer.apply(bodyHandler()),
1✔
667
            pushPromiseHandler().map(pushPromiseHandlerTransformer).orElse(null));
1✔
668
      }
669

670
      /**
671
       * Forwards the request to the next interceptor or to the client's backend if called by the
672
       * last interceptor.
673
       */
674
      HttpResponse<T> forward(HttpRequest request) throws IOException, InterruptedException;
675

676
      /**
677
       * Forwards the request to the next interceptor, or asynchronously to the client's backend if
678
       * called by the last interceptor.
679
       */
680
      CompletableFuture<HttpResponse<T>> forwardAsync(HttpRequest request);
681
    }
682
  }
683

684
  /** A base {@code Methanol} builder allowing to set the non-standard properties. */
685
  public abstract static class BaseBuilder<B extends BaseBuilder<B>> {
686
    final HeadersBuilder defaultHeadersBuilder = new HeadersBuilder();
1✔
687

688
    @MonotonicNonNull String userAgent;
689
    @MonotonicNonNull URI baseUri;
690
    @MonotonicNonNull Duration requestTimeout;
691
    @MonotonicNonNull Duration headersTimeout;
692
    @MonotonicNonNull Delayer headersTimeoutDelayer;
693
    @MonotonicNonNull Duration readTimeout;
694
    @MonotonicNonNull Delayer readTimeoutDelayer;
695
    @MonotonicNonNull AdapterCodec adapterCodec;
696
    boolean autoAcceptEncoding = true;
1✔
697

698
    final List<Interceptor> interceptors = new ArrayList<>();
1✔
699
    final List<Interceptor> backendInterceptors = new ArrayList<>();
1✔
700

701
    // These fields are put here for convenience, they're only writable by Builder.
702
    List<HttpCache> caches = List.of();
1✔
703
    @MonotonicNonNull Redirect redirectPolicy;
704

705
    BaseBuilder() {}
1✔
706

707
    /** Calls the given consumer against this builder. */
708
    @CanIgnoreReturnValue
709
    public final B apply(Consumer<? super B> consumer) {
710
      consumer.accept(self());
1✔
711
      return self();
1✔
712
    }
713

714
    /**
715
     * Sets a default {@code User-Agent} header to use when sending requests.
716
     *
717
     * @throws IllegalArgumentException if {@code userAgent} is an invalid header value
718
     */
719
    @CanIgnoreReturnValue
720
    public B userAgent(String userAgent) {
721
      defaultHeadersBuilder.set("User-Agent", userAgent);
1✔
722
      this.userAgent = userAgent;
1✔
723
      return self();
1✔
724
    }
725

726
    /**
727
     * Sets the base {@code URI} with which each outgoing requests' {@code URI} is {@link
728
     * URI#resolve(URI) resolved}.
729
     */
730
    @CanIgnoreReturnValue
731
    public B baseUri(String uri) {
732
      return baseUri(URI.create(uri));
1✔
733
    }
734

735
    /**
736
     * Sets the base {@code URI} with which each outgoing requests' {@code URI} is {@link
737
     * URI#resolve(URI) resolved}.
738
     */
739
    @CanIgnoreReturnValue
740
    public B baseUri(URI uri) {
741
      this.baseUri = validateUri(uri);
1✔
742
      return self();
1✔
743
    }
744

745
    /** Adds the given default header. */
746
    @CanIgnoreReturnValue
747
    public B defaultHeader(String name, String value) {
748
      defaultHeadersBuilder.add(name, value);
1✔
749
      if (name.equalsIgnoreCase("User-Agent")) {
1✔
750
        userAgent = value;
1✔
751
      }
752
      return self();
1✔
753
    }
754

755
    /** Adds each of the given default headers. */
756
    @CanIgnoreReturnValue
757
    public B defaultHeaders(String... headers) {
758
      requireArgument(
1!
759
          headers.length > 0 && headers.length % 2 == 0,
760
          "Illegal number of headers: %d",
761
          headers.length);
1✔
762
      for (int i = 0; i < headers.length; i += 2) {
1✔
763
        defaultHeader(headers[i], headers[i + 1]);
1✔
764
      }
765
      return self();
1✔
766
    }
767

768
    /** Configures the default headers as specified by the given consumer. */
769
    @CanIgnoreReturnValue
770
    public B defaultHeaders(Consumer<HeadersAccumulator<?>> configurator) {
771
      configurator.accept(defaultHeadersBuilder.asHeadersAccumulator());
1✔
772
      defaultHeadersBuilder
1✔
773
          .lastValue("User-Agent")
1✔
774
          .ifPresent(userAgent -> this.userAgent = userAgent);
1✔
775
      return self();
1✔
776
    }
777

778
    /** Sets a default request timeout to use when not explicitly by an {@code HttpRequest}. */
779
    @CanIgnoreReturnValue
780
    public B requestTimeout(Duration requestTimeout) {
781
      this.requestTimeout = requirePositiveDuration(requestTimeout);
1✔
782
      return self();
1✔
783
    }
784

785
    /**
786
     * Sets a timeout that will raise an {@link HttpHeadersTimeoutException} if all response headers
787
     * aren't received within the timeout. Timeout events are scheduled using a system-wide {@code
788
     * ScheduledExecutorService}.
789
     */
790
    @CanIgnoreReturnValue
791
    public B headersTimeout(Duration headersTimeout) {
792
      return headersTimeout(headersTimeout, Delayer.defaultDelayer());
1✔
793
    }
794

795
    /**
796
     * Same as {@link #headersTimeout(Duration)} but specifies a {@code ScheduledExecutorService} to
797
     * use for scheduling timeout events.
798
     */
799
    @CanIgnoreReturnValue
800
    public B headersTimeout(Duration headersTimeout, ScheduledExecutorService scheduler) {
UNCOV
801
      return headersTimeout(headersTimeout, Delayer.of(scheduler));
×
802
    }
803

804
    @CanIgnoreReturnValue
805
    B headersTimeout(Duration headersTimeout, Delayer delayer) {
806
      this.headersTimeout = requirePositiveDuration(headersTimeout);
1✔
807
      this.headersTimeoutDelayer = requireNonNull(delayer);
1✔
808
      return self();
1✔
809
    }
810

811
    /**
812
     * Sets a default {@link MoreBodySubscribers#withReadTimeout(BodySubscriber, Duration) read
813
     * timeout}. Timeout events are scheduled using a system-wide {@code ScheduledExecutorService}.
814
     */
815
    @CanIgnoreReturnValue
816
    public B readTimeout(Duration readTimeout) {
817
      return readTimeout(readTimeout, Delayer.defaultDelayer());
1✔
818
    }
819

820
    /**
821
     * Sets a default {@link MoreBodySubscribers#withReadTimeout(BodySubscriber, Duration,
822
     * ScheduledExecutorService) readtimeout} using the given {@code ScheduledExecutorService} for
823
     * scheduling timeout events.
824
     */
825
    @CanIgnoreReturnValue
826
    public B readTimeout(Duration readTimeout, ScheduledExecutorService scheduler) {
827
      return readTimeout(readTimeout, Delayer.of(scheduler));
1✔
828
    }
829

830
    @CanIgnoreReturnValue
831
    private B readTimeout(Duration readTimeout, Delayer delayer) {
832
      this.readTimeout = requirePositiveDuration(readTimeout);
1✔
833
      this.readTimeoutDelayer = requireNonNull(delayer);
1✔
834
      return self();
1✔
835
    }
836

837
    /** Specifies the {@code AdapterCodec} with which request and response payloads are mapped. */
838
    @CanIgnoreReturnValue
839
    public B adapterCodec(AdapterCodec adapterCodec) {
840
      this.adapterCodec = requireNonNull(adapterCodec);
1✔
841
      return self();
1✔
842
    }
843

844
    /**
845
     * If enabled, each request will have an {@code Accept-Encoding} header appended, the value of
846
     * which is the set of {@link Factory#installedBindings() supported encodings}. Additionally,
847
     * each received response will be transparently decompressed by wrapping its {@code BodyHandler}
848
     * with {@link MoreBodyHandlers#decoding(BodyHandler)}.
849
     *
850
     * <p>This value is {@code true} by default.
851
     */
852
    @CanIgnoreReturnValue
853
    public B autoAcceptEncoding(boolean autoAcceptEncoding) {
854
      this.autoAcceptEncoding = autoAcceptEncoding;
1✔
855
      return self();
1✔
856
    }
857

858
    /**
859
     * Adds an interceptor that is invoked right after the client receives a request. The
860
     * interceptor receives the request before it is decorated (its {@code URI} resolved with the
861
     * base {@code URI}, default headers added, etc...) or handled by an {@link HttpCache}.
862
     */
863
    @CanIgnoreReturnValue
864
    public B interceptor(Interceptor interceptor) {
865
      interceptors.add(requireNonNull(interceptor));
1✔
866
      return self();
1✔
867
    }
868

869
    /**
870
     * Adds an interceptor that is invoked right before the request is forwarded to the client's
871
     * backend. The interceptor receives the request after it is handled by all {@link
872
     * #interceptor(Interceptor) client interceptors}, is decorated (its {@code URI} resolved with
873
     * the base {@code URI}, default headers added, etc...) and finally handled by an {@link
874
     * HttpCache}. This implies that backend interceptors aren't called if network isn't used,
875
     * normally due to the presence of an {@code HttpCache} that is capable of serving a stored
876
     * response.
877
     */
878
    @CanIgnoreReturnValue
879
    public B backendInterceptor(Interceptor interceptor) {
880
      backendInterceptors.add(requireNonNull(interceptor));
1✔
881
      return self();
1✔
882
    }
883

884
    /**
885
     * @deprecated Use {@link #backendInterceptor(Interceptor)}
886
     */
887
    @CanIgnoreReturnValue
888
    @Deprecated(since = "1.5.0")
889
    @InlineMe(replacement = "this.backendInterceptor(interceptor)")
890
    public final B postDecorationInterceptor(Interceptor interceptor) {
UNCOV
891
      return backendInterceptor(interceptor);
×
892
    }
893

894
    /** Creates a new {@code Methanol} instance. */
895
    public Methanol build() {
896
      return SHUTDOWN != null ? new MethanolForJava21AndLater(this) : new Methanol(this);
1!
897
    }
898

899
    abstract B self();
900

901
    abstract HttpClient buildBackend();
902
  }
903

904
  /** A builder for {@code Methanol} instances with a pre-specified backend {@code HttpClient}. */
905
  public static final class WithClientBuilder extends BaseBuilder<WithClientBuilder> {
906
    private final HttpClient backend;
907

908
    WithClientBuilder(HttpClient backend) {
1✔
909
      this.backend = requireNonNull(backend);
1✔
910
    }
1✔
911

912
    @Override
913
    WithClientBuilder self() {
914
      return this;
1✔
915
    }
916

917
    @Override
918
    HttpClient buildBackend() {
919
      return backend;
1✔
920
    }
921
  }
922

923
  /** A builder of {@code Methanol} instances. */
924
  public static class Builder extends BaseBuilder<Builder> implements HttpClient.Builder {
925
    private static final @Nullable MethodHandle LOCAL_ADDRESS; // Since Java 19.
926

927
    static {
928
      MethodHandle localAddress;
929
      try {
930
        localAddress =
931
            MethodHandles.lookup()
1✔
932
                .findVirtual(
1✔
933
                    HttpClient.Builder.class,
934
                    "localAddress",
935
                    MethodType.methodType(HttpClient.Builder.class, InetAddress.class));
1✔
936
      } catch (NoSuchMethodException e) {
×
UNCOV
937
        localAddress = null;
×
UNCOV
938
      } catch (IllegalAccessException e) {
×
UNCOV
939
        throw new IllegalStateException(e);
×
940
      }
1✔
941
      LOCAL_ADDRESS = localAddress;
1✔
942
    }
1✔
943

944
    final HttpClient.Builder backendBuilder = HttpClient.newBuilder();
1✔
945

946
    private Builder() {}
1✔
947

948
    /** Sets the {@link HttpCache} to be used by the client. */
949
    @CanIgnoreReturnValue
950
    public Builder cache(HttpCache cache) {
951
      super.caches = List.of(cache);
1✔
952
      return this;
1✔
953
    }
954

955
    /**
956
     * Sets a chain of caches to be called one after another, in the order specified by the given
957
     * list. Each cache forwards to the other till a suitable response is found or the request is
958
     * sent to network. Although not enforced, it is highly recommended for the caches to be sorted
959
     * in the order of decreasing locality.
960
     */
961
    @CanIgnoreReturnValue
962
    public Builder cacheChain(List<HttpCache> caches) {
963
      var cachesCopy = List.copyOf(caches);
1✔
964
      requireArgument(!cachesCopy.isEmpty(), "Must have at least one cache in the chain");
1!
965
      this.caches = cachesCopy;
1✔
966
      return this;
1✔
967
    }
968

969
    @Override
970
    @CanIgnoreReturnValue
971
    public Builder cookieHandler(CookieHandler cookieHandler) {
972
      backendBuilder.cookieHandler(cookieHandler);
1✔
973
      return this;
1✔
974
    }
975

976
    @Override
977
    @CanIgnoreReturnValue
978
    public Builder connectTimeout(Duration duration) {
979
      backendBuilder.connectTimeout(duration);
1✔
980
      return this;
1✔
981
    }
982

983
    @Override
984
    @CanIgnoreReturnValue
985
    public Builder sslContext(SSLContext sslContext) {
986
      backendBuilder.sslContext(sslContext);
1✔
987
      return this;
1✔
988
    }
989

990
    @Override
991
    @CanIgnoreReturnValue
992
    public Builder sslParameters(SSLParameters sslParameters) {
UNCOV
993
      backendBuilder.sslParameters(sslParameters);
×
UNCOV
994
      return this;
×
995
    }
996

997
    @Override
998
    @CanIgnoreReturnValue
999
    public Builder executor(Executor executor) {
1000
      backendBuilder.executor(executor);
1✔
1001
      return this;
1✔
1002
    }
1003

1004
    @Override
1005
    @CanIgnoreReturnValue
1006
    public Builder followRedirects(Redirect policy) {
1007
      // Don't apply policy to base client until build() is called to know whether
1008
      // a RedirectingInterceptor is to be used instead in case a cache is installed.
1009
      redirectPolicy = requireNonNull(policy);
1✔
1010
      return this;
1✔
1011
    }
1012

1013
    @Override
1014
    @CanIgnoreReturnValue
1015
    public Builder version(Version version) {
1016
      backendBuilder.version(version);
1✔
1017
      return this;
1✔
1018
    }
1019

1020
    @Override
1021
    @CanIgnoreReturnValue
1022
    public Builder priority(int priority) {
UNCOV
1023
      backendBuilder.priority(priority);
×
UNCOV
1024
      return this;
×
1025
    }
1026

1027
    @Override
1028
    @CanIgnoreReturnValue
1029
    public Builder proxy(ProxySelector proxySelector) {
1030
      backendBuilder.proxy(proxySelector);
1✔
1031
      return this;
1✔
1032
    }
1033

1034
    @Override
1035
    @CanIgnoreReturnValue
1036
    public Builder authenticator(Authenticator authenticator) {
1037
      backendBuilder.authenticator(authenticator);
1✔
1038
      return this;
1✔
1039
    }
1040

1041
    @Override
1042
    Builder self() {
1043
      return this;
1✔
1044
    }
1045

1046
    @Override
1047
    HttpClient buildBackend() {
1048
      // Apply redirectPolicy if no caches are set. In such case we let the backend handle
1049
      // redirects.
1050
      if (caches.isEmpty() && redirectPolicy != null) {
1✔
1051
        backendBuilder.followRedirects(redirectPolicy);
1✔
1052
      }
1053
      return backendBuilder.build();
1✔
1054
    }
1055

1056
    static Builder create() {
1057
      return LOCAL_ADDRESS != null ? new BuilderForJava19AndLater() : new Builder();
1!
1058
    }
1059

1060
    private static final class BuilderForJava19AndLater extends Builder {
1061
      BuilderForJava19AndLater() {}
1✔
1062

1063
      // Note that the return type MUST be exactly the same as the overridden method's return type
1064
      // (in HttpClient.Builder). This is because we compile under Java 11 (or later with a
1065
      // --release 11 flag). When the override is covariant, the JVM will call the super interface
1066
      // method and not this one, because the exact return type is part of the method signature, and
1067
      // the compiler doesn't generate a synthetic method with the overridden function's signature
1068
      // because it didn't exist while compiling.
1069
      // @Override
1070
      @SuppressWarnings({"Since15", "UnusedMethod", "EffectivelyPrivate"})
1071
      public HttpClient.Builder localAddress(InetAddress localAddr) {
1072
        try {
1073
          castNonNull(LOCAL_ADDRESS).invoke(backendBuilder, localAddr);
1✔
1074
          return this;
1✔
UNCOV
1075
        } catch (Throwable e) {
×
UNCOV
1076
          Unchecked.propagateIfUnchecked(e);
×
UNCOV
1077
          throw new RuntimeException(e);
×
1078
        }
1079
      }
1080
    }
1081
  }
1082

1083
  private static final class InterceptorChain<T> implements Interceptor.Chain<T> {
1084
    private final HttpClient backend;
1085
    private final BodyHandler<T> bodyHandler;
1086
    private final @Nullable PushPromiseHandler<T> pushPromiseHandler;
1087
    private final List<Interceptor> interceptors;
1088
    private final int currentInterceptorIndex;
1089

1090
    InterceptorChain(
1091
        HttpClient backend,
1092
        BodyHandler<T> bodyHandler,
1093
        @Nullable PushPromiseHandler<T> pushPromiseHandler,
1094
        List<Interceptor> interceptors) {
1095
      this(backend, bodyHandler, pushPromiseHandler, interceptors, 0);
1✔
1096
    }
1✔
1097

1098
    private InterceptorChain(
1099
        HttpClient backend,
1100
        BodyHandler<T> bodyHandler,
1101
        @Nullable PushPromiseHandler<T> pushPromiseHandler,
1102
        List<Interceptor> interceptors,
1103
        int currentInterceptorIndex) {
1✔
1104
      this.backend = requireNonNull(backend);
1✔
1105
      this.bodyHandler = requireNonNull(bodyHandler);
1✔
1106
      this.pushPromiseHandler = pushPromiseHandler;
1✔
1107
      this.interceptors = requireNonNull(interceptors);
1✔
1108
      this.currentInterceptorIndex = currentInterceptorIndex;
1✔
1109
    }
1✔
1110

1111
    @Override
1112
    public BodyHandler<T> bodyHandler() {
1113
      return bodyHandler;
1✔
1114
    }
1115

1116
    @Override
1117
    public Optional<PushPromiseHandler<T>> pushPromiseHandler() {
1118
      return Optional.ofNullable(pushPromiseHandler);
1✔
1119
    }
1120

1121
    @Override
1122
    public Interceptor.Chain<T> withBodyHandler(BodyHandler<T> bodyHandler) {
1123
      return new InterceptorChain<>(
1✔
1124
          backend, bodyHandler, pushPromiseHandler, interceptors, currentInterceptorIndex);
1125
    }
1126

1127
    @Override
1128
    public Interceptor.Chain<T> withPushPromiseHandler(
1129
        @Nullable PushPromiseHandler<T> pushPromiseHandler) {
1130
      return new InterceptorChain<>(
1✔
1131
          backend, bodyHandler, pushPromiseHandler, interceptors, currentInterceptorIndex);
1132
    }
1133

1134
    @Override
1135
    public <U> Chain<U> with(
1136
        BodyHandler<U> bodyHandler, @Nullable PushPromiseHandler<U> pushPromiseHandler) {
1137
      return new InterceptorChain<>(
1✔
1138
          backend, bodyHandler, pushPromiseHandler, interceptors, currentInterceptorIndex);
1139
    }
1140

1141
    @Override
1142
    public HttpResponse<T> forward(HttpRequest request) throws IOException, InterruptedException {
1143
      requireNonNull(request);
1✔
1144
      return currentInterceptorIndex >= interceptors.size()
1✔
1145
          ? backend.send(request, bodyHandler)
1✔
1146
          : interceptors.get(currentInterceptorIndex).intercept(request, nextInterceptorChain());
1✔
1147
    }
1148

1149
    @Override
1150
    public CompletableFuture<HttpResponse<T>> forwardAsync(HttpRequest request) {
1151
      requireNonNull(request);
1✔
1152
      return currentInterceptorIndex >= interceptors.size()
1✔
1153
          ? backend.sendAsync(request, bodyHandler, pushPromiseHandler)
1✔
1154
          : interceptors
1155
              .get(currentInterceptorIndex)
1✔
1156
              .interceptAsync(request, nextInterceptorChain());
1✔
1157
    }
1158

1159
    private InterceptorChain<T> nextInterceptorChain() {
1160
      return new InterceptorChain<>(
1✔
1161
          backend, bodyHandler, pushPromiseHandler, interceptors, currentInterceptorIndex + 1);
1162
    }
1163
  }
1164

1165
  /** An interceptor that rewrites requests and responses as configured. */
1166
  private static final class RewritingInterceptor implements Interceptor {
1167
    private final Optional<URI> baseUri;
1168
    private final Optional<Duration> requestTimeout;
1169
    private final Optional<AdapterCodec> adapterCodec;
1170
    private final HttpHeaders defaultHeaders;
1171
    private final boolean autoAcceptEncoding;
1172

1173
    RewritingInterceptor(
1174
        Optional<URI> baseUri,
1175
        Optional<Duration> requestTimeout,
1176
        Optional<AdapterCodec> adapterCodec,
1177
        HttpHeaders defaultHeaders,
1178
        boolean autoAcceptEncoding) {
1✔
1179
      this.baseUri = baseUri;
1✔
1180
      this.requestTimeout = requestTimeout;
1✔
1181
      this.adapterCodec = adapterCodec;
1✔
1182
      this.defaultHeaders = defaultHeaders;
1✔
1183
      this.autoAcceptEncoding = autoAcceptEncoding;
1✔
1184
    }
1✔
1185

1186
    @Override
1187
    public <T> HttpResponse<T> intercept(HttpRequest request, Chain<T> chain)
1188
        throws IOException, InterruptedException {
1189
      var rewrittenRequest = rewriteRequest(request);
1✔
1190
      return autoAcceptEncoding(rewrittenRequest)
1✔
1191
          ? stripContentEncoding(decoding(chain).forward(rewrittenRequest))
1✔
1192
          : chain.forward(rewrittenRequest);
1✔
1193
    }
1194

1195
    @Override
1196
    public <T> CompletableFuture<HttpResponse<T>> interceptAsync(
1197
        HttpRequest request, Chain<T> chain) {
1198
      var rewrittenRequest = rewriteRequest(request);
1✔
1199
      return autoAcceptEncoding(rewrittenRequest)
1✔
1200
          ? decoding(chain)
1✔
1201
              .forwardAsync(rewrittenRequest)
1✔
1202
              .thenApply(RewritingInterceptor::stripContentEncoding)
1✔
1203
          : chain.forwardAsync(rewrittenRequest);
1✔
1204
    }
1205

1206
    private boolean autoAcceptEncoding(HttpRequest request) {
1207
      return autoAcceptEncoding && !request.method().equalsIgnoreCase("HEAD");
1✔
1208
    }
1209

1210
    private HttpRequest rewriteRequest(HttpRequest request) {
1211
      var rewrittenRequest = MutableRequest.copyOf(request);
1✔
1212
      if (rewrittenRequest.adapterCodec().isEmpty()) {
1✔
1213
        adapterCodec.ifPresent(rewrittenRequest::adapterCodec);
1✔
1214
      }
1215

1216
      baseUri.map(baseUri -> baseUri.resolve(request.uri())).ifPresent(rewrittenRequest::uri);
1✔
1217
      validateUri(rewrittenRequest.uri());
1✔
1218

1219
      var originalHeadersMap = request.headers().map();
1✔
1220
      var defaultHeadersMap = defaultHeaders.map();
1✔
1221
      defaultHeadersMap.forEach(
1✔
1222
          (name, values) -> {
1223
            if (!originalHeadersMap.containsKey(name)) {
1✔
1224
              values.forEach(value -> rewrittenRequest.header(name, value));
1✔
1225
            }
1226
          });
1✔
1227

1228
      if (autoAcceptEncoding
1✔
1229
          && !originalHeadersMap.containsKey("Accept-Encoding")
1✔
1230
          && !defaultHeadersMap.containsKey("Accept-Encoding")) {
1!
1231
        var supportedEncodings = BodyDecoder.Factory.installedBindings().keySet();
1✔
1232
        if (!supportedEncodings.isEmpty()) {
1!
1233
          rewrittenRequest.header("Accept-Encoding", String.join(", ", supportedEncodings));
1✔
1234
        }
1235
      }
1236

1237
      // Overwrite Content-Type if the request body has a MediaType.
1238
      rewrittenRequest
1✔
1239
          .mimeBody()
1✔
1240
          .map(MimeBody::mediaType)
1✔
1241
          .ifPresent(mediaType -> rewrittenRequest.setHeader("Content-Type", mediaType.toString()));
1✔
1242

1243
      if (request.timeout().isEmpty()) {
1✔
1244
        requestTimeout.ifPresent(rewrittenRequest::timeout);
1✔
1245
      }
1246
      return rewrittenRequest.toImmutableRequest();
1✔
1247
    }
1248

1249
    private static <T> Chain<T> decoding(Chain<T> chain) {
1250
      return chain.with(
1✔
1251
          MoreBodyHandlers::decoding,
1252
          pushPromiseHandler ->
1253
              transformPushPromiseHandler(
1✔
1254
                  pushPromiseHandler,
1255
                  MoreBodyHandlers::decoding,
1256
                  RewritingInterceptor::stripContentEncoding));
1257
    }
1258

1259
    private static <T> HttpResponse<T> stripContentEncoding(HttpResponse<T> response) {
1260
      // Don't strip if the response wasn't compressed.
1261
      return response.headers().map().containsKey("Content-Encoding")
1✔
1262
          ? ResponseBuilder.from(response)
1✔
1263
              .removeHeader("Content-Encoding")
1✔
1264
              .removeHeader("Content-Length")
1✔
1265
              .build()
1✔
1266
          : response;
1✔
1267
    }
1268
  }
1269

1270
  private static final class HeadersTimeoutInterceptor implements Interceptor {
1271
    private final Duration headersTimeout;
1272
    private final Delayer delayer;
1273

1274
    HeadersTimeoutInterceptor(Duration headersTimeout, Delayer delayer) {
1✔
1275
      this.headersTimeout = headersTimeout;
1✔
1276
      this.delayer = delayer;
1✔
1277
    }
1✔
1278

1279
    @Override
1280
    public <T> HttpResponse<T> intercept(HttpRequest request, Chain<T> chain)
1281
        throws IOException, InterruptedException {
UNCOV
1282
      return Utils.get(interceptAsync(request, chain));
×
1283
    }
1284

1285
    @Override
1286
    public <T> CompletableFuture<HttpResponse<T>> interceptAsync(
1287
        HttpRequest request, Chain<T> chain) {
1288
      var timeoutTrigger = new TimeoutTrigger();
1✔
1289
      var triggerFuture =
1✔
1290
          delayer.delay(timeoutTrigger::trigger, headersTimeout, FlowSupport.SYNC_EXECUTOR);
1✔
1291
      timeoutTrigger.onCancellation(() -> triggerFuture.cancel(false));
1✔
1292

1293
      var responseFuture = withHeadersTimeout(chain, timeoutTrigger).forwardAsync(request);
1✔
1294

1295
      // Make a dependent copy of the original response future, so we can cancel the original and
1296
      // complete the copy exceptionally on timeout. Cancelling the original future may lead
1297
      // to cancelling the actual request on JDK 16 or higher.
1298
      var responseFutureCopy = responseFuture.copy();
1✔
1299
      timeoutTrigger.onTimeout(
1✔
1300
          () -> {
1301
            responseFutureCopy.completeExceptionally(
1✔
1302
                new HttpHeadersTimeoutException("Couldn't receive headers on time"));
1303
            responseFuture.cancel(true);
1✔
1304
          });
1✔
1305
      return responseFutureCopy;
1✔
1306
    }
1307

1308
    private <T> Chain<T> withHeadersTimeout(Chain<T> chain, TimeoutTrigger timeoutTrigger) {
1309
      // TODO handle push promises
1310
      return chain.with(
1✔
1311
          bodyHandler ->
1312
              responseInfo ->
1✔
1313
                  timeoutTrigger.cancel()
1✔
1314
                      ? bodyHandler.apply(responseInfo)
1✔
1315
                      : new TimedOutSubscriber<>());
1✔
1316
    }
1317

1318
    private static final class TimeoutTrigger {
1319
      private final CompletableFuture<Void> onTimeout = new CompletableFuture<>();
1✔
1320

1321
      TimeoutTrigger() {}
1✔
1322

1323
      void trigger() {
1324
        onTimeout.complete(null);
1✔
1325
      }
1✔
1326

1327
      @SuppressWarnings("FutureReturnValueIgnored")
1328
      void onTimeout(Runnable action) {
1329
        onTimeout.thenRun(action);
1✔
1330
      }
1✔
1331

1332
      @SuppressWarnings("FutureReturnValueIgnored")
1333
      void onCancellation(Runnable action) {
1334
        onTimeout.whenComplete(
1✔
1335
            (__, e) -> {
1336
              if (e instanceof CancellationException) {
1✔
1337
                action.run();
1✔
1338
              }
1339
            });
1✔
1340
      }
1✔
1341

1342
      boolean cancel() {
1343
        return onTimeout.cancel(false);
1✔
1344
      }
1345
    }
1346

1347
    private static final class TimedOutSubscriber<T> implements BodySubscriber<T> {
1348
      TimedOutSubscriber() {}
1✔
1349

1350
      @Override
1351
      public CompletionStage<T> getBody() {
1352
        return CompletableFuture.failedFuture(
1✔
1353
            new HttpHeadersTimeoutException("couldn't receive headers ont time"));
1354
      }
1355

1356
      @Override
1357
      public void onSubscribe(Subscription subscription) {
UNCOV
1358
        subscription.cancel();
×
UNCOV
1359
      }
×
1360

1361
      @Override
1362
      public void onNext(List<ByteBuffer> item) {
UNCOV
1363
        requireNonNull(item);
×
UNCOV
1364
      }
×
1365

1366
      @Override
1367
      public void onError(Throwable throwable) {
1368
        requireNonNull(throwable);
×
1369
        logger.log(Level.WARNING, "Exception received after headers timeout", throwable);
×
UNCOV
1370
      }
×
1371

1372
      @Override
1373
      public void onComplete() {}
×
1374
    }
1375
  }
1376

1377
  /**
1378
   * Applies {@link MoreBodyHandlers#withReadTimeout read timeouts} to responses and push promises.
1379
   */
1380
  private static final class ReadTimeoutInterceptor implements Interceptor {
1381
    private final Duration readTimeout;
1382
    private final Delayer delayer;
1383

1384
    ReadTimeoutInterceptor(Duration readTimeout, Delayer delayer) {
1✔
1385
      this.readTimeout = readTimeout;
1✔
1386
      this.delayer = delayer;
1✔
1387
    }
1✔
1388

1389
    @Override
1390
    public <T> HttpResponse<T> intercept(HttpRequest request, Chain<T> chain)
1391
        throws IOException, InterruptedException {
1392
      return withReadTimeout(chain).forward(request);
×
1393
    }
1394

1395
    @Override
1396
    public <T> CompletableFuture<HttpResponse<T>> interceptAsync(
1397
        HttpRequest request, Chain<T> chain) {
1398
      return withReadTimeout(chain).forwardAsync(request);
1✔
1399
    }
1400

1401
    private <T> Chain<T> withReadTimeout(Chain<T> chain) {
1402
      return chain.with(
1✔
1403
          bodyHandler -> MoreBodyHandlers.withReadTimeout(bodyHandler, readTimeout, delayer),
1✔
1404
          pushPromiseHandler ->
UNCOV
1405
              transformPushPromiseHandler(
×
1406
                  pushPromiseHandler,
1407
                  bodyHandler ->
UNCOV
1408
                      MoreBodyHandlers.withReadTimeout(bodyHandler, readTimeout, delayer),
×
UNCOV
1409
                  UnaryOperator.identity()));
×
1410
    }
1411
  }
1412
}
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