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

co.cask.http.internal.BasicHttpResponder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2017 Cask Data, Inc.
 *
 * 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 co.cask.http.internal;

import co.cask.http.AbstractHttpResponder;
import co.cask.http.BodyProducer;
import co.cask.http.ChunkResponder;
import co.cask.http.HttpResponder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.FileRegion;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;

/**
 * Basic implementation of {@link HttpResponder} that uses {@link Channel} to write back to client.
 */
final class BasicHttpResponder extends AbstractHttpResponder {

  private static final Logger LOG = LoggerFactory.getLogger(BasicHttpResponder.class);

  private final Channel channel;
  private final AtomicBoolean responded;
  private final boolean sslEnabled;

  BasicHttpResponder(Channel channel, boolean sslEnabled) {
    this.channel = channel;
    this.responded = new AtomicBoolean(false);
    this.sslEnabled = sslEnabled;
  }

  @Override
  public ChunkResponder sendChunkStart(HttpResponseStatus status, HttpHeaders headers) {
    if (status.code() < 200 || status.code() >= 210) {
      throw new IllegalArgumentException("Status code must be between 200 and 210. Status code provided is "
                                           + status.code());
    }

    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
    addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE);

    if (HttpUtil.getContentLength(response, -1L) < 0) {
      HttpUtil.setTransferEncodingChunked(response, true);
    }

    checkNotResponded();
    channel.write(response);
    return new ChannelChunkResponder(channel);
  }

  @Override
  public void sendContent(HttpResponseStatus status, ByteBuf content, HttpHeaders headers) {
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
    response.headers().add(headers);
    HttpUtil.setContentLength(response, content.readableBytes());

    if (content.isReadable()) {
      addContentTypeIfMissing(response.headers(), OCTET_STREAM_TYPE);
    }

    checkNotResponded();
    channel.writeAndFlush(response);
  }

  @Override
  public void sendFile(File file, HttpHeaders headers) throws IOException {
    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
    addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE);

    HttpUtil.setTransferEncodingChunked(response, false);
    HttpUtil.setContentLength(response, file.length());

    // Open the file first to make sure it is readable before sending out the response
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    try {
      checkNotResponded();

      // Write the initial line and the header.
      channel.write(response);

      // Write the content.
      // FileRegion only works in non-SSL case. For SSL case, use ChunkedFile instead.
      // See https://github.com/netty/netty/issues/2075
      if (sslEnabled) {
        // The HttpChunkedInput will write out the last content
        channel.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 8192)));
      } else {
        // The FileRegion will close the file channel when it is done sending.
        FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, file.length());
        channel.write(region);
        channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
      }
    } catch (Throwable t) {
      try {
        raf.close();
      } catch (IOException ex) {
        t.addSuppressed(ex);
      }
      throw t;
    }
  }

  @Override
  public void sendContent(HttpResponseStatus status, final BodyProducer bodyProducer, HttpHeaders headers) {
    final long contentLength;
    try {
      contentLength = bodyProducer.getContentLength();
    } catch (Throwable t) {
      bodyProducer.handleError(t);
      // Response with error and close the connection
      sendContent(
        HttpResponseStatus.INTERNAL_SERVER_ERROR,
        Unpooled.copiedBuffer("Failed to determined content length. Cause: " + t.getMessage(), StandardCharsets.UTF_8),
        new DefaultHttpHeaders()
          .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
          .set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"));
      return;
    }

    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
    addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE);

    if (contentLength < 0L) {
      HttpUtil.setTransferEncodingChunked(response, true);
    } else {
      HttpUtil.setTransferEncodingChunked(response, false);
      HttpUtil.setContentLength(response, contentLength);
    }

    checkNotResponded();

    // Streams the data produced by the given BodyProducer
    channel.writeAndFlush(response).addListener(new ChannelFutureListener() {

      @Override
      public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
          callBodyProducerHandleError(bodyProducer, future.cause());
          channel.close();
          return;
        }
        channel.writeAndFlush(new HttpChunkedInput(new BodyProducerChunkedInput(bodyProducer, contentLength)))
          .addListener(createBodyProducerCompletionListener(bodyProducer));
      }
    });
  }

  /**
   * Returns {@code true} if response was sent.
   */
  boolean isResponded() {
    return responded.get();
  }

  private ChannelFutureListener createBodyProducerCompletionListener(final BodyProducer bodyProducer) {
    return new ChannelFutureListener() {
      @Override
      public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
          callBodyProducerHandleError(bodyProducer, future.cause());
          channel.close();
          return;
        }

        try {
          bodyProducer.finished();
        } catch (Throwable t) {
          callBodyProducerHandleError(bodyProducer, t);
          channel.close();
        }
      }
    };
  }

  private void callBodyProducerHandleError(BodyProducer bodyProducer, @Nullable Throwable failureCause) {
    try {
      bodyProducer.handleError(failureCause);
    } catch (Throwable t) {
      LOG.warn("Exception raised from BodyProducer.handleError() for {}", bodyProducer, t);
    }
  }

  private void checkNotResponded() {
    if (!responded.compareAndSet(false, true)) {
      throw new IllegalStateException("Response has already been sent");
    }
  }

  /**
   * A {@link ChunkedInput} implementation that produce chunks using {@link BodyProducer}.
   */
  private static final class BodyProducerChunkedInput implements ChunkedInput {

    private final BodyProducer bodyProducer;
    private final long length;
    private long bytesProduced;
    private ByteBuf nextChunk;
    private boolean completed;

    private BodyProducerChunkedInput(BodyProducer bodyProducer, long length) {
      this.bodyProducer = bodyProducer;
      this.length = length;
    }

    @Override
    public boolean isEndOfInput() throws Exception {
      if (completed) {
        return true;
      }
      if (nextChunk == null) {
        nextChunk = bodyProducer.nextChunk();
      }

      completed = !nextChunk.isReadable();
      if (completed && length >= 0 && bytesProduced != length) {
        throw new IllegalStateException("Body size doesn't match with content length. " +
                                          "Content-Length: " + length + ", bytes produced: " + bytesProduced);
      }

      return completed;
    }

    @Override
    public void close() throws Exception {
      // No-op. Calling of the BodyProducer.finish() is done via the channel future completion.
    }

    @Override
    public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
      return readChunk(ctx.alloc());
    }

    @Override
    public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
      if (isEndOfInput()) {
        // This shouldn't happen, but just to guard
        throw new IllegalStateException("No more data to produce from body producer");
      }
      ByteBuf chunk = nextChunk;
      bytesProduced += chunk.readableBytes();
      nextChunk = null;
      return chunk;
    }

    @Override
    public long length() {
      return length;
    }

    @Override
    public long progress() {
      return length >= 0 ? bytesProduced : 0;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy