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

com.twitter.finagle.http.Chunk.scala Maven / Gradle / Ivy

package com.twitter.finagle.http

import com.twitter.io.Buf
import java.nio.ByteBuffer

/**
 * Represents a piece of an HTTP stream, commonly referred to as a chunk.
 *
 * HTTP semantics treat trailing headers as the end of stream signal hence no writes (neither
 * trailers nor data) are allowed after them, only `close` (EOS) is valid. This is typically
 * enforced by an HTTP dispatcher in both clients and servers.
 *
 * Similarly, when consumed via [[com.twitter.io.Reader]], trailers are always followed by `None`,
 * readers's EOS. Users MUST read until the end of stream to ensure resource hygiene.
 *
 * The following example demonstrates one way of doing so: wiring in one extra read before
 * returning from a read-loop:
 *
 * {{{
 *   def accumulate(r: Reader[Chunk]): Future[(Buf, Option[HeaderMap])] = {
 *     def loop(acc: Buf, trailers: Option[HeaderMap]): Future[(Buf, Option[HeaderMap])] =
 *       r.read().flatMap {
 *         case Some(chunk) =>
 *           if (chunk.isLast && !chunk.trailers.isEmpty)
 *             loop(acc.concat(chunk.content), Some(chunk.trailers))
 *           else
 *             loop(acc.concat(chunk.content), None)
 *         case None =>
 *           Future.value(acc -> trailers)
 *       }
 *
 *     loop(Buf.Empty, None)
 *   }
 * }}}
 *
 * The HTTP dispatcher guarantees that `Chunk.Last` will be issued in the inbound stream
 * no matter if its `trailers` or `content` present.
 *
 * Note: when consuming interleaved HTTP streams (i.e., via `Reader.flatMap`) it's expected to
 * observe multiple trailers before reaching the EOS. These inter-stream trailers space out
 * individual HTTP streams from child readers.
 */
sealed abstract class Chunk {

  /**
   * The payload of this chunk. Can be empty if this is a last chunk.
   */
  def content: Buf

  /**
   * The trailing headers of this chunk. Can throw [[IllegalArgumentException]] if this is not the
   * last chunk.
   */
  def trailers: HeaderMap

  /**
   * Whether this chunk is last in the stream.
   */
  def isLast: Boolean
}

object Chunk {

  private final case class Cons(content: Buf) extends Chunk {
    def trailers: HeaderMap = throw new IllegalArgumentException("Not the last chunk")
    def isLast: Boolean = false
  }

  private final case class Last(content: Buf, trailers: HeaderMap) extends Chunk {
    def isLast: Boolean = true
  }

  /**
   * A last (end of stream) empty [[Chunk]] that has neither a payload nor trailing headers.
   */
  val lastEmpty: Chunk = last(HeaderMap.Empty)

  /**
   * Creates a last [[Chunk]] that carries trailing headers (`trailers`).
   */
  def last(trailers: HeaderMap): Chunk = last(Buf.Empty, trailers)

  /**
   * Creates a last [[Chunk]] that carries both payload (`content`) and trailing
   * headers (`trailers`).
   */
  def last(content: Buf, trailers: HeaderMap): Chunk = Last(content, trailers)

  /**
   * Creates a non last [[Chunk]] that carries a payload (`content`).
   */
  def fromBuf(content: Buf): Chunk = Cons(content)

  /**
   * A shortcut to create a [[Chunk]] out of a UTF-8 string.
   */
  def fromString(s: String): Chunk = fromBuf(Buf.Utf8(s))

  /**
   * A shortcut to create a [[Chunk]] out of a byte array.
   */
  def fromByteArray(ba: Array[Byte]): Chunk = fromBuf(Buf.ByteArray.Owned(ba))

  /**
   * A shortcut to create a [[Chunk]] out of a Java [[ByteBuffer]].
   */
  def fromByteBuffer(bb: ByteBuffer): Chunk = fromBuf(Buf.ByteBuffer.Owned(bb))

  /**
   * Creates a non last [[Chunk]] that carries a payload (`content`). This is an alias
   * for [[fromBuf]].
   */
  def apply(content: Buf): Chunk = fromBuf(content)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy