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

io.questdb.cutlass.http.HttpResponseSink Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2024 QuestDB
 *
 *  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.questdb.cutlass.http;

import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.network.Net;
import io.questdb.network.NetworkFacade;
import io.questdb.network.NoSpaceLeftInResponseBufferException;
import io.questdb.network.PeerDisconnectedException;
import io.questdb.network.PeerIsSlowToReadException;
import io.questdb.network.Socket;
import io.questdb.std.IntObjHashMap;
import io.questdb.std.MemoryTag;
import io.questdb.std.Misc;
import io.questdb.std.Mutable;
import io.questdb.std.Numbers;
import io.questdb.std.Unsafe;
import io.questdb.std.Vect;
import io.questdb.std.Zip;
import io.questdb.std.bytes.Bytes;
import io.questdb.std.datetime.millitime.DateFormatUtils;
import io.questdb.std.datetime.millitime.MillisecondClock;
import io.questdb.std.ex.ZLibException;
import io.questdb.std.str.StdoutSink;
import io.questdb.std.str.Utf8Sequence;
import io.questdb.std.str.Utf8Sink;
import io.questdb.std.str.Utf8s;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.Closeable;

import static io.questdb.cutlass.http.HttpConstants.*;
import static io.questdb.std.Chars.isBlank;
import static java.net.HttpURLConnection.*;

public class HttpResponseSink implements Closeable, Mutable {
    private static final int HTTP_RANGE_NOT_SATISFIABLE = 416;
    private static final int HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
    private static final Log LOG = LogFactory.getLog(HttpResponseSink.class);
    private static final IntObjHashMap httpStatusMap = new IntObjHashMap<>();
    private final ChunkUtf8Sink buffer;
    private final ChunkedResponseImpl chunkedResponse = new ChunkedResponseImpl();
    private final ChunkUtf8Sink compressOutBuffer;
    private final boolean connectionCloseHeader;
    private final boolean cookiesEnabled;
    private final boolean dumpNetworkTraffic;
    private final int forceSendFragmentationChunkSize;
    private final HttpResponseHeaderImpl headerImpl;
    private final String httpVersion;
    private final NetworkFacade nf;
    private final HttpRawSocketImpl rawSocket = new HttpRawSocketImpl();
    private final SimpleResponseImpl simpleResponse = new SimpleResponseImpl();
    private final ResponseSinkImpl sink = new ResponseSinkImpl();
    private boolean chunkedRequestDone;
    private boolean compressedHeaderDone;
    private boolean compressedOutputReady;
    private boolean compressionComplete;
    private int crc = 0;
    private boolean deflateBeforeSend = false;
    private boolean headersSent;
    private Socket socket;
    private long total = 0;
    private long totalBytesSent = 0;
    private long zStreamPtr = 0;

    public HttpResponseSink(HttpServerConfiguration configuration) {
        final int responseBufferSize = configuration.getSendBufferSize();
        this.nf = configuration.getNetworkFacade();
        this.buffer = new ChunkUtf8Sink(responseBufferSize);
        this.compressOutBuffer = new ChunkUtf8Sink(responseBufferSize);
        final HttpContextConfiguration contextConfiguration = configuration.getHttpContextConfiguration();
        this.headerImpl = new HttpResponseHeaderImpl(contextConfiguration.getMillisecondClock());
        this.dumpNetworkTraffic = contextConfiguration.getDumpNetworkTraffic();
        this.httpVersion = contextConfiguration.getHttpVersion();
        this.connectionCloseHeader = !contextConfiguration.getServerKeepAlive();
        this.cookiesEnabled = contextConfiguration.areCookiesEnabled();
        this.forceSendFragmentationChunkSize = contextConfiguration.getForceSendFragmentationChunkSize();
    }

    @Override
    public void clear() {
        headerImpl.clear();
        totalBytesSent = 0;
        headersSent = false;
        chunkedRequestDone = false;
        simpleResponse.clear();
        resetZip();
    }

    @Override
    public void close() {
        if (zStreamPtr != 0) {
            Zip.deflateEnd(zStreamPtr);
            zStreamPtr = 0;
            compressOutBuffer.close();
        }
        buffer.close();
        socket = null;
    }

    public HttpChunkedResponse getChunkedResponse() {
        return chunkedResponse;
    }

    public int getCode() {
        return headerImpl.getCode();
    }

    public void resumeSend() throws PeerDisconnectedException, PeerIsSlowToReadException {
        if (!headersSent || !deflateBeforeSend) {
            sendBuffer(buffer);
            return;
        }

        while (true) {
            if (!compressedOutputReady && !compressionComplete) {
                deflate();
            }

            if (compressedOutputReady) {
                sendBuffer(compressOutBuffer);
                compressedOutputReady = false;
                if (compressionComplete) {
                    break;
                }
            } else {
                break;
            }
        }
    }

    public void setDeflateBeforeSend(boolean deflateBeforeSend, long bufferSize) {
        this.deflateBeforeSend = deflateBeforeSend;
        if (zStreamPtr == 0 && deflateBeforeSend) {
            zStreamPtr = Zip.deflateInit();
            compressOutBuffer.reopen(bufferSize);
        }
    }

    public SimpleResponseImpl simpleResponse() {
        return simpleResponse;
    }

    private void deflate() {
        if (!compressedHeaderDone) {
            int len = Zip.gzipHeaderLen;
            Vect.memcpy(compressOutBuffer.getWriteAddress(len), Zip.gzipHeader, len);
            compressOutBuffer.onWrite(len);
            compressedHeaderDone = true;
        }

        int nInAvailable = (int) buffer.getReadNAvailable();
        if (nInAvailable > 0) {
            long inAddress = buffer.getReadAddress();
            LOG.debug().$("Zip.setInput [inAddress=").$(inAddress).$(", nInAvailable=").$(nInAvailable).I$();
            buffer.write64BitZeroPadding();
            Zip.setInput(zStreamPtr, inAddress, nInAvailable);
        }

        int ret;
        int len;
        // compress input until we run out of either input or output
        do {
            int sz = (int) compressOutBuffer.getWriteNAvailable() - 8;
            long p = compressOutBuffer.getWriteAddress(0);
            LOG.debug().$("deflate starting [p=").$(p).$(", sz=").$(sz).$(", chunkedRequestDone=").$(chunkedRequestDone).I$();
            ret = Zip.deflate(zStreamPtr, p, sz, chunkedRequestDone);
            len = sz - Zip.availOut(zStreamPtr);
            compressOutBuffer.onWrite(len);
            if (ret < 0) {
                // This is not an error, zlib just couldn't do any work with the input/output buffers it was provided.
                // This happens often (will depend on output buffer size) when there is no new input and zlib has finished generating
                // output from previously provided input
                if (ret != Zip.Z_BUF_ERROR || len != 0) {
                    throw HttpException.instance("could not deflate [ret=").put(ret);
                }
            }

            int availIn = Zip.availIn(zStreamPtr);
            int nInConsumed = nInAvailable - availIn;
            if (nInConsumed > 0) {
                this.crc = Zip.crc32(this.crc, buffer.getReadAddress(), nInConsumed);
                this.total += nInConsumed;
                buffer.onRead(nInConsumed);
                nInAvailable = availIn;
            }

            LOG.debug().$("deflate finished [ret=").$(ret).$(", len=").$(len).$(", availIn=").$(availIn).I$();
        } while (len == 0 && nInAvailable > 0);

        if (nInAvailable == 0) {
            buffer.clearAndPrepareToWriteToBuffer();
        }

        if (len == 0) {
            compressedOutputReady = false;
            return;
        }
        compressedOutputReady = true;

        // this is ZLib error, can't continue
        if (len < 0) {
            throw ZLibException.INSTANCE;
        }

        // trailer
        boolean finished = chunkedRequestDone && ret == Zip.Z_STREAM_END;
        if (finished) {
            long p = compressOutBuffer.getWriteAddress(0);
            Unsafe.getUnsafe().putInt(p, crc); // crc
            Unsafe.getUnsafe().putInt(p + 4, (int) total); // total
            compressOutBuffer.onWrite(8);
            compressionComplete = true;
        }
        compressOutBuffer.prepareToReadFromBuffer(true, finished);
    }

    private void dumpBuffer(long buffer, int size) {
        if (dumpNetworkTraffic && size > 0) {
            StdoutSink.INSTANCE.put('<');
            Net.dump(buffer, size);
        }
    }

    private void flushSingle() throws PeerDisconnectedException, PeerIsSlowToReadException {
        sendBuffer(buffer);
    }

    private long getFd() {
        return socket != null ? socket.getFd() : -1;
    }

    private void prepareHeaderSink() {
        buffer.prepareToReadFromBuffer(false, false);
        headerImpl.prepareToSend();
    }

    private void resetZip() {
        if (zStreamPtr != 0) {
            Zip.deflateReset(zStreamPtr);
            compressOutBuffer.clear();
            crc = 0;
            total = 0;
            compressedHeaderDone = false;
            compressedOutputReady = false;
            compressionComplete = false;
        }
    }

    private void sendBuffer(ChunkUtf8Sink sendBuf) throws PeerDisconnectedException, PeerIsSlowToReadException {
        int available = (int) sendBuf.getReadNAvailable();
        int nSend = Math.min(forceSendFragmentationChunkSize, available);
        while (nSend > 0) {
            int n = socket.send(sendBuf.getReadAddress(), nSend);
            if (n < 0) {
                // disconnected
                LOG.error()
                        .$("disconnected [errno=").$(nf.errno())
                        .$(", fd=").$(socket.getFd())
                        .I$();
                throw PeerDisconnectedException.INSTANCE;
            }
            if (n == 0) {
                // test how many times we tried to send before parking up
                throw PeerIsSlowToReadException.INSTANCE;
            } else if (available <= forceSendFragmentationChunkSize) {
                dumpBuffer(sendBuf.getReadAddress(), n);
                sendBuf.onRead(n);
                nSend -= n;
                totalBytesSent += n;
            } else {
                // This branch is for tests only
                sendBuf.onRead(n);
                throw PeerIsSlowToReadException.INSTANCE;
            }
        }
        assert sendBuf.getReadNAvailable() == 0;
        sendBuf.clearAndPrepareToWriteToBuffer();
    }

    HttpResponseHeader getHeader() {
        return headerImpl;
    }

    HttpRawSocket getRawSocket() {
        return rawSocket;
    }

    long getTotalBytesSent() {
        return totalBytesSent;
    }

    void of(Socket socket, long bufferSize) {
        this.socket = socket;
        if (socket != null) {
            buffer.reopen(bufferSize);
        }
    }

    void open(long bufferSize) {
        buffer.reopen(bufferSize);
    }

    private class ChunkUtf8Sink implements Utf8Sink, Closeable, Mutable {
        private static final String EOF_CHUNK = "\r\n00\r\n\r\n";
        private static final int MAX_CHUNK_HEADER_SIZE = 12;
        private long _rptr;
        private long _wptr;
        private long bufSize;
        private long bufStart;
        private long bufStartOfData;

        private ChunkUtf8Sink(int bufSize) {
            this.bufSize = bufSize;
        }

        @Override
        public void clear() {
            _wptr = _rptr = bufStartOfData;
        }

        @Override
        public void close() {
            if (bufStart != 0) {
                Unsafe.free(bufStart, bufSize + MAX_CHUNK_HEADER_SIZE + EOF_CHUNK.length(), MemoryTag.NATIVE_HTTP_CONN);
                bufStart = bufStartOfData = _wptr = _rptr = 0;
            }
        }

        @Override
        public Utf8Sink put(byte b) {
            Unsafe.getUnsafe().putByte(getWriteAddress(1), b);
            onWrite(1);
            return this;
        }

        @Override
        public Utf8Sink put(@Nullable Utf8Sequence us) {
            if (us != null) {
                int size = us.size();
                Utf8s.strCpy(us, size, getWriteAddress(size));
                onWrite(size);
            }
            return this;
        }

        @Override
        public Utf8Sink putNonAscii(long lo, long hi) {
            final int size = Bytes.checkedLoHiSize(lo, hi, 0);
            final long dest = getWriteAddress(size);
            Vect.memcpy(dest, lo, size);
            onWrite(size);
            return this;
        }

        public void reopen(long bufSize) {
            if (bufStart == 0) {
                this.bufSize = bufSize;
                bufStart = Unsafe.malloc(bufSize + MAX_CHUNK_HEADER_SIZE + EOF_CHUNK.length(), MemoryTag.NATIVE_HTTP_CONN);
                bufStartOfData = bufStart + MAX_CHUNK_HEADER_SIZE;
                clear();
            }
        }

        void clearAndPrepareToWriteToBuffer() {
            _rptr = _wptr = bufStartOfData;
        }

        long getReadAddress() {
            assert _rptr != 0;
            return _rptr;
        }

        long getReadNAvailable() {
            return _wptr - _rptr;
        }

        long getWriteAddress(long size) {
            assert _wptr != 0;
            if (getWriteNAvailable() >= size) {
                return _wptr;
            }
            throw NoSpaceLeftInResponseBufferException.instance(size);
        }

        long getWriteNAvailable() {
            return bufStartOfData + bufSize - _wptr;
        }

        void onRead(int nRead) {
            assert nRead >= 0 && nRead <= getReadNAvailable();
            _rptr += nRead;
        }

        void onWrite(int nWrite) {
            assert nWrite >= 0 && nWrite <= getWriteNAvailable();
            _wptr += nWrite;
        }

        void prepareToReadFromBuffer(boolean addChunkHeader, boolean addEofChunk) {
            if (addChunkHeader) {
                int len = (int) (_wptr - bufStartOfData);
                int padding = len == 0 ? 6 : (Integer.numberOfLeadingZeros(len) >> 3) << 1;
                long tmp = _wptr;
                _rptr = _wptr = bufStart + padding;
                putEOL();
                Numbers.appendHex(this, len);
                putEOL();
                _wptr = tmp;
            }
            if (addEofChunk) {
                int len = EOF_CHUNK.length();
                Utf8s.strCpyAscii(EOF_CHUNK, len, _wptr);
                _wptr += len;
                LOG.debug().$("end chunk sent [fd=").$(getFd()).I$();
            }
        }

        void write64BitZeroPadding() {
            Unsafe.getUnsafe().putLong(bufStartOfData - 8, 0);
            Unsafe.getUnsafe().putLong(_wptr, 0);
        }
    }

    private class ChunkedResponseImpl extends ResponseSinkImpl implements HttpChunkedResponse {
        private long bookmark = 0L;

        @Override
        public void bookmark() {
            bookmark = buffer._wptr;
        }

        @Override
        public void done() throws PeerDisconnectedException, PeerIsSlowToReadException {
            if (!chunkedRequestDone) {
                sendChunk(true);
            }
        }

        @Override
        public HttpResponseHeader headers() {
            return headerImpl;
        }

        @Override
        public boolean resetToBookmark() {
            if (bookmark != 0) {
                buffer._wptr = bookmark;
                return bookmark != buffer.bufStartOfData;
            }
            return false;
        }

        @Override
        public void sendChunk(boolean done) throws PeerDisconnectedException, PeerIsSlowToReadException {
            headersSent = true;
            chunkedRequestDone = done;
            if (buffer.getReadNAvailable() > 0 || done) {
                if (!deflateBeforeSend) {
                    buffer.prepareToReadFromBuffer(true, chunkedRequestDone);
                }
                resumeSend();
            }
        }

        @Override
        public void sendHeader() throws PeerDisconnectedException, PeerIsSlowToReadException {
            chunkedRequestDone = false;
            prepareHeaderSink();
            flushSingle();
            buffer.clearAndPrepareToWriteToBuffer();
        }

        @Override
        public void shutdownWrite() {
            socket.shutdown(Net.SHUT_WR);
        }

        @Override
        public void status(int status, CharSequence contentType) {
            super.status(status, contentType);
            if (deflateBeforeSend) {
                headerImpl.putAscii("Content-Encoding: gzip").putEOL();
            }
        }

        /**
         * Variant of `put(long lo, long hi)` that writes up to the available space in the buffer.
         * If there isn't enough space to write the whole length, the written length is returned.
         */
        @Override
        public int writeBytes(long srcAddr, int len) {
            assert len > 0;
            len = (int) Math.min(len, buffer.getWriteNAvailable());
            putNonAscii(srcAddr, srcAddr + len);
            return len;
        }
    }

    public class HttpRawSocketImpl implements HttpRawSocket {

        @Override
        public long getBufferAddress() {
            return buffer.getWriteAddress(1);
        }

        @Override
        public int getBufferSize() {
            return (int) buffer.getWriteNAvailable();
        }

        @Override
        public void send(int size) throws PeerDisconnectedException, PeerIsSlowToReadException {
            buffer.onWrite(size);
            buffer.prepareToReadFromBuffer(false, false);
            flushSingle();
            buffer.clearAndPrepareToWriteToBuffer();
        }
    }

    public class HttpResponseHeaderImpl implements Utf8Sink, HttpResponseHeader, Mutable {
        private final MillisecondClock clock;
        private boolean chunked;
        private int code;

        public HttpResponseHeaderImpl(MillisecondClock clock) {
            this.clock = clock;
        }

        @Override
        public void clear() {
            buffer.clearAndPrepareToWriteToBuffer();
            chunked = false;
        }

        // this is used for HTTP access logging
        public int getCode() {
            return code;
        }

        public boolean isChunked() {
            return chunked;
        }

        @Override
        public Utf8Sink put(byte b) {
            Unsafe.getUnsafe().putByte(buffer.getWriteAddress(1), b);
            buffer.onWrite(1);
            return this;
        }

        @Override
        public Utf8Sink put(@Nullable Utf8Sequence us) {
            if (us != null) {
                int size = us.size();
                Utf8s.strCpy(us, size, buffer.getWriteAddress(size));
                buffer.onWrite(size);
            }
            return this;
        }

        @Override
        public Utf8Sink putNonAscii(long lo, long hi) {
            buffer.putNonAscii(lo, hi);
            return this;
        }

        @Override
        public void send() throws PeerDisconnectedException, PeerIsSlowToReadException {
            headerImpl.prepareToSend();
            flushSingle();
        }

        @Override
        public void setCookie(CharSequence name, CharSequence value) {
            if (cookiesEnabled) {
                put(HEADER_SET_COOKIE).putAscii(": ").put(name).putAscii(COOKIE_VALUE_SEPARATOR).put(value).putEOL();
            }
        }

        @Override
        public String status(CharSequence httpProtocolVersion, int code, CharSequence contentType, long contentLength) {
            this.code = code;
            String status = httpStatusMap.get(code);
            if (status == null) {
                throw new IllegalArgumentException("Illegal status code: " + code);
            }
            buffer.clearAndPrepareToWriteToBuffer();
            putAscii(httpProtocolVersion).put(code).put(' ').putAscii(status).putEOL();
            putAscii("Server: ").putAscii("questDB/1.0").putEOL();
            putAscii("Date: ");
            DateFormatUtils.formatHTTP(this, clock.getTicks());
            putEOL();
            if (contentLength > -2) {
                chunked = (contentLength == -1);
                if (chunked) {
                    putAscii("Transfer-Encoding: chunked").putEOL();
                } else {
                    putAscii("Content-Length: ").put(contentLength).putEOL();
                }
                putAscii("Content-Type: ").put(contentType).putEOL();
            }

            if (connectionCloseHeader) {
                putAscii("Connection: close").putEOL();
            }

            return status;
        }

        private void prepareToSend() {
            if (!chunked) {
                putEOL();
            }
        }
    }

    private class ResponseSinkImpl implements Utf8Sink {

        @Override
        public Utf8Sink put(@Nullable Utf8Sequence us) {
            buffer.put(us);
            return this;
        }

        @Override
        public Utf8Sink put(byte b) {
            buffer.put(b);
            return this;
        }

        @Override
        public Utf8Sink put(double value) {
            if (Numbers.isNull(value)) {
                putAscii("null");
                return this;
            }
            return Utf8Sink.super.put(value);
        }

        @Override
        public Utf8Sink put(float value) {
            if (Numbers.isNull(value)) {
                putAscii("null");
                return this;
            }
            return Utf8Sink.super.put(value);
        }

        @Override
        public Utf8Sink putNonAscii(long lo, long hi) {
            buffer.putNonAscii(lo, hi);
            return this;
        }

        public void status(int status, CharSequence contentType) {
            buffer.clearAndPrepareToWriteToBuffer();
            headerImpl.status("HTTP/1.1 ", status, contentType, -1);
        }
    }

    public class SimpleResponseImpl {
        private boolean contentSent = false;
        private boolean headerSent = false;

        public void clear() {
            contentSent = false;
            headerSent = false;
        }

        @SuppressWarnings("unused")
        public void sendStatusJsonContent(
                int code
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusJsonContent(code, null, null, null, null, true);
        }

        public void sendStatusJsonContent(
                int code,
                @Nullable CharSequence message
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusJsonContent(code, message, null, null, null, true);
        }

        public void sendStatusJsonContent(
                int code,
                @Nullable CharSequence message,
                boolean appendEOL
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusJsonContent(code, message, null, null, null, appendEOL);
        }

        public void sendStatusJsonContent(
                int code,
                @Nullable CharSequence message,
                @Nullable CharSequence header,
                @Nullable CharSequence cookieName,
                @Nullable CharSequence cookieValue,
                boolean appendEOL
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusWithContent(CONTENT_TYPE_JSON, code, message, header, cookieName, cookieValue, message != null ? message.length() : -1, appendEOL);
        }

        public void sendStatusNoContent(int code, @Nullable CharSequence header) throws PeerDisconnectedException, PeerIsSlowToReadException {
            if (!headerSent) {
                buffer.clearAndPrepareToWriteToBuffer();
                headerImpl.status(httpVersion, code, null, -2L);
                if (header != null) {
                    headerImpl.put(header).put(Misc.EOL);
                }
                prepareHeaderSink();
                headerSent = true;
            }
            flushSingle();
        }

        public void sendStatusNoContent(int code, @NotNull Utf8Sequence header) throws PeerDisconnectedException, PeerIsSlowToReadException {
            if (!headerSent) {
                buffer.clearAndPrepareToWriteToBuffer();
                headerImpl.status(httpVersion, code, null, -2L);
                headerImpl.put(header).put(Misc.EOL);
                prepareHeaderSink();
                headerSent = true;
            }
            flushSingle();
        }

        public void sendStatusNoContent(int code) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusNoContent(code, (CharSequence) null);
        }

        /**
         * Sends "text/plain" content type response with customised message and
         * optional additional header and cookie.
         *
         * @param code        response code, has to be compatible with "text" response type
         * @param message     optional message, if not provided, a standard message for the response code will be used
         * @param header      optional header
         * @param cookieName  optional cookie name, when name is not null the value must be not-null too
         * @param cookieValue optional cookie value
         * @throws PeerDisconnectedException exception if HTTP client disconnects during us sending
         * @throws PeerIsSlowToReadException exception if HTTP client does not keep up with us sending
         */
        public void sendStatusTextContent(
                int code,
                @Nullable CharSequence message,
                @Nullable CharSequence header,
                @Nullable CharSequence cookieName,
                @Nullable CharSequence cookieValue
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusWithContent(CONTENT_TYPE_TEXT, code, message, header, cookieName, cookieValue, -1, true);
        }

        public void sendStatusTextContent(
                int code,
                CharSequence message,
                CharSequence header
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusTextContent(code, message, header, null, null);
        }

        public void sendStatusTextContent(int code) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusTextContent(code, null, null);
        }

        public void sendStatusTextContent(int code, CharSequence header) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusTextContent(code, null, header);
        }

        public void sendStatusWithCookie(int code, CharSequence message, CharSequence cookieName, CharSequence cookieValue) throws PeerDisconnectedException, PeerIsSlowToReadException {
            sendStatusTextContent(code, message, null, cookieName, cookieValue);
        }

        public void shutdownWrite() {
            socket.shutdown(Net.SHUT_WR);
        }

        private void sendStatusWithContent(
                String contentType,
                int code,
                @Nullable CharSequence message,
                @Nullable CharSequence header,
                @Nullable CharSequence cookieName,
                @Nullable CharSequence cookieValue,
                long contentLength,
                boolean appendEOL
        ) throws PeerDisconnectedException, PeerIsSlowToReadException {
            if (!headerSent) {
                buffer.clearAndPrepareToWriteToBuffer();
                headerImpl.status(httpVersion, code, contentType, contentLength);
                if (header != null) {
                    headerImpl.put(header).put(Misc.EOL);
                }
                if (cookieName != null) {
                    setCookie(cookieName, cookieValue);
                }
                prepareHeaderSink();
                headerSent = true;
            }

            if (!contentSent) {
                flushSingle();
                buffer.clearAndPrepareToWriteToBuffer();
                sink.put(message == null ? httpStatusMap.get(code) : message);
                if (appendEOL) {
                    sink.putEOL();
                }
                final boolean chunked = headerImpl.isChunked();
                buffer.prepareToReadFromBuffer(chunked, chunked);
                contentSent = true;
            }
            resumeSend();
        }

        private void setCookie(CharSequence name, CharSequence value) {
            if (cookiesEnabled) {
                headerImpl.put(HEADER_SET_COOKIE).putAscii(": ").put(name).putAscii(COOKIE_VALUE_SEPARATOR).put(!isBlank(value) ? value : "").putEOL();
            }
        }
    }

    static {
        httpStatusMap.put(HTTP_OK, "OK");
        httpStatusMap.put(HTTP_NO_CONTENT, "OK");
        httpStatusMap.put(HTTP_PARTIAL, "Partial content");
        httpStatusMap.put(HTTP_MOVED_PERM, "Moved Permanently");
        httpStatusMap.put(HTTP_MOVED_TEMP, "Temporarily Moved");
        httpStatusMap.put(HTTP_NOT_MODIFIED, "Not Modified");
        httpStatusMap.put(HTTP_BAD_REQUEST, "Bad request");
        httpStatusMap.put(HTTP_UNAUTHORIZED, "Unauthorized");
        httpStatusMap.put(HTTP_FORBIDDEN, "Forbidden");
        httpStatusMap.put(HTTP_NOT_FOUND, "Not Found");
        httpStatusMap.put(HTTP_BAD_METHOD, "Method Not Allowed");
        httpStatusMap.put(HTTP_CONFLICT, "Conflict");
        httpStatusMap.put(HTTP_CLIENT_TIMEOUT, "Request Timeout");
        httpStatusMap.put(HTTP_LENGTH_REQUIRED, "Length Required");
        httpStatusMap.put(HTTP_ENTITY_TOO_LARGE, "Content Too Large");
        httpStatusMap.put(HTTP_UNSUPPORTED_TYPE, "Bad request");
        httpStatusMap.put(HTTP_RANGE_NOT_SATISFIABLE, "Request range not satisfiable");
        httpStatusMap.put(HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE, "Headers too large");
        httpStatusMap.put(HTTP_INTERNAL_ERROR, "Internal server error");
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy