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

io.helidon.webclient.http1.Http1CallOutputStreamChain Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023, 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.http1;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

import io.helidon.common.buffers.BufferData;
import io.helidon.common.buffers.Bytes;
import io.helidon.common.buffers.DataReader;
import io.helidon.common.buffers.DataWriter;
import io.helidon.common.socket.SocketContext;
import io.helidon.common.uri.UriInfo;
import io.helidon.http.ClientRequestHeaders;
import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Headers;
import io.helidon.http.Http1HeadersParser;
import io.helidon.http.Method;
import io.helidon.http.Status;
import io.helidon.http.WritableHeaders;
import io.helidon.webclient.api.ClientConnection;
import io.helidon.webclient.api.ClientRequest;
import io.helidon.webclient.api.ClientUri;
import io.helidon.webclient.api.HttpClientConfig;
import io.helidon.webclient.api.WebClientServiceRequest;
import io.helidon.webclient.api.WebClientServiceResponse;

import static java.lang.System.Logger.Level.TRACE;

class Http1CallOutputStreamChain extends Http1CallChainBase {
    private static final System.Logger LOGGER = System.getLogger(Http1CallOutputStreamChain.class.getName());
    private final Http1ClientImpl http1Client;
    private final CompletableFuture whenSent;
    private final ClientRequest.OutputStreamHandler osHandler;

    Http1CallOutputStreamChain(Http1ClientImpl http1Client,
                               Http1ClientRequestImpl clientRequest,
                               CompletableFuture whenSent,
                               CompletableFuture whenComplete,
                               ClientRequest.OutputStreamHandler osHandler) {
        super(http1Client, clientRequest, whenComplete);
        this.http1Client = http1Client;
        this.whenSent = whenSent;
        this.osHandler = osHandler;
    }

