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

mizosoft / methanol / #585

22 Aug 2025 05:42PM UTC coverage: 89.013% (-0.9%) from 89.944%
#585

push

github

web-flow
Properly setup coveralls (#132)

* Move buildSrc to gradle/src

* Setup coveralls with another Gradle plugin

 - Previously "com.github.kt3k.coveralls" was used which was removed in #130 due to issues with Gradle 9.0.0 (https://github.com/kt3k/coveralls-gradle-plugin/issues/119)

* Add CI job for coverallsJacoco

* Setup covered & test-contributing projects properly

2324 of 2796 branches covered (83.12%)

Branch coverage included in aggregate %.

7633 of 8390 relevant lines covered (90.98%)

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✔
127
    } catch (NoSuchMethodException e) {
×
128
      shutdown = null;
×
129
    } catch (IllegalAccessException e) {
×
130
      throw new IllegalStateException(e);
×
131
    }
1✔
132

133
    if (shutdown == null) {
1!
134
      SHUTDOWN = null;
×
135
      AWAIT_TERMINATION = null;
×
136
      IS_TERMINATED = null;
×
137
      SHUTDOWN_NOW = null;
×
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✔
153
      } catch (NoSuchMethodException | IllegalAccessException e) {
×
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
  public Optional<URI> baseUri() {
274
    return baseUri;
1✔
275
  }
276

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

704
    BaseBuilder() {}
1✔
705

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

898
    abstract B self();
899

900
    abstract HttpClient buildBackend();
901
  }
902

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

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

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

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

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

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

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

945
    private Builder() {}
1✔
946

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1320
      TimeoutTrigger() {}
1✔
1321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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