HttpBuilder.java

/**
 * Copyright (C) 2017 HttpBuilder-NG Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package groovyx.net.http;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import org.codehaus.groovy.runtime.MethodClosure;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Collections.*;

/**
 * This class is the main entry point into the "HttpBuilder-NG" API. It provides access to the HTTP Client configuration and the HTTP verbs to be
 * executed.
 *
 * The `HttpBuilder` is configured using one of the three static `configure` methods:
 *
 * [source,groovy]
 * ----
 * HttpBuilder configure(Function<HttpObjectConfig, ? extends HttpBuilder> factory)
 *
 * HttpBuilder configure(@DelegatesTo(HttpObjectConfig) Closure closure)
 *
 * HttpBuilder configure(Function<HttpObjectConfig, ? extends HttpBuilder> factory, @DelegatesTo(HttpObjectConfig) Closure closure)
 * ----
 *
 * Where the `factory` parameter is a `Function` used to instantiate the underlying builder with the appropriate HTTP Client implementation. Two
 * implementations are provided by default, one based on the https://hc.apache.org/httpcomponents-client-ga/[Apache HTTPClient], and the other based
 * on the {@link java.net.HttpURLConnection} class. It is generally preferable to use the Apache implementation; however, the `HttpURLConnection` is
 * instantiated by default to minimize required external dependencies. Both are fully supported.
 *
 * The `closure` parameter is used to provide configuration for the client instance - the allowed configuration values are provided by the configuration
 * interfaces (delegated to by the closure): {@link HttpConfig} and {@link HttpObjectConfig}.
 *
 * Once you have the `HttpBuilder` configured, you can execute HTTP verb operations (e.g. GET, POST):
 *
 * [source,groovy]
 * ----
 * def http = HttpBuilder.configure {
 *     request.uri = 'http://localhost:10101/rest'
 * }
 *
 * def content = http.get {
 *     request.uri.path = '/list'
 * }
 *
 * def result = http.post {
 *     request.uri.path = '/save'
 *     request.body = infoRecord
 *     request.contentType = ContentTypes.JSON[0]
 * }
 * ----
 *
 * :linkattrs:
 *
 * See the HTTP verb method docs (below) or the https://http-builder-ng.github.io/http-builder-ng/asciidoc/html5/[User Guide^] for more examples.
 */
public abstract class HttpBuilder implements Closeable {

    private static final Function<HttpObjectConfig, ? extends HttpBuilder> factory = JavaHttpBuilder::new;

    @SuppressWarnings("unused")
    static void noOp() {}

    static Closure NO_OP = new MethodClosure(HttpBuilder.class, "noOp");

    /**
     * Creates an `HttpBuilder` with the default configuration using the provided factory function ({@link JavaHttpBuilder} or
     * one of the other HTTP client implementation functions.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure({ c -> new ApacheHttpBuilder(c); } as Function)
     * ----
     *
     * When configuring the `HttpBuilder` with this method, the verb configurations are required to specify the `request.uri` property.
     *
     * @param factory the `HttpObjectConfig` factory function ({@link JavaHttpBuilder} or one of the other HTTP client implementation functions
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(final Function<HttpObjectConfig, ? extends HttpBuilder> factory) {
        return configure(factory, NO_OP);
    }

    /**
     * Creates an `HttpBuilder` using the `JavaHttpBuilder` factory instance configured with the provided configuration closure.
     *
     * The configuration closure delegates to the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig} interface -
     * configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * ----
     *
     * @param closure the configuration closure (delegated to {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(@DelegatesTo(HttpObjectConfig.class) final Closure closure) {
        return configure(factory, closure);
    }

    /**
     * Creates an `HttpBuilder` configured with the provided configuration closure, using the `defaultFactory` as the client factory.
     *
     * The configuration closure delegates to the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig} interface -
     * configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * [source,groovy]
     * ----
     * def factory = { c -> new ApacheHttpBuilder(c); } as Function
     *
     * def http = HttpBuilder.configure(factory){
     *     request.uri = 'http://localhost:10101'
     * }
     * ----
     *
     * @param factory the {@link HttpObjectConfig} factory function ({@link JavaHttpBuilder} or {@link groovyx.net.http.ApacheHttpBuilder})
     * @param closure the configuration closure (delegated to {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(final Function<HttpObjectConfig, ? extends HttpBuilder> factory, @DelegatesTo(HttpObjectConfig.class) final Closure closure) {
        HttpObjectConfig impl = new HttpObjectConfigImpl();
        closure.setDelegate(impl);
        closure.setResolveStrategy(Closure.DELEGATE_FIRST);
        closure.call();
        return factory.apply(impl);
    }

    /**
     * Creates an `HttpBuilder` using the `JavaHttpBuilder` factory instance configured with the provided configuration function.
     *
     * The configuration {@link Consumer} function accepts an instance of the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig}
     * interface - configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * This configuration method is generally meant for use with standard Java.
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(new Consumer<HttpObjectConfig>() {
     *     public void accept(HttpObjectConfig config) {
     *         config.getRequest().setUri(format("http://localhost:%d", serverRule.getPort()));
     *     }
     * });
     * ----
     *
     * Or, using lambda expressions:
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(config -> {
     *     config.getRequest().setUri(format("http://localhost:%d", serverRule.getPort()));
     * });
     * ----
     *
     * @param configuration the configuration function (accepting {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(final Consumer<HttpObjectConfig> configuration) {
        return configure(factory, configuration);
    }

    /**
     * Creates an `HttpBuilder` using the provided client factory function, configured with the provided configuration function.
     *
     * The configuration {@link Consumer} function accepts an instance of the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig}
     * interface - configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * This configuration method is generally meant for use with standard Java.
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(JavaHttpBuilder::new, new Consumer<HttpObjectConfig>() {
     *     public void accept(HttpObjectConfig config) {
     *         config.getRequest().setUri("http://localhost:10101");
     *     }
     * });
     * ----
     *
     * Or, using lambda expressions:
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(JavaHttpBuilder::new, config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * ----
     *
     * @param factory the {@link HttpObjectConfig} factory function ({@link JavaHttpBuilder} or {@link groovyx.net.http.optional.ApacheHttpBuilder})
     * @param configuration the configuration function (accepting {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(final Function<HttpObjectConfig, ? extends HttpBuilder> factory, final Consumer<HttpObjectConfig> configuration) {
        HttpObjectConfig impl = new HttpObjectConfigImpl();
        configuration.accept(impl);
        return factory.apply(impl);
    }

    private final EnumMap<HttpVerb, BiFunction<ChainedHttpConfig, Function<ChainedHttpConfig, Object>, Object>> interceptors;
    private final CookieManager cookieManager;

    protected HttpBuilder(final HttpObjectConfig objectConfig) {
        this.interceptors = new EnumMap<>(objectConfig.getExecution().getInterceptors());
        this.cookieManager = new CookieManager(makeCookieStore(objectConfig), CookiePolicy.ACCEPT_ALL);
    }

    private CookieStore makeCookieStore(final HttpObjectConfig objectConfig) {
        if (objectConfig.getClient().getCookiesEnabled()) {
            final File folder = objectConfig.getClient().getCookieFolder();
            return (folder == null ?
                new NonBlockingCookieStore() :
                new FileBackedCookieStore(folder, objectConfig.getExecution().getExecutor()));
        } else {
            return NullCookieStore.instance();
        }
    }

    protected CookieManager getCookieManager() {
        return cookieManager;
    }

    /**
     * Returns the cookie store used by this builder
     *
     * @return the cookie store used by this builder
     */
    public CookieStore getCookieStore() {
        return cookieManager.getCookieStore();
    }

    protected Map<String, String> cookiesToAdd(final HttpObjectConfig.Client clientConfig, final ChainedHttpConfig.ChainedRequest cr) throws URISyntaxException {
        Map<String, String> tmp = new HashMap<>();

        try {
            final URI uri = cr.getUri().toURI();
            for (HttpCookie cookie : cr.actualCookies(new ArrayList<>())) {
                final String keyName = clientConfig.getCookieVersion() == 0 ? "Set-Cookie" : "Set-Cookie2";
                final Map<String, List<String>> toPut = singletonMap(keyName, singletonList(cookie.toString()));
                cookieManager.put(cr.getUri().forCookie(cookie), toPut);
            }

            Map<String, List<String>> found = cookieManager.get(uri, emptyMap());
            for (Map.Entry<String, List<String>> e : found.entrySet()) {
                if (e.getValue() != null && !e.getValue().isEmpty()) {
                    tmp.put(e.getKey(), String.join("; ", e.getValue()));
                }
            }
        } catch (IOException ioe) {
            throw new TransportingException(ioe);
        }

        return tmp;
    }

    public static List<HttpCookie> cookies(final List<FromServer.Header<?>> headers) {
        final List<HttpCookie> cookies = new ArrayList<>();
        for (FromServer.Header<?> header : headers) {
            if (header.getKey().equalsIgnoreCase("Set-Cookie") ||
                header.getKey().equalsIgnoreCase("Set-Cookie2")) {
                final List<?> found = (List<?>) header.getParsed();
                for (Object o : found) {
                    cookies.add((HttpCookie) o);
                }
            }
        }

        return Collections.unmodifiableList(cookies);

    }

    protected void addCookieStore(final URI uri, final List<FromServer.Header<?>> headers) {
        for (HttpCookie cookie : cookies(headers)) {
            cookieManager.getCookieStore().add(uri, cookie);
        }
    }

    /**
     * Executes a GET request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.get()
     * ----
     *
     * @return the resulting content
     */
    public Object get() {
        return get(NO_OP);
    }

    /**
     * Executes a GET request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast to
     * the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * String result = http.get(String){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T get(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.GET).apply(configureRequest(type, HttpVerb.GET, closure), this::doGet));
    }

    /**
     * Executes a GET request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.get(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T get(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.GET).apply(configureRequest(type, HttpVerb.GET, configuration), this::doGet));
    }

    /**
     * Executes an asynchronous GET request on the configured URI (asynchronous alias to the `get()` method. The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.getAsync()
     * def result = future.get()
     * ----
     *
     * @return a {@link CompletableFuture} for retrieving the resulting content
     */
    public CompletableFuture<Object> getAsync() {
        return CompletableFuture.supplyAsync(() -> get(), getExecutor());
    }

    /**
     * Executes an asynchronous GET request on the configured URI (asynchronous alias to the `get(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.getAsync(){
     *     request.uri.path = '/something'
     * }
     * def result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public CompletableFuture<Object> getAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> get(closure), getExecutor());
    }

    /**
     * Executes an asynchronous GET request on the configured URI (asynchronous alias to `get(Consumer)`), with additional configuration provided by the
     * configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.getAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> getAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> get(configuration), getExecutor());
    }

    /**
     * Executes asynchronous GET request on the configured URI (alias for the `get(Class, Closure)` method), with additional configuration provided by
     * the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.getAsync(String){
     *     request.uri.path = '/something'
     * }
     * String result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} for the resulting content cast to the specified type
     */
    public <T> CompletableFuture<T> getAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> get(type, closure), getExecutor());
    }

    /**
     * Executes asynchronous GET request on the configured URI (alias for the `get(Class, Consumer)` method), with additional configuration provided by
     * the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally meant for use with standard Java.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.get(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} for the resulting content cast to the specified type
     */
    public <T> CompletableFuture<T> getAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> get(type, configuration), getExecutor());
    }

    /**
     * Executes a HEAD request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.head()
     * ----
     *
     * @return the resulting content - which should always be `null` for this method
     */
    public Object head() {
        return head(NO_OP);
    }

    /**
     * Executes a HEAD request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast to
     * the specified `type`. A response to a HEAD request contains no data; however, the `response.when()` methods may provide data based on response
     * headers, which will be cast to the specified type.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * Date result = http.head(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T head(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.HEAD).apply(configureRequest(type, HttpVerb.HEAD, closure), this::doHead));
    }

    /**
     * Executes a HEAD request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.head(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T head(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.HEAD).apply(configureRequest(type, HttpVerb.HEAD, configuration), this::doHead));
    }

    /**
     * Executes an asynchronous HEAD request on the configured URI (asynchronous alias to the `head()` method. The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.headAsync()
     * ----
     *
     * There is no response content, unless provided by `request.when()` method closure.
     *
     * @return a {@link CompletableFuture} for retrieving the resulting content
     */
    public CompletableFuture<Object> headAsync() {
        return CompletableFuture.supplyAsync(() -> head(), getExecutor());
    }

    /**
     * Executes an asynchronous HEAD request on the configured URI (asynchronous alias to the `head(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.headAsync(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * The response will not contain content unless the `response.when()` method closure provides it based on the response headers.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public CompletableFuture<Object> headAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> head(closure), getExecutor());
    }

    /**
     * Executes an asynchronous HEAD request on the configured URI (asynchronous alias to `head(Consumer)`), with additional configuration provided by the
     * configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.headAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> headAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> head(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous HEAD request on the configured URI (asynchronous alias to the `head(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`. A response to a HEAD request contains no
     * data; however, the `response.when()` methods may provide data based on response headers, which will be cast to the specified type.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * CompletableFuture future = http.headAsync(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * Date result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return a {@link CompletableFuture} which may be used to access the resulting content (if present)
     */
    public <T> CompletableFuture<T> headAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> head(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous HEAD request on the configured URI (asynchronous alias to `head(Class,Consumer)`), with additional configuration
     * provided by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.headAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type wrapped in a {@link CompletableFuture}
     */
    public <T> CompletableFuture<T> headAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> head(type, configuration), getExecutor());
    }

    /**
     * Executes a POST request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.post()
     * ----
     *
     * @return the resulting content
     */
    public Object post() {
        return post(NO_OP);
    }

    /**
     * Executes an POST request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast
     * to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * Date date = http.post(Date){
     *     request.uri.path = '/date'
     *     request.body = '{ "timezone": "America/Chicago" }'
     *     request.contentType = 'application/json'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the result of the request cast to the specified type
     */
    public <T> T post(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.POST).apply(configureRequest(type, HttpVerb.POST, closure), this::doPost));
    }

    /**
     * Executes a POST request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.post(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T post(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.POST).apply(configureRequest(type, HttpVerb.POST, configuration), this::doPost));
    }

    /**
     * Executes an asynchronous POST request on the configured URI (asynchronous alias to the `post()` method). The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.postAsync()
     * def result = future.get()
     * ----
     *
     * @return the {@link CompletableFuture} containing the future access to the response
     */
    public CompletableFuture<Object> postAsync() {
        return CompletableFuture.supplyAsync(() -> post(NO_OP), getExecutor());
    }

    /**
     * Executes an asynchronous POST request on the configured URI (an asynchronous alias to the `post(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.postAsync(){
     *     request.uri.path = '/something'
     *     request.body = 'My content'
     *     request.contentType = 'text/plain'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the future result data
     */
    public CompletableFuture<Object> postAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> post(closure), getExecutor());
    }

    /**
     * Executes an asynchronous POST request on the configured URI (asynchronous alias to `post(Consumer)`), with additional configuration provided by the
     * configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.postAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> postAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> post(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous POST request on the configured URI (asynchronous alias to the `post(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletedFuture<Date> future = http.postAsync(Date){
     *     request.uri.path = '/date'
     *     request.body = '{ "timezone": "America/Chicago" }'
     *     request.contentType = 'application/json'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * Date date = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the result of the request
     */
    public <T> CompletableFuture<T> postAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> post(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous POST request on the configured URI (asynchronous alias to `put(Class,Consumer)`), with additional configuration provided
     * by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<String> future = http.postAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * String result = future.get();
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type, wrapped in a {@link CompletableFuture}
     */
    public <T> CompletableFuture<T> postAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> post(type, configuration), getExecutor());
    }

    /**
     * Executes a PUT request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.put()
     * ----
     *
     * @return the resulting content
     */
    public Object put() {
        return put(NO_OP);
    }

    /**
     * Executes an PUT request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast
     * to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * Date date = http.put(Date){
     *     request.uri.path = '/date'
     *     request.body = '{ "timezone": "America/Chicago" }'
     *     request.contentType = 'application/json'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the result of the request cast to the specified type
     */
    public <T> T put(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.PUT).apply(configureRequest(type, HttpVerb.PUT, closure), this::doPut));
    }

    /**
     * Executes a PUT request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.get(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T put(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.PUT).apply(configureRequest(type, HttpVerb.PUT, configuration), this::doPut));
    }

    /**
     * Executes an asynchronous PUT request on the configured URI (asynchronous alias to the `put()` method). The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.putAsync()
     * def result = future.get()
     * ----
     *
     * @return the {@link CompletableFuture} containing the future access to the response
     */
    public CompletableFuture<Object> putAsync() {
        return CompletableFuture.supplyAsync(() -> put(NO_OP), getExecutor());
    }

    /**
     * Executes an asynchronous PUT request on the configured URI (an asynchronous alias to the `put(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.putAsync(){
     *     request.uri.path = '/something'
     *     request.body = 'My content'
     *     request.contentType = 'text/plain'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the future result data
     */
    public CompletableFuture<Object> putAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> put(closure), getExecutor());
    }

    /**
     * Executes an asynchronous PUT request on the configured URI (asynchronous alias to `put(Consumer)`), with additional configuration provided by the
     * configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.putAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> putAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> put(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous PUT request on the configured URI (asynchronous alias to the `put(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletedFuture<Date> future = http.putAsync(Date){
     *     request.uri.path = '/date'
     *     request.body = '{ "timezone": "America/Chicago" }'
     *     request.contentType = 'application/json'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * Date date = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the result of the request
     */
    public <T> CompletableFuture<T> putAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> put(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous PUT request on the configured URI (asynchronous alias to `put(Class,Consumer)`), with additional configuration provided
     * by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<String> future = http.putAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * String result = future.get();
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type, wrapped in a {@link CompletableFuture}
     */
    public <T> CompletableFuture<T> putAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> put(type, configuration), getExecutor());
    }

    /**
     * Executes a DELETE request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.delete()
     * ----
     *
     * @return the resulting content
     */
    public Object delete() {
        return delete(NO_OP);
    }

    /**
     * Executes an DELETE request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast
     * to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * Date date = http.delete(Date){
     *     request.uri.path = '/date'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the result of the request cast to the specified type
     */
    public <T> T delete(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.DELETE).apply(configureRequest(type, HttpVerb.DELETE, closure), this::doDelete));
    }

    /**
     * Executes a DELETE request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.delete(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T delete(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.DELETE).apply(configureRequest(type, HttpVerb.DELETE, configuration), this::doDelete));
    }

    /**
     * Executes an asynchronous DELETE request on the configured URI (asynchronous alias to the `delete()` method). The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.deleteAsync()
     * def result = future.get()
     * ----
     *
     * @return the {@link CompletableFuture} containing the future access to the response
     */
    public CompletableFuture<Object> deleteAsync() {
        return CompletableFuture.supplyAsync(() -> delete(NO_OP), getExecutor());
    }

    /**
     * Executes an asynchronous DELETE request on the configured URI (an asynchronous alias to the `delete(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.deleteAsync(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the future result data
     */
    public CompletableFuture<Object> deleteAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> delete(closure), getExecutor());
    }

    /**
     * Executes an asynchronous DELETE request on the configured URI (asynchronous alias to the `delete(Consumer)` method), with additional
     * configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.deleteAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the result of the request
     */
    public CompletableFuture<Object> deleteAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> delete(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous DELETE request on the configured URI (asynchronous alias to the `delete(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletedFuture<Date> future = http.deleteAsync(Date){
     *     request.uri.path = '/date'
     *     response.parser('text/date') { ChainedHttpConfig config, FromServer fromServer ->
     *         Date.parse('yyyy.MM.dd HH:mm', fromServer.inputStream.text)
     *     }
     * }
     * Date date = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the resulting object
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the result of the request
     */
    public <T> CompletableFuture<T> deleteAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> delete(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous DELETE request on the configured URI (asynchronous alias to the `delete(Class,Consumer)` method), with additional
     * configuration provided by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<String> future = http.deleteAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * String result = future.get();
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the resulting object
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} containing the result of the request
     */
    public <T> CompletableFuture<T> deleteAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> delete(type, configuration), getExecutor());
    }

    /**
     * Executes a GET request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.get(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object get(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return get(Object.class, closure);
    }

    /**
     * Executes a GET request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.delete(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object get(final Consumer<HttpConfig> configuration) {
        return get(Object.class, configuration);
    }

    /**
     * Executes a HEAD request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.head(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content - which will be `null` unless provided by a `request.when()` closure
     */
    public Object head(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return head(Object.class, closure);
    }

    /**
     * Executes a HEAD request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.delete(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object head(final Consumer<HttpConfig> configuration) {
        return head(Object.class, configuration);
    }

    /**
     * Executes a POST request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.post(){
     *     request.uri.path = '/something'
     *     request.body = 'My content'
     *     request.contentType = 'text/plain'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object post(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return post(Object.class, closure);
    }

    /**
     * Executes a POST request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.post(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object post(final Consumer<HttpConfig> configuration) {
        return post(Object.class, configuration);
    }

    /**
     * Executes a PUT request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.put(){
     *     request.uri.path = '/something'
     *     request.body = 'My content'
     *     request.contentType = 'text/plain'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object put(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return put(Object.class, closure);
    }

    /**
     * Executes a PUT request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.put(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object put(final Consumer<HttpConfig> configuration) {
        return put(Object.class, configuration);
    }

    /**
     * Executes a DELETE request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.delete(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object delete(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return delete(Object.class, closure);
    }

    /**
     * Executes a DELETE request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.delete(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object delete(final Consumer<HttpConfig> configuration) {
        return delete(Object.class, configuration);
    }

    /**
     * Executes a PATCH request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.patch()
     * ----
     *
     * @return the resulting content
     */
    public Object patch() {
        return patch(NO_OP);
    }

    /**
     * Executes a PATCH request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * def result = http.patch(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object patch(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return patch(Object.class, closure);
    }

    /**
     * Executes a PATCH request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.patch(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object patch(final Consumer<HttpConfig> configuration) {
        return patch(Object.class, configuration);
    }

    /**
     * Executes a PATCH request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast to
     * the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * String result = http.patch(String){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T patch(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.PATCH).apply(configureRequest(type, HttpVerb.PATCH, closure), this::doPatch));
    }

    /**
     * Executes a PATCH request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.patch(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T patch(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.PATCH).apply(configureRequest(type, HttpVerb.PATCH, configuration), this::doPatch));
    }

    /**
     * Executes an asynchronous PATCH request on the configured URI (asynchronous alias to the `patch()` method. The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.patchAsync()
     * def result = future.get()
     * ----
     *
     * @return a {@link CompletableFuture} for retrieving the resulting content
     */
    public CompletableFuture<Object> patchAsync() {
        return CompletableFuture.supplyAsync(() -> patch(), getExecutor());
    }

    /**
     * Executes an asynchronous PATCH request on the configured URI (asynchronous alias to the `patch(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.patchAsync(){
     *     request.uri.path = '/something'
     * }
     * def result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public CompletableFuture<Object> patchAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> patch(closure), getExecutor());
    }

    /**
     * Executes an asynchronous PATCH request on the configured URI (asynchronous alias to `patch(Consumer)`), with additional configuration provided by the
     * configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.patchAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> patchAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> patch(configuration), getExecutor());
    }

    /**
     * Executes asynchronous PATCH request on the configured URI (alias for the `patch(Class, Closure)` method), with additional configuration provided by
     * the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * CompletableFuture future = http.patchAsync(String){
     *     request.uri.path = '/something'
     * }
     * String result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} for the resulting content cast to the specified type
     */
    public <T> CompletableFuture<T> patchAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> patch(type, closure), getExecutor());
    }

    /**
     * Executes asynchronous PATCH request on the configured URI (alias for the `patch(Class, Consumer)` method), with additional configuration provided by
     * the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally meant for use with standard Java.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.patch(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the {@link CompletableFuture} for the resulting content cast to the specified type
     */
    public <T> CompletableFuture<T> patchAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> patch(type, configuration), getExecutor());
    }

    /**
     * Executes a OPTIONS request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.options(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content - which will be `null` unless provided by a `request.when()` closure
     */
    public Object options(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return options(Object.class, closure);
    }

    /**
     * Executes a OPTIONS request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.options(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object options(final Consumer<HttpConfig> configuration) {
        return options(Object.class, configuration);
    }

    /**
     * Executes a OPTIONS request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.options()
     * ----
     *
     * @return the resulting content - which should always be `null` for this method
     */
    public Object options() {
        return options(NO_OP);
    }

    /**
     * Executes a OPTIONS request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast to
     * the specified `type`. A response to an OPTIONS request contains no data; however, the `response.when()` methods may provide data based on response
     * headers, which will be cast to the specified type.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * Date result = http.options(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T options(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.OPTIONS).apply(configureRequest(type, HttpVerb.OPTIONS, closure), this::doOptions));
    }

    /**
     * Executes a OPTIONS request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.options(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T options(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.OPTIONS).apply(configureRequest(type, HttpVerb.OPTIONS, configuration), this::doOptions));
    }

    /**
     * Executes an asynchronous OPTIONS request on the configured URI (asynchronous alias to the `options()` method. The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.optionsAsync()
     * ----
     *
     * There is no response content, unless provided by `request.when()` method closure.
     *
     * @return a {@link CompletableFuture} for retrieving the resulting content
     */
    public CompletableFuture<Object> optionsAsync() {
        return CompletableFuture.supplyAsync(() -> options(), getExecutor());
    }

    /**
     * Executes an asynchronous OPTIONS request on the configured URI (asynchronous alias to the `options(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.optionsAsync(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * The response will not contain content unless the `response.when()` method closure provides it based on the response headers.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public CompletableFuture<Object> optionsAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> options(closure), getExecutor());
    }

    /**
     * Executes an asynchronous OPTIONS request on the configured URI (asynchronous alias to `options(Consumer)`), with additional configuration
     * provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.optionsAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> optionsAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> options(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous OPTIONS request on the configured URI (asynchronous alias to the `options(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`. A response to a OPTIONS request contains no
     * data; however, the `response.when()` methods may provide data based on response headers, which will be cast to the specified type.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * CompletableFuture future = http.optionsAsync(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * Date result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return a {@link CompletableFuture} which may be used to access the resulting content (if present)
     */
    public <T> CompletableFuture<T> optionsAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> options(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous OPTIONS request on the configured URI (asynchronous alias to `options(Class,Consumer)`), with additional configuration
     * provided by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.optionsAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type wrapped in a {@link CompletableFuture}
     */
    public <T> CompletableFuture<T> optionsAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> options(type, configuration), getExecutor());
    }

    /**
     * Executes a TRACE request on the configured URI, with additional configuration provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.trace(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object trace(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return trace(Object.class, closure);
    }

    /**
     * Executes a TRACE request on the configured URI, with additional configuration provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * http.trace(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public Object trace(final Consumer<HttpConfig> configuration) {
        return trace(Object.class, configuration);
    }

    /**
     * Executes a TRACE request on the configured URI. The `request.uri` property should be configured in the global client configuration in order to
     * have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.trace()
     * ----
     *
     * @return the resulting content
     */
    public Object trace() {
        return trace(NO_OP);
    }

    /**
     * Executes a TRACE request on the configured URI, with additional configuration provided by the configuration closure. The result will be cast to
     * the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * Date result = http.trace(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T trace(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return type.cast(interceptors.get(HttpVerb.TRACE).apply(configureRequest(type, HttpVerb.TRACE, closure), this::doTrace));
    }

    /**
     * Executes a TRACE request on the configured URI, with additional configuration provided by the configuration function. The result will be cast to
     * the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.options(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type
     */
    public <T> T trace(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return type.cast(interceptors.get(HttpVerb.TRACE).apply(configureRequest(type, HttpVerb.TRACE, configuration), this::doTrace));
    }

    /**
     * Executes an asynchronous TRACE request on the configured URI (asynchronous alias to the `trace()` method. The `request.uri` property should be
     * configured in the global client configuration in order to have a target for the request.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.traceAsync()
     * ----
     *
     * @return a {@link CompletableFuture} for retrieving the resulting content
     */
    public CompletableFuture<Object> traceAsync() {
        return CompletableFuture.supplyAsync(() -> trace(), getExecutor());
    }

    /**
     * Executes an asynchronous TRACE request on the configured URI (asynchronous alias to the `trace(Closure)` method), with additional configuration
     * provided by the configuration closure.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101'
     * }
     * http.traceAsync(){
     *     request.uri.path = '/something'
     * }
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content
     */
    public CompletableFuture<Object> traceAsync(@DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> trace(closure), getExecutor());
    }

    /**
     * Executes an asynchronous TRACE request on the configured URI (asynchronous alias to `trace(Consumer)`), with additional configuration
     * provided by the configuration function.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,java]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * CompletableFuture<Object> future = http.traceAsync(config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * Object result = future.get();
     * ----
     *
     * The `configuration` function allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param configuration the additional configuration closure (delegated to {@link HttpConfig})
     * @return the resulting content wrapped in a {@link CompletableFuture}
     */
    public CompletableFuture<Object> traceAsync(final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> trace(configuration), getExecutor());
    }

    /**
     * Executes an asynchronous TRACE request on the configured URI (asynchronous alias to the `trace(Class,Closure)` method), with additional
     * configuration provided by the configuration closure. The result will be cast to the specified `type`.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     *     request.uri = 'http://localhost:10101/date'
     * }
     * CompletableFuture future = http.traceAsync(Date){
     *      response.success { FromServer fromServer ->
     *          Date.parse('yyyy.MM.dd HH:mm', fromServer.headers.find { it.key == 'stamp' }.value)
     *      }
     * }
     * Date result = future.get()
     * ----
     *
     * The configuration `closure` allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param closure the additional configuration closure (delegated to {@link HttpConfig})
     * @return a {@link CompletableFuture} which may be used to access the resulting content (if present)
     */
    public <T> CompletableFuture<T> traceAsync(final Class<T> type, @DelegatesTo(HttpConfig.class) final Closure closure) {
        return CompletableFuture.supplyAsync(() -> trace(type, closure), getExecutor());
    }

    /**
     * Executes an asynchronous TRACE request on the configured URI (asynchronous alias to `trace(Class,Consumer)`), with additional configuration
     * provided by the configuration function. The result will be cast to the specified `type`.
     *
     * This method is generally used for Java-specific configuration.
     *
     * [source,groovy]
     * ----
     * HttpBuilder http = HttpBuilder.configure(config -> {
     *     config.getRequest().setUri("http://localhost:10101");
     * });
     * String result = http.traceAsync(String.class, config -> {
     *     config.getRequest().getUri().setPath("/foo");
     * });
     * ----
     *
     * The `configuration` {@link Consumer} allows additional configuration for this request based on the {@link HttpConfig} interface.
     *
     * @param type the type of the response content
     * @param configuration the additional configuration function (delegated to {@link HttpConfig})
     * @return the resulting content cast to the specified type wrapped in a {@link CompletableFuture}
     */
    public <T> CompletableFuture<T> traceAsync(final Class<T> type, final Consumer<HttpConfig> configuration) {
        return CompletableFuture.supplyAsync(() -> trace(type, configuration), getExecutor());
    }

    /**
     * Used to retrieve the instance of the internal client implementation. All client configuration will have been performed by the time this
     * method is accessible. If additional configuration is desired and not supported by HttpBuilder-NG directly, you should use the
     * `HttpObjectConfig::Client::clientCustomizer(Consumer<Object>)` method.
     *
     * This functionality is optional and client-implementation-dependent. If access to the internal client is not supported, an
     * {@link UnsupportedOperationException} will be thrown.
     *
     * @return a reference to the internal client implementation used (or null if not supported)
     */
    public abstract Object getClientImplementation();

    protected abstract Object doGet(final ChainedHttpConfig config);

    protected abstract Object doHead(final ChainedHttpConfig config);

    protected abstract Object doPost(final ChainedHttpConfig config);

    protected abstract Object doPut(final ChainedHttpConfig config);

    protected abstract Object doDelete(final ChainedHttpConfig config);

    protected abstract Object doPatch(final ChainedHttpConfig config);

    protected abstract Object doOptions(final ChainedHttpConfig config);

    protected abstract Object doTrace(final ChainedHttpConfig config);

    protected abstract ChainedHttpConfig getObjectConfig();

    public abstract Executor getExecutor();

    private ChainedHttpConfig configureRequest(final Class<?> type, final HttpVerb verb, final Closure closure) {
        final HttpConfigs.BasicHttpConfig myConfig = HttpConfigs.requestLevel(getObjectConfig());
        closure.setDelegate(myConfig);
        closure.setResolveStrategy(Closure.DELEGATE_FIRST);
        closure.call();

        myConfig.getChainedRequest().setVerb(verb);
        myConfig.getChainedResponse().setType(type);

        return myConfig;
    }

    private ChainedHttpConfig configureRequest(final Class<?> type, final HttpVerb verb, final Consumer<HttpConfig> configuration) {
        final HttpConfigs.BasicHttpConfig myConfig = HttpConfigs.requestLevel(getObjectConfig());
        configuration.accept(myConfig);

        myConfig.getChainedRequest().setVerb(verb);
        myConfig.getChainedResponse().setType(type);

        return myConfig;
    }

    public static Throwable findCause(final Exception e) {
        if (e instanceof TransportingException) {
            return e.getCause();
        } else if (e instanceof UndeclaredThrowableException) {
            final UndeclaredThrowableException ute = (UndeclaredThrowableException) e;
            if (ute.getCause() != null) {
                return ute.getCause();
            } else {
                return e;
            }
        } else {
            return e;
        }
    }

    protected Object handleException(final ChainedHttpConfig.ChainedResponse cr, final Exception e) {
        return cr.actualException().apply(findCause(e));
    }

    protected static class ResponseHandlerFunction implements BiFunction<ChainedHttpConfig, FromServer, Object> {

        static final ResponseHandlerFunction HANDLER_FUNCTION = new ResponseHandlerFunction();

        @Override
        public Object apply(ChainedHttpConfig requestConfig, FromServer fromServer) {
            try {
                final BiFunction<ChainedHttpConfig, FromServer, Object> parser = requestConfig.findParser(fromServer.getContentType());
                final BiFunction<FromServer, Object, ?> action = requestConfig.getChainedResponse().actualAction(fromServer.getStatusCode());

                return action.apply(fromServer, fromServer.getHasBody() ? parser.apply(requestConfig, fromServer) : null);

            } finally {
                fromServer.finish();
            }
        }
    }
}