All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.http.HttpClientConnection Maven / Gradle / Ivy

/*
 * Copyright (C) 2015 SoftIndex LLC.
 *
 * 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 io.datakernel.http;

import io.datakernel.async.Callback;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.eventloop.AsyncTcpSocket;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.exception.ParseException;

import java.net.InetSocketAddress;

import static io.datakernel.bytebuf.ByteBufStrings.SP;
import static io.datakernel.bytebuf.ByteBufStrings.decodeDecimal;
import static io.datakernel.http.HttpHeaders.CONNECTION;

@SuppressWarnings("ThrowableInstanceNeverThrown")
final class HttpClientConnection extends AbstractHttpConnection {
	private static final HttpHeaders.Value CONNECTION_KEEP_ALIVE = HttpHeaders.asBytes(CONNECTION, "keep-alive");

	private Callback callback;
	private HttpResponse response;
	private final AsyncHttpClient client;
	private final AsyncHttpClient.Inspector inspector;

	final InetSocketAddress remoteAddress;
	HttpClientConnection addressPrev;
	HttpClientConnection addressNext;

	private Exception closeError;

	HttpClientConnection(Eventloop eventloop, InetSocketAddress remoteAddress,
	                     AsyncTcpSocket asyncTcpSocket, AsyncHttpClient client, char[] headerChars,
	                     int maxHttpMessageSize) {
		super(eventloop, asyncTcpSocket, headerChars, maxHttpMessageSize);
		this.remoteAddress = remoteAddress;
		this.client = client;
		this.inspector = client.inspector;
	}

	@Override
	public void onRegistered() {
	}

	@Override
	public void onClosedWithError(Exception e) {
		if (inspector != null && e != null) inspector.onHttpError(this, callback == null, e);
		readQueue.clear();
		if (callback != null) {
			Callback callback = this.callback;
			eventloop.post(() -> callback.setException(e));
			this.callback = null;
		} else {
			closeError = e;
		}
		onClosed();
	}

	@Override
	protected void onFirstLine(ByteBuf line) throws ParseException {
		if (line.peek(0) != 'H' || line.peek(1) != 'T' || line.peek(2) != 'T' || line.peek(3) != 'P' || line.peek(4) != '/' || line.peek(5) != '1') {
			line.recycle();
			throw new ParseException("Invalid response");
		}

		keepAlive = false;
		int sp1;
		if (line.peek(6) == SP) {
			sp1 = line.readPosition() + 7;
		} else if (line.peek(6) == '.' && (line.peek(7) == '1' || line.peek(7) == '0') && line.peek(8) == SP) {
			if (line.peek(7) == '1')
				keepAlive = true;
			sp1 = line.readPosition() + 9;
		} else {
			line.recycle();
			throw new ParseException("Invalid response: " + new String(line.array(), line.readPosition(), line.readRemaining()));
		}

		int sp2;
		for (sp2 = sp1; sp2 < line.writePosition(); sp2++) {
			if (line.at(sp2) == SP) {
				break;
			}
		}

		int statusCode = decodeDecimal(line.array(), sp1, sp2 - sp1);
		if (!(statusCode >= 100 && statusCode < 600)) {
			line.recycle();
			throw new ParseException("Invalid HTTP Status Code " + statusCode);
		}
		response = HttpResponse.ofCode(statusCode);
		if (isNoBodyMessage(response)) {
			// Reset Content-Length for the case keep-alive connection
			contentLength = 0;
		}

		line.recycle();
	}

	/**
	 * RFC 2616, section 4.4
	 * 1.Any response message which "MUST NOT" include a message-body (such as the 1xx, 204, and 304 responses and any response to a HEAD request) is always
	 * terminated by the first empty line after the header fields, regardless of the entity-header fields present in the message.
	 */
	private static boolean isNoBodyMessage(HttpResponse message) {
		int messageCode = message.getCode();
		return (messageCode >= 100 && messageCode < 200) || messageCode == 204 || messageCode == 304;
	}

	@Override
	protected void onHeader(HttpHeader header, ByteBuf value) throws ParseException {
		super.onHeader(header, value);
		response.addHeader(header, value);
	}

	@Override
	protected void onHttpMessage(ByteBuf bodyBuf) {
		assert !isClosed();
		Callback callback = this.callback;
		HttpResponse response = this.response;
		this.response = null;
		this.callback = null;
		response.setBody(bodyBuf);

		if (inspector != null) inspector.onHttpResponse(this, response);
		if (keepAlive && client.keepAliveTimeoutMillis != 0) {
			reset();
			client.returnToKeepAlivePool(this);
		} else {
			close();
		}

		callback.set(response);
		response.recycleBufs();
	}

	@Override
	public void onReadEndOfStream() {
		if (callback != null) {
			closeWithError(CLOSED_CONNECTION);
			assert callback == null;
		}
		close();
	}

	@Override
	protected void reset() {
		reading = END_OF_STREAM;
		if (response != null) {
			response.recycleBufs();
			response = null;
		}
		super.reset();
	}

	/**
	 * Sends the request, recycles it and closes connection in case of timeout
	 *
	 * @param request request for sending
	 */
	public void send(HttpRequest request, Callback callback) {
		this.callback = callback;
		assert pool == null;
		(pool = client.poolWriting).addLastNode(this);
		poolTimestamp = eventloop.currentTimeMillis();
		request.addHeader(CONNECTION_KEEP_ALIVE);
		asyncTcpSocket.write(request.toByteBuf());
		request.recycleBufs();
	}

	@Override
	public void onWrite() {
		assert !isClosed();
		reading = FIRSTLINE;
		assert pool == client.poolWriting;
		pool.removeNode(this);
		(pool = client.poolReading).addLastNode(this);
		poolTimestamp = eventloop.currentTimeMillis();
		asyncTcpSocket.read();
	}

	/**
	 * After closing this connection it removes it from its connections cache and recycles
	 * Http response.
	 */
	protected void onClosed() {
		assert callback == null;
		if (pool == client.poolKeepAlive) {
			AddressLinkedList addresses = client.addresses.get(remoteAddress);
			addresses.removeNode(this);
			if (addresses.isEmpty()) {
				client.addresses.remove(remoteAddress);
			}
		}

		// pool will be null if socket was closed by the peer just before connection.send() invocation
		if (pool != null) {
			pool.removeNode(this);
			pool = null;
		}

		client.onConnectionClosed();
		bodyQueue.clear();
		if (response != null) {
			response.recycleBufs();
		}
	}

	public Exception getCloseError() {
		return closeError;
	}

	@Override
	public String toString() {
		return "HttpClientConnection{" +
				"callback=" + callback +
				", response=" + response +
				", httpClient=" + client +
				", keepAlive=" + (pool == client.poolKeepAlive) +
//				", lastRequestUrl='" + (request.getFullUrl() == null ? "" : request.getFullUrl()) + '\'' +
				", remoteAddress=" + remoteAddress +
				',' + super.toString() +
				'}';
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy