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

io.helidon.webclient.NettyClientHandler Maven / Gradle / Ivy

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2020, 2024 Oracle and/or its affiliates.
 *
 * 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.helidon.webclient;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.helidon.common.http.DataChunk;
import io.helidon.common.http.Http;
import io.helidon.common.reactive.BufferedEmittingPublisher;
import io.helidon.common.reactive.Single;
import io.helidon.webclient.spi.WebClientService;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.AttributeKey;

import static io.helidon.webclient.WebClientRequestBuilderImpl.COMPLETED;
import static io.helidon.webclient.WebClientRequestBuilderImpl.IN_USE;
import static io.helidon.webclient.WebClientRequestBuilderImpl.RECEIVED;
import static io.helidon.webclient.WebClientRequestBuilderImpl.REQUEST;
import static io.helidon.webclient.WebClientRequestBuilderImpl.REQUEST_ID;
import static io.helidon.webclient.WebClientRequestBuilderImpl.RESPONSE;
import static io.helidon.webclient.WebClientRequestBuilderImpl.RESPONSE_RECEIVED;
import static io.helidon.webclient.WebClientRequestBuilderImpl.RESULT;
import static io.helidon.webclient.WebClientRequestBuilderImpl.RETURN;
import static io.helidon.webclient.WebClientRequestBuilderImpl.WILL_CLOSE;

/**
 * Created for each request/response interaction.
 */
class NettyClientHandler extends SimpleChannelInboundHandler {

    private static final Logger LOGGER = Logger.getLogger(NettyClientHandler.class.getName());

    private static final AttributeKey SERVICE_RESPONSE = AttributeKey.valueOf("serviceResponse");
    /**
     * Instance of the publisher used to handle response.
     */
    static final AttributeKey PUBLISHER = AttributeKey.valueOf("publisher");

    private static final List HTTP_INTERCEPTORS = new ArrayList<>();

    static {
        HTTP_INTERCEPTORS.add(new RedirectInterceptor());
    }

    private HttpResponsePublisher publisher;
    private ResponseCloser responseCloser;
    private long requestId;

    /**
     * Creates new instance.
     */
    NettyClientHandler() {
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        ctx.flush();
        if (publisher != null && publisher.hasRequests()) {
            channel.read();
        }
        if (!channel.attr(WILL_CLOSE).get()
                && channel.hasAttr(RETURN)
                && channel.attr(RETURN).get().compareAndSet(true, false)) {
            LOGGER.finest(() -> "(client reqID: " + requestId + ") "
                    + "Returning channel " + channel.hashCode() + " to the cache");
            channel.attr(IN_USE).get().set(false);
            responseCloser.cf.complete(null);
            publisher.complete();
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws IOException {
        Channel channel = ctx.channel();
        if (msg instanceof HttpResponse) {
            channel.config().setAutoRead(false);
            HttpResponse response = (HttpResponse) msg;
            this.requestId = channel.attr(REQUEST_ID).get();
            channel.attr(RESPONSE_RECEIVED).set(true);
            WebClientRequestImpl clientRequest = channel.attr(REQUEST).get();
            RequestConfiguration requestConfiguration = clientRequest.configuration();
            LOGGER.finest(() -> "(client reqID: " + requestId + ") Initial http response message received");

            this.publisher = new HttpResponsePublisher(ctx);
            channel.attr(PUBLISHER).set(this.publisher);
            this.responseCloser = new ResponseCloser(ctx);
            WebClientResponseImpl.Builder responseBuilder = WebClientResponseImpl.builder();
            responseBuilder.contentPublisher(publisher)
                    .readerContext(requestConfiguration.readerContext())
                    .status(helidonStatus(response.status()))
                    .httpVersion(Http.Version.create(response.protocolVersion().toString()))
                    .responseCloser(responseCloser)
                    .lastEndpointURI(requestConfiguration.requestURI())
                    .mediaTypeParserRelaxed(requestConfiguration.mediaTypeParserRelaxed());

            HttpHeaders nettyHeaders = response.headers();
            for (String name : nettyHeaders.names()) {
                List values = nettyHeaders.getAll(name);
                responseBuilder.addHeader(name, values);
            }

            String connection = nettyHeaders.get(Http.Header.CONNECTION, HttpHeaderValues.CLOSE.toString());
            if (connection.equals(HttpHeaderValues.CLOSE.toString())) {
                ctx.channel().attr(WILL_CLOSE).set(true);
            }

            // we got a response, we can safely complete the future
            // all errors are now fed only to the publisher
            WebClientResponse clientResponse = responseBuilder.build();
            channel.attr(RESPONSE).set(clientResponse);

            requestConfiguration.cookieManager().put(requestConfiguration.requestURI(),
                                                     clientResponse.headers().toMap());

            for (HttpInterceptor interceptor : HTTP_INTERCEPTORS) {
                if (interceptor.shouldIntercept(response.status(), requestConfiguration)) {
                    boolean continueAfter = !interceptor.continueAfterInterception();
                    if (continueAfter) {
                        responseCloser.close().thenAccept(future -> LOGGER.finest(() -> "Response closed due to redirection"));
                    }
                    interceptor.handleInterception(response, clientRequest, channel.attr(RESULT).get());
                    if (continueAfter) {
                        return;
                    }
                }
            }

            WebClientServiceResponse clientServiceResponse =
                    new WebClientServiceResponseImpl(requestConfiguration.context().get(),
                                                     clientResponse.headers(),
                                                     clientResponse.status());

            channel.attr(SERVICE_RESPONSE).set(clientServiceResponse);

            List services = requestConfiguration.services();
            CompletionStage csr = CompletableFuture.completedFuture(clientServiceResponse);

            for (WebClientService service : services) {
                csr = csr.thenCompose(clientSerResponse -> service.response(clientRequest, clientSerResponse));
            }

            CompletableFuture responseReceived = channel.attr(RECEIVED).get();
            CompletableFuture responseFuture = channel.attr(RESULT).get();
            csr.whenComplete((clientSerResponse, throwable) -> {
                if (throwable != null) {
                    responseReceived.completeExceptionally(throwable);
                    responseFuture.completeExceptionally(throwable);
                    responseCloser.close();
                } else {
                    responseReceived.complete(clientServiceResponse);
                    responseReceived.thenRun(() -> {
                        if (shouldResponseAutomaticallyClose(clientResponse)) {
                            responseCloser.close()
                                    .thenAccept(aVoid -> {
                                        LOGGER.finest(() -> "Response automatically closed. No entity expected");
                                    });
                        }
                        responseFuture.complete(clientResponse);
                    }).exceptionally(t -> {
                        responseFuture.completeExceptionally(t);
                        responseCloser.close();
                        return null;
                    });
                }
            });
        }

        if (responseCloser.isClosed()) {
            if (!channel.attr(WILL_CLOSE).get() && channel.hasAttr(RETURN)) {
                if (msg instanceof LastHttpContent) {
                    LOGGER.finest(() -> "(client reqID: " + requestId + ") Draining finished");
                    if (channel.isActive()) {
                        channel.attr(RETURN).get().set(true);
                    }
                } else {
                    LOGGER.finest(() -> "(client reqID: " + requestId + ") Draining not finished, requesting new chunk");
                }
                channel.read();
            }
            return;
        }

        // never "else-if" - msg may be an instance of more than one type, we must process all of them
        if (msg instanceof HttpContent) {
            HttpContent content = (HttpContent) msg;
            publisher.emit(content.content());
        }

        if (msg instanceof LastHttpContent) {
            LOGGER.finest(() -> "(client reqID: " + requestId + ") Last http content received");
            if (channel.hasAttr(RETURN)) {
                channel.attr(RETURN).get().set(true);
                responseCloser.close();
                channel.read();
            } else {
                responseCloser.close();
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        // Connection closed without last HTTP content received. Some server problem
        // so we need to fail the publisher and report an exception.
        if (publisher != null && !responseCloser.isClosed()) {
            WebClientException exception = new WebClientException("Connection reset by the host");
            publisher.fail(exception);
        }
    }

    private boolean shouldResponseAutomaticallyClose(WebClientResponse clientResponse) {
        WebClientResponseHeaders headers = clientResponse.headers();
        if (clientResponse.status() == Http.Status.NO_CONTENT_204) {
            return true;
        }
        return headers.contentType().isEmpty()
                && noContentLength(headers)
                && notChunked(headers);
    }

    private boolean noContentLength(WebClientResponseHeaders headers) {
        return headers.contentLength()
                .map(value -> value == 0)
                .orElse(true);
    }

    private boolean notChunked(WebClientResponseHeaders headers) {
        return !headers.transferEncoding().contains("chunked");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        CompletableFuture responseFuture = ctx.channel().attr(RESULT).get();
        if (responseFuture.isDone()) {
            // we failed during entity processing, or during connecting to the remote site
            if (publisher != null) {
                publisher.fail(cause);
            }
        } else {
            // we failed before getting response
            responseFuture.completeExceptionally(cause);
        }
        ctx.close();
    }

    private Http.ResponseStatus helidonStatus(HttpResponseStatus nettyStatus) {
        return Http.ResponseStatus.create(nettyStatus.code(), nettyStatus.reasonPhrase());
    }

    private static final class HttpResponsePublisher extends BufferedEmittingPublisher {

        private final ReentrantReadWriteLock.WriteLock lock = new ReentrantReadWriteLock().writeLock();

        HttpResponsePublisher(ChannelHandlerContext ctx) {
            super.onRequest((n, cnt) -> {
                ctx.channel().config().setAutoRead(super.isUnbounded());

                try {
                    lock.lock();
                    if (super.hasRequests()) {
                        ctx.channel().read();
                    }
                } finally {
                    lock.unlock();
                }
            });
        }



        public void emit(final ByteBuf buf) {
            buf.retain();
            super.emit(DataChunk.create(false, true, buf::release,
                       buf.nioBuffer().asReadOnlyBuffer()));
        }
    }

    final class ResponseCloser {

        private final AtomicBoolean closed;
        private final ChannelHandlerContext ctx;
        private final CompletableFuture cf;

        ResponseCloser(ChannelHandlerContext ctx) {
            this.ctx = ctx;
            this.closed = new AtomicBoolean();
            this.cf = new CompletableFuture<>();
        }

        boolean isClosed() {
            return closed.get();
        }

        /**
         * Asynchronous close method.
         *
         * @return single of the closing process
         */
        Single close() {
            if (closed.compareAndSet(false, true)) {
                LOGGER.finest(() -> "(client reqID: " + requestId + ") Closing the response from the server");
                Channel channel = ctx.channel();
                WebClientServiceResponse clientServiceResponse = channel.attr(SERVICE_RESPONSE).get();
                CompletableFuture requestComplete = channel.attr(COMPLETED).get();
                requestComplete.complete(clientServiceResponse);
                if (channel.attr(WILL_CLOSE).get() || !channel.hasAttr(RETURN)) {
                    ctx.close()
                            .addListener(future -> {
                                if (future.isSuccess()) {
                                    LOGGER.finest(() -> "(client reqID: " + requestId + ") Response from the server has been "
                                            + "closed");
                                    cf.complete(null);
                                } else {
                                    LOGGER.log(Level.SEVERE,
                                               future.cause(),
                                               () -> "An exception occurred while closing the response");
                                    cf.completeExceptionally(future.cause());
                                }
                            });
                    publisher.complete();
                } else if (!channel.attr(RETURN).get().get()) {
                    LOGGER.finest(() -> "(client reqID: " + requestId + ") Drain possible remaining entity parts");
                    channel.read();
                }
            }
            return Single.create(cf, true);
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy