io.vertx.core.http.impl.HttpClientRequestImpl.java Source code

Java tutorial

Introduction

Here is the source code for io.vertx.core.http.impl.HttpClientRequestImpl.java

Source

/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.http.impl;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpConnection;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.StreamPriority;
import io.vertx.core.http.impl.headers.VertxHttpHeaders;
import io.vertx.core.impl.Arguments;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static io.vertx.core.http.HttpHeaders.*;

/**
 * This class is optimised for performance when used on the same event loop that is was passed to the handler with.
 * However it can be used safely from other threads.
 *
 * The internal state is protected using the synchronized keyword. If always used on the same event loop, then
 * we benefit from biased locking which makes the overhead of synchronized near zero.
 *
 * This class uses {@code this} for synchronization purpose. The {@link #client}  or{@link #stream} instead are
 * called must not be called under this lock to avoid deadlocks.
 *
 * @author <a href="http://tfox.org">Tim Fox</a>
 */
public class HttpClientRequestImpl extends HttpClientRequestBase implements HttpClientRequest {

    static final Logger log = LoggerFactory.getLogger(HttpClientRequestImpl.class);

    private final ContextInternal context;
    private final Promise<Void> endPromise;
    private final Future<Void> endFuture;
    private boolean chunked;
    private String hostHeader;
    private String rawMethod;
    private Handler<Void> continueHandler;
    private Handler<Void> drainHandler;
    private Handler<HttpClientRequest> pushHandler;
    private Handler<Throwable> exceptionHandler;
    private boolean ended;
    private Throwable reset;
    private ByteBuf pendingChunks;
    private List<Handler<AsyncResult<Void>>> pendingHandlers;
    private int pendingMaxSize = -1;
    private int followRedirects;
    private VertxHttpHeaders headers;
    private StreamPriority priority;
    public HttpClientStream stream;
    private boolean connecting;
    private Promise<NetSocket> netSocketPromise;

    HttpClientRequestImpl(HttpClientImpl client, ContextInternal context, boolean ssl, HttpMethod method,
            SocketAddress server, String host, int port, String relativeURI) {
        super(client, context, ssl, method, server, host, port, relativeURI);
        this.chunked = false;
        this.context = context;
        this.endPromise = context.promise();
        this.endFuture = endPromise.future();
        this.priority = HttpUtils.DEFAULT_STREAM_PRIORITY;
    }

    @Override
    void handleException(Throwable t) {
        super.handleException(t);
        Handler<Throwable> handler;
        synchronized (this) {
            if (exceptionHandler != null && !endFuture.isComplete()) {
                handler = exceptionHandler;
            } else {
                handler = log::error;
            }
        }
        handler.handle(t);
        endPromise.tryFail(t);
    }

    @Override
    public synchronized int streamId() {
        return stream == null ? -1 : stream.id();
    }

    @Override
    public synchronized Future<NetSocket> netSocket() {
        if (client.getOptions().isPipelining()) {
            return Future.failedFuture("Cannot upgrade a pipe-lined request");
        }
        if (netSocketPromise == null) {
            netSocketPromise = context.promise();
        }
        return netSocketPromise.future();
    }

    @Override
    public synchronized HttpClientRequest setFollowRedirects(boolean followRedirects) {
        checkEnded();
        if (followRedirects) {
            this.followRedirects = client.getOptions().getMaxRedirects() - 1;
        } else {
            this.followRedirects = 0;
        }
        return this;
    }

    @Override
    public synchronized HttpClientRequest setMaxRedirects(int maxRedirects) {
        Arguments.require(maxRedirects >= 0, "Max redirects must be >= 0");
        checkEnded();
        followRedirects = maxRedirects;
        return this;
    }

    @Override
    public synchronized HttpClientRequestImpl setChunked(boolean chunked) {
        checkEnded();
        if (stream != null) {
            throw new IllegalStateException("Cannot set chunked after data has been written on request");
        }
        // HTTP 1.0 does not support chunking so we ignore this if HTTP 1.0
        if (client.getOptions().getProtocolVersion() != io.vertx.core.http.HttpVersion.HTTP_1_0) {
            this.chunked = chunked;
        }
        return this;
    }

    @Override
    public synchronized boolean isChunked() {
        return chunked;
    }

    @Override
    public synchronized String getRawMethod() {
        return rawMethod;
    }

    @Override
    public synchronized HttpClientRequest setRawMethod(String method) {
        this.rawMethod = method;
        return this;
    }

    @Override
    public synchronized HttpClientRequest setHost(String host) {
        this.hostHeader = host;
        return this;
    }

    @Override
    public synchronized String getHost() {
        return hostHeader;
    }

    @Override
    public synchronized MultiMap headers() {
        if (headers == null) {
            headers = new VertxHttpHeaders();
        }
        return headers;
    }

    @Override
    public synchronized HttpClientRequest putHeader(String name, String value) {
        checkEnded();
        headers().set(name, value);
        return this;
    }

    @Override
    public synchronized HttpClientRequest putHeader(String name, Iterable<String> values) {
        checkEnded();
        headers().set(name, values);
        return this;
    }

    @Override
    public synchronized HttpClientRequest setWriteQueueMaxSize(int maxSize) {
        checkEnded();
        if (stream == null) {
            pendingMaxSize = maxSize;
        } else {
            stream.doSetWriteQueueMaxSize(maxSize);
        }
        return this;
    }

    @Override
    public synchronized boolean writeQueueFull() {
        checkEnded();
        synchronized (this) {
            checkEnded();
            if (stream == null) {
                // Should actually check with max queue size and not always blindly return false
                return false;
            }
        }
        return stream.isNotWritable();
    }

    private synchronized Handler<Throwable> exceptionHandler() {
        return exceptionHandler;
    }

    public synchronized HttpClientRequest exceptionHandler(Handler<Throwable> handler) {
        if (handler != null) {
            checkEnded();
            this.exceptionHandler = handler;
        } else {
            this.exceptionHandler = null;
        }
        return this;
    }

    @Override
    public synchronized HttpClientRequest drainHandler(Handler<Void> handler) {
        if (handler != null) {
            checkEnded();
        }
        checkEnded();
        drainHandler = handler;
        return this;
    }

    @Override
    public synchronized HttpClientRequest continueHandler(Handler<Void> handler) {
        if (handler != null) {
            checkEnded();
        }
        this.continueHandler = handler;
        return this;
    }

    @Override
    public Future<HttpVersion> sendHead() {
        Promise<HttpVersion> promise = context.promise();
        sendHead(promise);
        return promise.future();
    }

    @Override
    public synchronized HttpClientRequest sendHead(Handler<AsyncResult<HttpVersion>> headersHandler) {
        checkEnded();
        checkResponseHandler();
        if (stream != null) {
            throw new IllegalStateException("Head already written");
        } else {
            connect(headersHandler);
        }
        return this;
    }

    @Override
    public synchronized HttpClientRequest putHeader(CharSequence name, CharSequence value) {
        checkEnded();
        headers().set(name, value);
        return this;
    }

    @Override
    public synchronized HttpClientRequest putHeader(CharSequence name, Iterable<CharSequence> values) {
        checkEnded();
        headers().set(name, values);
        return this;
    }

    @Override
    public synchronized HttpClientRequest pushHandler(Handler<HttpClientRequest> handler) {
        pushHandler = handler;
        return this;
    }

    @Override
    boolean reset(Throwable cause) {
        HttpClientStream s;
        synchronized (this) {
            if (reset != null) {
                return false;
            }
            reset = cause;
            s = stream;
        }
        if (s != null) {
            s.reset(cause);
        } else {
            handleException(cause);
        }
        return true;
    }

    private void tryComplete() {
        endPromise.tryComplete();
    }

    @Override
    public synchronized HttpConnection connection() {
        return stream == null ? null : stream.connection();
    }

    @Override
    public HttpClientRequest writeCustomFrame(int type, int flags, Buffer payload) {
        HttpClientStream s;
        synchronized (this) {
            checkEnded();
            if ((s = stream) == null) {
                throw new IllegalStateException("Not yet connected");
            }
        }
        s.writeFrame(type, flags, payload.getByteBuf());
        return this;
    }

    void handleDrained() {
        Handler<Void> handler;
        synchronized (this) {
            if ((handler = drainHandler) == null || endFuture.isComplete()) {
                return;
            }
        }
        try {
            handler.handle(null);
        } catch (Throwable t) {
            handleException(t);
        }
    }

    private void handleNextRequest(HttpClientRequest next, long timeoutMs) {
        next.setHandler(responsePromise.future().getHandler());
        next.exceptionHandler(exceptionHandler());
        exceptionHandler(null);
        next.pushHandler(pushHandler);
        next.setMaxRedirects(followRedirects - 1);
        if (next.getHost() == null) {
            next.setHost(hostHeader);
        }
        if (headers != null) {
            next.headers().addAll(headers);
        }
        endFuture.setHandler(ar -> {
            if (ar.succeeded()) {
                if (timeoutMs > 0) {
                    next.setTimeout(timeoutMs);
                }
                next.end();
            } else {
                next.reset(0);
            }
        });
    }

    void handleContinue() {
        Handler<Void> handler;
        synchronized (this) {
            handler = continueHandler;
        }
        if (handler != null) {
            handler.handle(null);
        }
    }

    void handleResponse(HttpClientResponse resp, long timeoutMs) {
        if (reset == null) {
            int statusCode = resp.statusCode();
            if (followRedirects > 0 && statusCode >= 300 && statusCode < 400) {
                Future<HttpClientRequest> next = client.redirectHandler().apply(resp);
                if (next != null) {
                    next.setHandler(ar -> {
                        if (ar.succeeded()) {
                            handleNextRequest(ar.result(), timeoutMs);
                        } else {
                            responsePromise.fail(ar.cause());
                        }
                    });
                    return;
                }
            }
            responsePromise.complete(resp);
        }
    }

    @Override
    protected String hostHeader() {
        return hostHeader != null ? hostHeader : super.hostHeader();
    }

    private synchronized void connect(Handler<AsyncResult<HttpVersion>> headersHandler) {
        if (!connecting) {

            if (method == HttpMethod.OTHER && rawMethod == null) {
                throw new IllegalStateException(
                        "You must provide a rawMethod when using an HttpMethod.OTHER method");
            }

            SocketAddress peerAddress;
            if (hostHeader != null) {
                int idx = hostHeader.lastIndexOf(':');
                if (idx != -1) {
                    peerAddress = SocketAddress.inetSocketAddress(Integer.parseInt(hostHeader.substring(idx + 1)),
                            hostHeader.substring(0, idx));
                } else {
                    peerAddress = SocketAddress.inetSocketAddress(80, hostHeader);
                }
            } else {
                String peerHost = host;
                if (peerHost.endsWith(".")) {
                    peerHost = peerHost.substring(0, peerHost.length() - 1);
                }
                peerAddress = SocketAddress.inetSocketAddress(port, peerHost);
            }

            // We defer actual connection until the first part of body is written or end is called
            // This gives the user an opportunity to set an exception handler before connecting so
            // they can capture any exceptions on connection
            connecting = true;
            client.getConnectionForRequest(context, peerAddress, this, netSocketPromise, ssl, server, ar -> {
                if (ar.succeeded()) {
                    HttpClientStream stream = ar.result();
                    // No need to synchronize as the thread is the same that set exceptionOccurred to true
                    // exceptionOccurred=true getting the connection => it's a TimeoutException
                    if (reset != null) {
                        stream.reset(reset);
                    } else {
                        connected(headersHandler, stream);
                    }
                } else {
                    handleException(ar.cause());
                }
            });
        }
    }

    private void connected(Handler<AsyncResult<HttpVersion>> headersHandler, HttpClientStream stream) {
        synchronized (this) {
            this.stream = stream;

            // If anything was written or the request ended before we got the connection, then
            // we need to write it now

            if (pendingMaxSize != -1) {
                stream.doSetWriteQueueMaxSize(pendingMaxSize);
            }
            ByteBuf pending = pendingChunks;
            pendingChunks = null;
            Handler<AsyncResult<Void>> handler = null;
            if (pendingHandlers != null) {
                List<Handler<AsyncResult<Void>>> handlers = pendingHandlers;
                pendingHandlers = null;
                handler = ar -> {
                    handlers.forEach(h -> h.handle(ar));
                };
            }
            if (headersHandler != null) {
                Handler<AsyncResult<Void>> others = handler;
                handler = ar -> {
                    if (others != null) {
                        others.handle(ar);
                    }
                    headersHandler.handle(ar.map(stream.version()));
                };
            }
            stream.writeHead(method, rawMethod, uri, headers, hostHeader(), chunked, pending, ended, priority,
                    handler);
            if (ended) {
                tryComplete();
            }
            this.connecting = false;
            this.stream = stream;
        }
    }

    @Override
    public Future<Void> end(String chunk) {
        Promise<Void> promise = context.promise();
        end(chunk, promise);
        return promise.future();
    }

    @Override
    public void end(String chunk, Handler<AsyncResult<Void>> handler) {
        end(Buffer.buffer(chunk), handler);
    }

    @Override
    public Future<Void> end(String chunk, String enc) {
        Promise<Void> promise = context.promise();
        end(chunk, enc, promise);
        return promise.future();
    }

    @Override
    public void end(String chunk, String enc, Handler<AsyncResult<Void>> handler) {
        Objects.requireNonNull(enc, "no null encoding accepted");
        end(Buffer.buffer(chunk, enc), handler);
    }

    @Override
    public Future<Void> end(Buffer chunk) {
        Promise<Void> promise = context.promise();
        write(chunk.getByteBuf(), true, promise);
        return promise.future();
    }

    @Override
    public void end(Buffer chunk, Handler<AsyncResult<Void>> handler) {
        write(chunk.getByteBuf(), true, handler);
    }

    @Override
    public Future<Void> end() {
        Promise<Void> promise = context.promise();
        end(promise);
        return promise.future();
    }

    @Override
    public void end(Handler<AsyncResult<Void>> handler) {
        write(null, true, handler);
    }

    @Override
    public Future<Void> write(Buffer chunk) {
        Promise<Void> promise = context.promise();
        write(chunk, promise);
        return promise.future();
    }

    @Override
    public void write(Buffer chunk, Handler<AsyncResult<Void>> handler) {
        ByteBuf buf = chunk.getByteBuf();
        write(buf, false, handler);
    }

    @Override
    public Future<Void> write(String chunk) {
        Promise<Void> promise = context.promise();
        write(chunk, promise);
        return promise.future();
    }

    @Override
    public void write(String chunk, Handler<AsyncResult<Void>> handler) {
        write(Buffer.buffer(chunk).getByteBuf(), false, handler);
    }

    @Override
    public Future<Void> write(String chunk, String enc) {
        Promise<Void> promise = context.promise();
        write(chunk, enc, promise);
        return promise.future();
    }

    @Override
    public void write(String chunk, String enc, Handler<AsyncResult<Void>> handler) {
        Objects.requireNonNull(enc, "no null encoding accepted");
        write(Buffer.buffer(chunk, enc).getByteBuf(), false, handler);
    }

    private boolean requiresContentLength() {
        return !chunked && (headers == null || !headers.contains(CONTENT_LENGTH));
    }

    private void write(ByteBuf buff, boolean end, Handler<AsyncResult<Void>> completionHandler) {
        if (buff == null && !end) {
            return;
        }
        HttpClientStream s;
        synchronized (this) {
            if (ended) {
                completionHandler
                        .handle(Future.failedFuture(new IllegalStateException("Request already complete")));
                return;
            }
            checkResponseHandler();
            if (end) {
                if (buff != null && requiresContentLength()) {
                    headers().set(CONTENT_LENGTH, String.valueOf(buff.readableBytes()));
                }
            } else if (requiresContentLength()) {
                throw new IllegalStateException(
                        "You must set the Content-Length header to be the total size of the message "
                                + "body BEFORE sending any data if you are not using HTTP chunked encoding.");
            }
            ended |= end;
            if (stream == null) {
                if (buff != null) {
                    if (pendingChunks == null) {
                        pendingChunks = buff;
                    } else {
                        CompositeByteBuf pending;
                        if (pendingChunks instanceof CompositeByteBuf) {
                            pending = (CompositeByteBuf) pendingChunks;
                        } else {
                            pending = Unpooled.compositeBuffer();
                            pending.addComponent(true, pendingChunks);
                            pendingChunks = pending;
                        }
                        pending.addComponent(true, buff);
                    }
                }
                if (completionHandler != null) {
                    if (pendingHandlers == null) {
                        pendingHandlers = new ArrayList<>();
                    }
                    pendingHandlers.add(completionHandler);
                }
                connect(null);
                return;
            }
            s = stream;
        }
        s.writeBuffer(buff, end, completionHandler);
        if (end) {
            tryComplete();
        }
    }

    private void checkEnded() {
        if (ended) {
            throw new IllegalStateException("Request already complete");
        }
    }

    private void checkResponseHandler() {
        if (stream == null && !connecting && responsePromise.future().getHandler() == null) {
            throw new IllegalStateException("You must set a response handler before connecting to the server");
        }
    }

    synchronized Handler<HttpClientRequest> pushHandler() {
        return pushHandler;
    }

    @Override
    public synchronized HttpClientRequest setStreamPriority(StreamPriority priority) {
        if (stream != null) {
            stream.updatePriority(priority);
        } else {
            this.priority = priority;
        }
        return this;
    }

    @Override
    public synchronized StreamPriority getStreamPriority() {
        HttpClientStream s = stream;
        return s != null ? s.priority() : priority;
    }
}