    @Override
    WebClientServiceResponse doProceed(ClientConnection connection,
                                       WebClientServiceRequest serviceRequest,
                                       ClientRequestHeaders headers,
                                       DataWriter writer,
                                       DataReader reader,
                                       BufferData writeBuffer) {

        ClientConnectionOutputStream cos = new ClientConnectionOutputStream(connection,
                                                                            writer,
                                                                            reader,
                                                                            writeBuffer,
                                                                            headers,
                                                                            http1Client.clientConfig(),
                                                                            http1Client.protocolConfig(),
                                                                            serviceRequest,
                                                                            originalRequest(),
                                                                            whenSent,
                                                                            whenComplete());

        boolean interrupted = false;
        try {
            osHandler.handle(cos);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (OutputStreamInterruptedException e) {
            interrupted = true;
        }

        if (interrupted || cos.interrupted()) {
            //If cos is marked as interrupted, we know that our interrupted exception has been thrown, but
            //it was intercepted by the user OutputStreamHandler and not rethrown.
            //This is a fallback mechanism to correctly handle such a situations.
            return cos.serviceResponse();
        } else if (!cos.closed()) {
            throw new IllegalStateException("Output stream was not closed in handler");
        }

        reader = cos.reader;
        connection = cos.connection;

        Status responseStatus;
        try {
            responseStatus = Http1StatusParser.readStatus(reader, http1Client.protocolConfig().maxStatusLineLength());
            if (responseStatus == Status.CONTINUE_100) {
                // skip the next empty end of line
                readHeaders(reader);
                responseStatus = Http1StatusParser.readStatus(reader, http1Client.protocolConfig().maxStatusLineLength());
            }
        } catch (UncheckedIOException e) {
            // if we get a timeout or connection close, we must close the resource (as otherwise we may receive
            // data of this request on the next use of this connection
            try {
                connection.closeResource();
            } catch (Exception ex) {
                e.addSuppressed(ex);
            }
            throw e;
        }
        connection.helidonSocket().log(LOGGER, TRACE, "client received status %n%s", responseStatus);
        ClientResponseHeaders responseHeaders = readHeaders(reader);
        connection.helidonSocket().log(LOGGER, TRACE, "client received headers %n%s", responseHeaders);

        if (originalRequest().followRedirects()
                && RedirectionProcessor.redirectionStatusCode(responseStatus)) {
            checkRedirectHeaders(responseHeaders);
            URI newUri = URI.create(responseHeaders.get(HeaderNames.LOCATION).get());
            ClientUri redirectUri = ClientUri.create(newUri);
            if (newUri.getHost() == null) {
                UriInfo resolvedUri = cos.lastRequest.resolvedUri();
                redirectUri.scheme(resolvedUri.scheme());
                redirectUri.host(resolvedUri.host());
                redirectUri.port(resolvedUri.port());
            }
            Http1ClientRequestImpl request = new Http1ClientRequestImpl(cos.lastRequest,
                                                                        Method.GET,
                                                                        redirectUri,
                                                                        cos.lastRequest.properties());
            Http1ClientResponseImpl clientResponse = RedirectionProcessor.invokeWithFollowRedirects(request,
                                                                                                    1,
                                                                                                    BufferData.EMPTY_BYTES);
            return createServiceResponse(clientConfig(),
                                         serviceRequest,
                                         clientResponse.connection(),
                                         clientResponse.connection().reader(),
                                         clientResponse.status(),
                                         clientResponse.headers(),
                                         whenComplete());
        }

        return createServiceResponse(clientConfig(),
                                     serviceRequest,
                                     connection,
                                     reader,
                                     responseStatus,
                                     responseHeaders,
                                     whenComplete());
    }

    private static void checkRedirectHeaders(Headers headerValues) {
        if (!headerValues.contains(HeaderNames.LOCATION)) {
            throw new IllegalStateException("There is no " + HeaderNames.LOCATION + " header present in the"
                                                    + " response! "
                                                    + "It is not clear where to redirect.");
        }
    }

    private static class ClientConnectionOutputStream extends OutputStream {
        private static final byte[] TERMINATING_CHUNK = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);

        private final WebClientServiceRequest request;
        private final Http1ClientRequestImpl originalRequest;
        private final CompletableFuture whenSent;
        private final CompletableFuture whenComplete;
        private final HttpClientConfig clientConfig;
        private final Http1ClientProtocolConfig protocolConfig;
        private final WritableHeaders headers;
        private final BufferData prologue;

        private boolean chunked;
        private BufferData firstPacket;
        private long bytesWritten;
        private long contentLength;
        private boolean noData = true;
        private boolean closed;
        private boolean interrupted;
        private ClientConnection connection;
        private SocketContext ctx;
        private DataWriter writer;
        private DataReader reader;
        private Http1ClientRequestImpl lastRequest;
        private Http1ClientResponseImpl response;
        private WebClientServiceResponse serviceResponse;

        private ClientConnectionOutputStream(ClientConnection connection,
                                             DataWriter writer,
                                             DataReader reader,
                                             BufferData prologue,
                                             WritableHeaders headers,
                                             HttpClientConfig clientConfig,
                                             Http1ClientProtocolConfig protocolConfig,
                                             WebClientServiceRequest request,
                                             Http1ClientRequestImpl originalRequest,
                                             CompletableFuture whenSent,
                                             CompletableFuture whenComplete) {
            this.connection = connection;
            this.ctx = connection.helidonSocket();
            this.writer = writer;
            this.reader = reader;
            this.headers = headers;
            this.prologue = prologue;
            this.clientConfig = clientConfig;
            this.protocolConfig = protocolConfig;
            this.contentLength = headers.contentLength().orElse(-1);
            this.chunked = contentLength == -1 || headers.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED);
            this.request = request;
            this.originalRequest = originalRequest;
            this.lastRequest = originalRequest;
            this.whenSent = whenSent;
            this.whenComplete = whenComplete;
        }

        @Override
        public void write(int b) throws IOException {
            // this method should not be called, as we are wrapped with a buffered stream
            byte[] data = {(byte) b};
            write(data, 0, 1);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (interrupted) {
                //If this OS was interrupted, it becomes NOOP.
                return;
            } else if (closed) {
                throw new IOException("Output stream already closed");
            }

            BufferData data = BufferData.create(b, off, len);

            if (!chunked) {
                if (firstPacket == null) {
                    firstPacket = data;
                } else {
                    chunked = true;
                    sendFirstChunk();
                }
                noData = false;
            }

            if (chunked) {
                if (noData) {
                    noData = false;
                    sendPrologueAndHeader();
                }
                writeChunked(data);
            }
        }

