/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
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.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.CaseInsensitiveHeaders;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpConnection;
import io.vertx.core.http.HttpFrame;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.net.NetSocket;
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.
*
* @author <a href="http://tfox.org">Tim Fox</a>
*/
public class HttpClientRequestImpl extends HttpClientRequestBase implements HttpClientRequest {
private final VertxInternal vertx;
private Handler<HttpClientResponse> respHandler;
private Handler<Void> endHandler;
private boolean chunked;
private String hostHeader;
private String rawMethod;
private Handler<Void> continueHandler;
private HttpClientStream stream;
private volatile Object lock;
private Handler<Void> drainHandler;
private Handler<HttpClientRequest> pushHandler;
private Handler<HttpConnection> connectionHandler;
private boolean headWritten;
private boolean completed;
private Handler<Void> completionHandler;
private Long reset;
private HttpClientResponseImpl response;
private ByteBuf pendingChunks;
private CompositeByteBuf cachedChunks;
private int pendingMaxSize = -1;
private int followRedirects;
private boolean connecting;
private boolean writeHead;
private long written;
private CaseInsensitiveHeaders headers;
HttpClientRequestImpl(HttpClientImpl client, boolean ssl, HttpMethod method, String host, int port,
String relativeURI, VertxInternal vertx) {
super(client, ssl, method, host, port, relativeURI);
this.chunked = false;
this.vertx = vertx;
}
@Override
public int streamId() {
synchronized (getLock()) {
return stream != null ? stream.id() : -1;
}
}
@Override
public HttpClientRequest handler(Handler<HttpClientResponse> handler) {
synchronized (getLock()) {
if (handler != null) {
checkComplete();
respHandler = checkConnect(method, handler);
} else {
respHandler = null;
}
return this;
}
}
@Override
public HttpClientRequest pause() {
return this;
}
@Override
public HttpClientRequest resume() {
return this;
}
@Override
public HttpClientRequest setFollowRedirects(boolean followRedirects) {
synchronized (getLock()) {
checkComplete();
if (followRedirects) {
this.followRedirects = client.getOptions().getMaxRedirects() - 1;
} else {
this.followRedirects = 0;
}
return this;
}
}
@Override
public HttpClientRequest endHandler(Handler<Void> endHandler) {
synchronized (getLock()) {
if (endHandler != null) {
checkComplete();
}
this.endHandler = endHandler;
return this;
}
}
@Override
public HttpClientRequestImpl setChunked(boolean chunked) {
synchronized (getLock()) {
checkComplete();
if (written > 0) {
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 boolean isChunked() {
synchronized (getLock()) {
return chunked;
}
}
@Override
public String getRawMethod() {
synchronized (getLock()) {
return rawMethod;
}
}
@Override
public HttpClientRequest setRawMethod(String method) {
synchronized (getLock()) {
this.rawMethod = method;
return this;
}
}
@Override
public HttpClientRequest setHost(String host) {
synchronized (getLock()) {
this.hostHeader = host;
return this;
}
}
@Override
public String getHost() {
synchronized (getLock()) {
return hostHeader;
}
}
@Override
public MultiMap headers() {
synchronized (getLock()) {
if (headers == null) {
headers = new CaseInsensitiveHeaders();
}
return headers;
}
}
@Override
public HttpClientRequest putHeader(String name, String value) {
synchronized (getLock()) {
checkComplete();
headers().set(name, value);
return this;
}
}
@Override
public HttpClientRequest putHeader(String name, Iterable<String> values) {
synchronized (getLock()) {
checkComplete();
headers().set(name, values);
return this;
}
}
@Override
public HttpClientRequestImpl write(Buffer chunk) {
synchronized (getLock()) {
checkComplete();
checkResponseHandler();
ByteBuf buf = chunk.getByteBuf();
write(buf, false);
return this;
}
}
@Override
public HttpClientRequestImpl write(String chunk) {
synchronized (getLock()) {
checkComplete();
checkResponseHandler();
return write(Buffer.buffer(chunk));
}
}
@Override
public HttpClientRequestImpl write(String chunk, String enc) {
synchronized (getLock()) {
Objects.requireNonNull(enc, "no null encoding accepted");
checkComplete();
checkResponseHandler();
return write(Buffer.buffer(chunk, enc));
}
}
@Override
public HttpClientRequest setWriteQueueMaxSize(int maxSize) {
synchronized (getLock()) {
checkComplete();
if (stream != null) {
stream.doSetWriteQueueMaxSize(maxSize);
} else {
pendingMaxSize = maxSize;
}
return this;
}
}
@Override
public boolean writeQueueFull() {
synchronized (getLock()) {
checkComplete();
return stream != null && stream.isNotWritable();
}
}
@Override
public HttpClientRequest drainHandler(Handler<Void> handler) {
synchronized (getLock()) {
checkComplete();
this.drainHandler = handler;
if (stream != null) {
stream.getContext().runOnContext(v -> {
synchronized (getLock()) {
if (stream != null) {
stream.checkDrained();
}
}
});
}
return this;
}
}
@Override
public HttpClientRequest continueHandler(Handler<Void> handler) {
synchronized (getLock()) {
checkComplete();
this.continueHandler = handler;
return this;
}
}
@Override
public HttpClientRequest sendHead() {
return sendHead(null);
}
@Override
public HttpClientRequest sendHead(Handler<HttpVersion> completionHandler) {
synchronized (getLock()) {
checkComplete();
checkResponseHandler();
if (stream != null) {
if (!headWritten) {
writeHead();
if (completionHandler != null) {
completionHandler.handle(stream.version());
}
}
} else {
connect(completionHandler);
writeHead = true;
}
return this;
}
}
@Override
public void end(String chunk) {
synchronized (getLock()) {
end(Buffer.buffer(chunk));
}
}
@Override
public void end(String chunk, String enc) {
synchronized (getLock()) {
Objects.requireNonNull(enc, "no null encoding accepted");
end(Buffer.buffer(chunk, enc));
}
}
@Override
public void end(Buffer chunk) {
synchronized (getLock()) {
checkComplete();
checkResponseHandler();
write(chunk.getByteBuf(), true);
}
}
@Override
public void end() {
synchronized (getLock()) {
checkComplete();
checkResponseHandler();
write(null, true);
}
}
@Override
public HttpClientRequest putHeader(CharSequence name, CharSequence value) {
synchronized (getLock()) {
checkComplete();
headers().set(name, value);
return this;
}
}
@Override
public HttpClientRequest putHeader(CharSequence name, Iterable<CharSequence> values) {
synchronized (getLock()) {
checkComplete();
headers().set(name, values);
return this;
}
}
@Override
public HttpClientRequest pushHandler(Handler<HttpClientRequest> handler) {
synchronized (getLock()) {
pushHandler = handler;
}
return this;
}
@Override
public boolean reset(long code) {
synchronized (getLock()) {
if (reset == null) {
reset = code;
if (!completed) {
completed = true;
if (stream != null) {
stream.resetRequest(code);
}
if (completionHandler != null) {
completionHandler.handle(null);
}
} else {
if (response != null) {
stream.resetResponse(code);
}
}
return true;
}
return false;
}
}
@Override
public HttpConnection connection() {
synchronized (getLock()) {
return stream != null ? stream.connection() : null;
}
}
@Override
public HttpClientRequest connectionHandler(@Nullable Handler<HttpConnection> handler) {
synchronized (getLock()) {
connectionHandler = handler;
return this;
}
}
@Override
public HttpClientRequest writeCustomFrame(int type, int flags, Buffer payload) {
synchronized (getLock()) {
if (stream == null) {
throw new IllegalStateException("Not yet connected");
}
stream.writeFrame(type, flags, payload.getByteBuf());
}
return this;
}
void handleDrained() {
synchronized (getLock()) {
if (!completed && drainHandler != null) {
try {
drainHandler.handle(null);
} catch (Throwable t) {
handleException(t);
}
}
}
}
private void handleNextRequest(HttpClientResponse resp, HttpClientRequestImpl next, long timeoutMs) {
next.handler(respHandler);
next.exceptionHandler(exceptionHandler());
exceptionHandler(null);
next.endHandler(endHandler);
next.pushHandler = pushHandler;
next.followRedirects = followRedirects - 1;
next.written = written;
if (next.hostHeader == null) {
next.hostHeader = hostHeader;
}
if (headers != null && next.headers == null) {
next.headers().addAll(headers);
}
ByteBuf body;
switch (next.method) {
case GET:
body = null;
break;
case OTHER:
next.rawMethod = rawMethod;
body = null;
break;
default:
if (cachedChunks != null) {
body = cachedChunks;
} else {
body = null;
}
break;
}
cachedChunks = null;
Future<Void> fut = Future.future();
fut.setHandler(ar -> {
if (ar.succeeded()) {
if (timeoutMs > 0) {
next.setTimeout(timeoutMs);
}
next.write(body, true);
} else {
next.handleException(ar.cause());
}
});
if (exceptionOccurred != null) {
fut.fail(exceptionOccurred);
}
else if (completed) {
fut.complete();
} else {
exceptionHandler(err -> {
if (!fut.isComplete()) {
fut.fail(err);
}
});
completionHandler = v -> {
if (!fut.isComplete()) {
fut.complete();
}
};
}
}
protected void doHandleResponse(HttpClientResponseImpl resp, long timeoutMs) {
if (reset != null) {
stream.resetResponse(reset);
} else {
response = resp;
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(resp, (HttpClientRequestImpl) ar.result(), timeoutMs);
} else {
handleException(ar.cause());
}
});
return;
}
}
if (statusCode == 100) {
if (continueHandler != null) {
continueHandler.handle(null);
}
} else {
if (respHandler != null) {
respHandler.handle(resp);
}
if (endHandler != null) {
endHandler.handle(null);
}
}
}
}
@Override
protected String hostHeader() {
return hostHeader != null ? hostHeader : super.hostHeader();
}
// After connecting we should synchronize on the client connection instance to prevent deadlock conditions
// but there is a catch - the client connection is null before connecting so we synchronized on this before that
// point
protected Object getLock() {
// We do the initial check outside the synchronized block to prevent the hit of synchronized once the conn has
// been set
if (lock != null) {
return lock;
} else {
synchronized (this) {
if (lock != null) {
return lock;
} else {
return this;
}
}
}
}
private Handler<HttpClientResponse> checkConnect(io.vertx.core.http.HttpMethod method, Handler<HttpClientResponse> handler) {
if (method == io.vertx.core.http.HttpMethod.CONNECT) {
// special handling for CONNECT
handler = connectHandler(handler);
}
return handler;
}
private Handler<HttpClientResponse> connectHandler(Handler<HttpClientResponse> responseHandler) {
Objects.requireNonNull(responseHandler, "no null responseHandler accepted");
return resp -> {
HttpClientResponse response;
if (resp.statusCode() == 200) {
// connect successful force the modification of the ChannelPipeline
// beside this also pause the socket for now so the user has a chance to register its dataHandler
// after received the NetSocket
NetSocket socket = resp.netSocket();
socket.pause();
response = new HttpClientResponse() {
private boolean resumed;
@Override
public HttpClientRequest request() {
return resp.request();
}
@Override
public int statusCode() {
return resp.statusCode();
}
@Override
public String statusMessage() {
return resp.statusMessage();
}
@Override
public MultiMap headers() {
return resp.headers();
}
@Override
public String getHeader(String headerName) {
return resp.getHeader(headerName);
}
@Override
public String getHeader(CharSequence headerName) {
return resp.getHeader(headerName);
}
@Override
public String getTrailer(String trailerName) {
return resp.getTrailer(trailerName);
}
@Override
public MultiMap trailers() {
return resp.trailers();
}
@Override
public List<String> cookies() {
return resp.cookies();
}
@Override
public HttpVersion version() {
return resp.version();
}
@Override
public HttpClientResponse bodyHandler(Handler<Buffer> bodyHandler) {
resp.bodyHandler(bodyHandler);
return this;
}
@Override
public HttpClientResponse customFrameHandler(Handler<HttpFrame> handler) {
resp.customFrameHandler(handler);
return this;
}
@Override
public synchronized NetSocket netSocket() {
if (!resumed) {
resumed = true;
vertx.getContext().runOnContext((v) -> socket.resume()); // resume the socket now as the user had the chance to register a dataHandler
}
return socket;
}
@Override
public HttpClientResponse endHandler(Handler<Void> endHandler) {
resp.endHandler(endHandler);
return this;
}
@Override
public HttpClientResponse handler(Handler<Buffer> handler) {
resp.handler(handler);
return this;
}
@Override
public HttpClientResponse pause() {
resp.pause();
return this;
}
@Override
public HttpClientResponse resume() {
resp.resume();
return this;
}
@Override
public HttpClientResponse exceptionHandler(Handler<Throwable> handler) {
resp.exceptionHandler(handler);
return this;
}
};
} else {
response = resp;
}
responseHandler.handle(response);
};
}
private synchronized void connect(Handler<HttpVersion> headersCompletionHandler) {
if (!connecting) {
if (method == HttpMethod.OTHER && rawMethod == null) {
throw new IllegalStateException("You must provide a rawMethod when using an HttpMethod.OTHER method");
}
Waiter waiter = new Waiter(this, vertx.getContext()) {
@Override
void handleFailure(Throwable failure) {
handleException(failure);
}
@Override
void handleConnection(HttpClientConnection conn) {
synchronized (HttpClientRequestImpl.this) {
if (connectionHandler != null) {
connectionHandler.handle(conn);
}
}
}
@Override
void handleStream(HttpClientStream stream) {
connected(stream, headersCompletionHandler);
}
@Override
boolean isCancelled() {
// No need to synchronize as the thread is the same that set exceptionOccurred to true
// exceptionOccurred=true getting the connection => it's a TimeoutException
return exceptionOccurred != null || reset != null;
}
};
String peerHost;
if (hostHeader != null) {
int idx = hostHeader.lastIndexOf(':');
if (idx != -1) {
peerHost = hostHeader.substring(0, idx);
} else {
peerHost = hostHeader;
}
} else {
peerHost = host;
}
// 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
client.getConnectionForRequest(peerHost, ssl, port, host, waiter);
connecting = true;
}
}
private void connected(HttpClientStream stream, Handler<HttpVersion> headersCompletionHandler) {
HttpClientConnection conn = stream.connection();
synchronized (this) {
this.stream = stream;
stream.beginRequest(this);
// If anything was written or the request ended before we got the connection, then
// we need to write it now
if (pendingMaxSize != -1) {
this.stream.doSetWriteQueueMaxSize(pendingMaxSize);
}
if (pendingChunks != null) {
ByteBuf pending = pendingChunks;
pendingChunks = null;
if (completed) {
// we also need to write the head so optimize this and write all out in once
writeHeadWithContent(pending, true);
conn.reportBytesWritten(written);
if (respHandler != null) {
this.stream.endRequest();
}
} else {
writeHeadWithContent(pending, false);
if (headersCompletionHandler != null) {
headersCompletionHandler.handle(stream.version());
}
}
} else {
if (completed) {
// we also need to write the head so optimize this and write all out in once
writeHeadWithContent(null, true);
conn.reportBytesWritten(written);
if (respHandler != null) {
this.stream.endRequest();
}
} else {
if (writeHead) {
writeHead();
if (headersCompletionHandler != null) {
headersCompletionHandler.handle(stream.version());
}
}
}
}
// Set the lock at the end of the block so we are sure that another non vertx thread will get access to the connection
// when this callback runs on the 'this' lock
this.lock = conn;
}
}
private boolean contentLengthSet() {
return headers != null && headers().contains(CONTENT_LENGTH);
}
private void writeHead() {
stream.writeHead(method, rawMethod, uri, headers, hostHeader(), chunked);
headWritten = true;
}
private void writeHeadWithContent(ByteBuf buf, boolean end) {
stream.writeHeadWithContent(method, rawMethod, uri, headers, hostHeader(), chunked, buf, end);
headWritten = true;
}
private void write(ByteBuf buff, boolean end) {
if (buff == null && !end) {
// nothing to write to the connection just return
return;
}
if (end) {
if (buff != null && !chunked && !contentLengthSet()) {
headers().set(CONTENT_LENGTH, String.valueOf(buff.readableBytes()));
}
} else {
if (!chunked && !contentLengthSet()) {
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.");
}
}
if (buff != null) {
written += buff.readableBytes();
if (followRedirects > 0) {
if (cachedChunks == null) {
cachedChunks = Unpooled.compositeBuffer();
}
cachedChunks.addComponent(true, buff);
}
}
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);
}
}
connect(null);
} else {
if (!headWritten) {
writeHeadWithContent(buff, end);
} else {
stream.writeBuffer(buff, end);
}
if (end) {
stream.connection().reportBytesWritten(written);
if (respHandler != null) {
stream.endRequest();
}
}
}
if (end) {
completed = true;
if (completionHandler != null) {
completionHandler.handle(null);
}
}
}
void handleResponseEnd() {
synchronized (getLock()) {
response = null;
}
}
protected void checkComplete() {
if (completed) {
throw new IllegalStateException("Request already complete");
}
}
private void checkResponseHandler() {
if (respHandler == null) {
throw new IllegalStateException("You must set an handler for the HttpClientResponse before connecting");
}
}
Handler<HttpClientRequest> pushHandler() {
synchronized (getLock()) {
return pushHandler;
}
}
}