        @Override
        public void close() throws IOException {
            if (closed || interrupted) {
                return;
            }
            this.closed = true;
            if (chunked) {
                if (firstPacket != null) {
                    sendFirstChunk();
                } else if (noData) {
                    sendPrologueAndHeader();
                }
                BufferData terminating = BufferData.create(TERMINATING_CHUNK);
                if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
                    ctx.log(LOGGER, System.Logger.Level.TRACE, "send data%n%s", terminating.debugDataHex());
                }
                writer.write(terminating);
            } else {
                headers.remove(HeaderNames.TRANSFER_ENCODING);
                if (noData) {
                    headers.set(HeaderValues.CONTENT_LENGTH_ZERO);
                    contentLength = 0;
                }
                if (noData || firstPacket != null) {
                    sendPrologueAndHeader();
                }
                if (firstPacket != null) {
                    writeContent(firstPacket);
                }
            }
            super.close();
        }

        WebClientServiceResponse serviceResponse() {
            if (serviceResponse != null) {
                return serviceResponse;
            }

            return createServiceResponse(clientConfig,
                                         request,
                                         response.connection(),
                                         response.connection().reader(),
                                         response.status(),
                                         response.headers(),
                                         whenComplete);
        }

        boolean closed() {
            return closed;
        }

        boolean interrupted() {
            return interrupted;
        }

        Http1ClientResponseImpl response() {
            return response;
        }

        private void writeChunked(BufferData buffer) {
            int available = buffer.available();
            byte[] hex = Integer.toHexString(available).getBytes(StandardCharsets.UTF_8);

            BufferData toWrite = BufferData.create(available + hex.length + 4); // \r\n after size, another after chunk
            toWrite.write(hex);
            toWrite.write(Bytes.CR_BYTE);
            toWrite.write(Bytes.LF_BYTE);
            toWrite.write(buffer);
            toWrite.write(Bytes.CR_BYTE);
            toWrite.write(Bytes.LF_BYTE);

            if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
                ctx.log(LOGGER, System.Logger.Level.TRACE, "send data:%n%s", toWrite.debugDataHex());
            }
            writer.writeNow(toWrite);
        }

        private void writeContent(BufferData buffer) throws IOException {
            bytesWritten += buffer.available();
            if (contentLength != -1 && bytesWritten > contentLength) {
                throw new IOException("Content length was set to " + contentLength
                                              + ", but you are writing additional " + (bytesWritten - contentLength) + " "
                                              + "bytes");
            }
            if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
                ctx.log(LOGGER, System.Logger.Level.TRACE, "send data:%n%s", buffer.debugDataHex());
            }
            writer.writeNow(buffer);
        }

        private void sendPrologueAndHeader() {
            boolean expects100Continue = clientConfig.sendExpectContinue() && !noData;
            if (expects100Continue) {
                headers.add(HeaderValues.EXPECT_100);
            }

            if (chunked) {
                // Add chunked encoding, if there is no other transfer-encoding headers
                if (!headers.contains(HeaderNames.TRANSFER_ENCODING)) {
                    headers.set(HeaderValues.TRANSFER_ENCODING_CHUNKED);
                } else {
                    // Add chunked encoding, if it's not part of existing transfer-encoding headers
                    if (!headers.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED)) {
                        headers.add(HeaderValues.TRANSFER_ENCODING_CHUNKED);
                    }
                }
                headers.remove(HeaderNames.CONTENT_LENGTH);
            }

            if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
                ctx.log(LOGGER, System.Logger.Level.TRACE, "send prologue: %n%s", prologue.debugDataHex());
            }
            writer.writeNow(prologue);

            BufferData headerBuffer = BufferData.growing(128);
            if (LOGGER.isLoggable(System.Logger.Level.TRACE)) {
                ctx.log(LOGGER, System.Logger.Level.TRACE, "send headers:%n%s", headers);
            }
            writeHeaders(headers, headerBuffer, protocolConfig.validateRequestHeaders());
            writer.writeNow(headerBuffer);

            whenSent.complete(request);

            if (expects100Continue) {
                Status responseStatus;

                try {
                    connection.readTimeout(originalRequest.readContinueTimeout());
                    responseStatus = Http1StatusParser.readStatus(reader, protocolConfig.maxStatusLineLength());
                    connection.helidonSocket().log(LOGGER, TRACE, "recv status: %n%s", responseStatus);
                } catch (UncheckedIOException ignored) {
                    // we assume this is a timeout exception, if the socket got closed, next read will throw appropriate exception
                    // we treat this as receiving 100-Continue
                    responseStatus = null;
                } finally {
                    connection.readTimeout(originalRequest.readTimeout());
                }
                if (responseStatus == Status.CONTINUE_100) {
                    // there is the status and (usually) empty headers. We ignore such headers
                    Http1HeadersParser.readHeaders(reader,
                                                   protocolConfig.maxHeaderSize(),
                                                   protocolConfig.validateResponseHeaders());
                }
                if (responseStatus == null) {
                    responseStatus = Status.CONTINUE_100;
                }

                if (responseStatus != Status.CONTINUE_100) {
                    WritableHeaders responseHeaders = Http1HeadersParser.readHeaders(reader,
                                                                                        protocolConfig.maxHeaderSize(),
                                                                                        protocolConfig.validateResponseHeaders());
                    connection.helidonSocket().log(LOGGER, TRACE, "client received headers %n%s", responseHeaders);

                    if (RedirectionProcessor.redirectionStatusCode(responseStatus) && originalRequest.followRedirects()) {
                        // redirect as needed
                        // Discard any remaining data from the response
                        reader.skip(reader.available());
                        checkRedirectHeaders(responseHeaders);
                        redirect(responseStatus, responseHeaders);
                    } else {
                        //OS changed its state to interrupted, that means other usage of this OS will result in NOOP actions.
                        this.interrupted = true;
                        this.serviceResponse = createServiceResponse(clientConfig,
                                                                     request,
                                                                     connection,
                                                                     reader,
                                                                     responseStatus,
                                                                     ClientResponseHeaders.create(responseHeaders),
                                                                     whenComplete);
                        //we are not sending anything by this OS, we need to interrupt it.
                        throw new OutputStreamInterruptedException();
                    }
                }
            }
        }

        private void redirect(Status lastStatus, WritableHeaders headerValues) {
            String redirectedUri = headerValues.get(HeaderNames.LOCATION).get();
            ClientUri lastUri = originalRequest.uri();
            Method method;
            boolean sendEntity;
            if (lastStatus == Status.TEMPORARY_REDIRECT_307
                    || lastStatus == Status.PERMANENT_REDIRECT_308) {
                method = originalRequest.method();
                sendEntity = true;
            } else {
                method = Method.GET;
                sendEntity = false;
            }
            for (int i = 0; i < clientConfig.maxRedirects(); i++) {
                URI newUri = URI.create(redirectedUri);
                ClientUri redirectUri = ClientUri.create(newUri);
                if (newUri.getHost() == null) {
                    redirectUri.scheme(lastUri.scheme());
                    redirectUri.host(lastUri.host());
                    redirectUri.port(lastUri.port());
                }
                lastUri = redirectUri;
                connection.releaseResource();
                Http1ClientRequestImpl clientRequest = new Http1ClientRequestImpl(originalRequest,
                                                                                  method,
                                                                                  redirectUri,
                                                                                  request.properties());
                Http1ClientResponseImpl response;
                if (sendEntity) {
                    response = (Http1ClientResponseImpl) clientRequest
                            .header(HeaderValues.EXPECT_100)
                            .header(HeaderValues.TRANSFER_ENCODING_CHUNKED)
                            .readTimeout(originalRequest.readContinueTimeout())
                            .request();
                    response.connection().readTimeout(originalRequest.readTimeout());
                } else {
                    response = (Http1ClientResponseImpl) clientRequest.request();
                }
                lastRequest = clientRequest;

                connection = response.connection();
                ctx = connection.helidonSocket();
                reader = connection.reader();
                writer = connection.writer();

                if (RedirectionProcessor.redirectionStatusCode(response.status())) {
                    try (response) {
                        checkRedirectHeaders(response.headers());
                        if (response.status() != Status.TEMPORARY_REDIRECT_307
                                && response.status() != Status.PERMANENT_REDIRECT_308) {
                            method = Method.GET;
                            sendEntity = false;
                        }
                        redirectedUri = response.headers().get(HeaderNames.LOCATION).get();
                    }
                } else {
                    if (!sendEntity) {
                        //OS changed its state to interrupted, that means other usage of this OS will result in NOOP actions.
                        this.interrupted = true;
                        this.response = response;
                        //we are not sending anything by this OS, we need to interrupt it.
                        throw new OutputStreamInterruptedException();
                    } else {
                        reader.skip(reader.available());
                    }
                    return;
                }

            }
            throw new IllegalStateException("Maximum number of request redirections ("
                                                    + clientConfig.maxRedirects() + ") reached.");
        }

        private void sendFirstChunk() {
            sendPrologueAndHeader();
            writeChunked(firstPacket);
            firstPacket = null;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy