org.http4s.Message.scala Maven / Gradle / Ivy
package org.http4s
import java.io.File
import java.net.{InetAddress, InetSocketAddress}
import cats._
import cats.implicits._
import fs2._
import fs2.interop.cats._
import fs2.text._
import org.http4s.headers._
import org.http4s.util.nonEmptyList._
import org.http4s.server.ServerSoftware
import org.http4s.util.CaseInsensitiveString
import org.log4s.getLogger
/**
 * Represents a HTTP Message. The interesting subclasses are Request and
 * Response while most of the functionality is found in [[MessageSyntax]] and
 * [[ResponseOps]]
 * @see [[MessageSyntax]], [[ResponseOps]]
 */
sealed trait Message extends MessageOps { self =>
  type Self <: Message { type Self = self.Self }
  def httpVersion: HttpVersion
  def headers: Headers
  def body: EntityBody
  final def bodyAsText(implicit defaultCharset: Charset = DefaultCharset): Stream[Task, String] = {
    (charset getOrElse defaultCharset) match {
      case Charset.`UTF-8` =>
        // suspect this one is more efficient, though this is superstition
        body.through(utf8Decode)
      case cs =>
        body.through(util.decode(cs))
    }
  }
  /** True if and only if the body is composed solely of Emits and Halt. This
    * indicates that the body can be re-run without side-effects. */
  // TODO fs2 port need to replace unemit
  /*
  def isBodyPure: Boolean =
    body.unemit._2.isHalt
   */
  def attributes: AttributeMap
  protected def change(body: EntityBody = body,
                       headers: Headers = headers,
                       attributes: AttributeMap = attributes): Self
  override def transformHeaders(f: Headers => Headers): Self =
    change(headers = f(headers))
  override def withAttribute[A](key: AttributeKey[A], value: A): Self =
    change(attributes = attributes.put(key, value))
  /** Replace the body of this message with a new body
    *
    * @param b body to attach to this method
    * @param w [[EntityEncoder]] with which to convert the body to an [[EntityBody]]
    * @tparam T type of the Body
    * @return a new message with the new body
    */
  def withBody[T](b: T)(implicit w: EntityEncoder[T]): Task[Self] = {
    w.toEntity(b).flatMap { entity =>
      val hs = entity.length match {
        case Some(l) => `Content-Length`.fromLong(l).fold(_ =>
          Task.now {
            Message.logger.warn(s"Attempt to provide a negative content length of $l")
            w.headers.toList
          },
          cl => Task.now(cl :: w.headers.toList))
        case None    => Task.now(w.headers)
      }
      hs.map(newHeaders => change(body = entity.body, headers = headers ++ newHeaders))
    }
  }
  /** Sets the entity body without affecting headers such as `Transfer-Encoding`
    * or `Content-Length`. Most use cases are better served by [[withBody]],
    * which uses an [[EntityEncoder]] to maintain the headers.
    */
  def withBodyStream(body: EntityBody): Self
  /** Set an empty entity body on this message, and remove all payload headers
    * that make no sense with an empty body.
    */
  def withEmptyBody: Self =
    withBodyStream(EmptyBody).transformHeaders(_.removePayloadHeaders)
  def contentLength: Option[Long] = headers.get(`Content-Length`).map(_.length)
  def contentType: Option[`Content-Type`] = headers.get(`Content-Type`)
  /** Returns the charset parameter of the `Content-Type` header, if present. Does
    * not introspect the body for media types that define a charset
    * internally.
    */
  def charset: Option[Charset] = contentType.flatMap(_.charset)
  def isChunked: Boolean = headers.get(`Transfer-Encoding`).exists(_.values.contains(TransferCoding.chunked))
  /** The trailer headers, as specified in Section 3.6.1 of RFC 2616. The
    * resulting task might not complete unless the entire body has been
    * consumed.
    */
  def trailerHeaders: Task[Headers] = attributes.get(Message.Keys.TrailerHeaders).getOrElse(Task.now(Headers.empty))
  /** Decode the [[Message]] to the specified type
    *
    * @param decoder [[EntityDecoder]] used to decode the [[Message]]
    * @tparam T type of the result
    * @return the `Task` which will generate the `DecodeResult[T]`
    */
  override def attemptAs[T](implicit decoder: EntityDecoder[T]): DecodeResult[T] =
    decoder.decode(this, strict = false)
}
object Message {
  private[http4s] val logger = getLogger
  object Keys {
    val TrailerHeaders = AttributeKey[Task[Headers]]
  }
}
/** Representation of an incoming HTTP message
  *
  * A Request encapsulates the entirety of the incoming HTTP request including the
  * status line, headers, and a possible request body.
  *
  * @param method [[Method.GET]], [[Method.POST]], etc.
  * @param uri representation of the request URI
  * @param httpVersion the HTTP version
  * @param headers collection of [[Header]]s
  * @param body scalaz.stream.Process[Task,Chunk] defining the body of the request
  * @param attributes Immutable Map used for carrying additional information in a type safe fashion
  */
sealed abstract case class Request(
  method: Method = Method.GET,
  uri: Uri = Uri(path = "/"),
  httpVersion: HttpVersion = HttpVersion.`HTTP/1.1`,
  headers: Headers = Headers.empty,
  body: EntityBody = EmptyBody,
  attributes: AttributeMap = AttributeMap.empty
) extends Message with RequestOps {
  import Request._
  type Self = Request
  private def requestCopy(
      method: Method = this.method,
      uri: Uri = this.uri,
      httpVersion: HttpVersion = this.httpVersion,
      headers: Headers = this.headers,
      body: EntityBody = this.body,
      attributes: AttributeMap = this.attributes
    ): Request =
    Request(
      method = method,
      uri = uri,
      httpVersion = httpVersion,
      headers = headers,
      body = body,
      attributes = attributes
    )
  @deprecated(message = "Copy method is unsafe for setting path info. Use with... methods instead", "0.17.0-M3")
  def copy(
    method: Method = this.method,
    uri: Uri = this.uri,
    httpVersion: HttpVersion = this.httpVersion,
    headers: Headers = this.headers,
    body: EntityBody = this.body,
    attributes: AttributeMap = this.attributes
  ): Request =
    requestCopy(
      method = method,
      uri = uri,
      httpVersion = httpVersion,
      headers = headers,
      body = body,
      attributes = attributes
    )
  def withMethod(method: Method) = requestCopy(method = method)
  def withUri(uri: Uri) = requestCopy(uri = uri, attributes = attributes -- Request.Keys.PathInfoCaret)
  def withHttpVersion(httpVersion: HttpVersion) = requestCopy(httpVersion = httpVersion)
  def withHeaders(headers: Headers) = requestCopy(headers = headers)
  def withAttributes(attributes: AttributeMap) = requestCopy(attributes = attributes)
  def withBodyStream(body: EntityBody): Request =
    requestCopy(body = body)
  override protected def change(body: EntityBody, headers: Headers, attributes: AttributeMap): Self =
    requestCopy(body = body, headers = headers, attributes = attributes)
  lazy val authType: Option[AuthScheme] = headers.get(Authorization).map(_.credentials.authScheme)
  lazy val (scriptName, pathInfo) = {
    val caret = attributes.get(Request.Keys.PathInfoCaret).getOrElse(0)
    uri.path.splitAt(caret)
  }
  def withPathInfo(pi: String): Request =
    withUri(uri.withPath(scriptName + pi))
  lazy val pathTranslated: Option[File] = attributes.get(Keys.PathTranslated)
  def queryString: String = uri.query.renderString
  /**
    * Representation of the query string as a map
    *
    * In case a parameter is available in query string but no value is there the
    * sequence will be empty. If the value is empty the the sequence contains an
    * empty string.
    *
    * =====Examples=====
    * 
    * Query String Map  
    * ?param=vMap("param" -> Seq("v")) 
    * ?param=Map("param" -> Seq("")) 
    * ?paramMap("param" -> Seq()) 
    * ?=valueMap("" -> Seq("value")) 
    * ?p1=v1&p1=v2&p2=v3&p2=v3Map("p1" -> Seq("v1","v2"), "p2" -> Seq("v3","v4")) 
    * 
    *
    * The query string is lazily parsed. If an error occurs during parsing
    * an empty `Map` is returned.
    */
  def multiParams: Map[String, Seq[String]] = uri.multiParams
  /** View of the head elements of the URI parameters in query string.
    *
    * In case a parameter has no value the map returns an empty string.
    *
    * @see multiParams
    */
  def params: Map[String, String] = uri.params
  private lazy val connectionInfo = attributes.get(Keys.ConnectionInfo)
  lazy val remote: Option[InetSocketAddress] = connectionInfo.map(_.remote)
  lazy val remoteAddr: Option[String] = remote.map(_.getHostString)
  lazy val remoteHost: Option[String] = remote.map(_.getHostName)
  lazy val remotePort: Option[Int]    = remote.map(_.getPort)
  lazy val remoteUser: Option[String] = None
  lazy val server: Option[InetSocketAddress] = connectionInfo.map(_.local)
  lazy val serverAddr: String = {
    server.map(_.getHostString)
      .orElse(uri.host.map(_.value))
      .orElse(headers.get(Host).map(_.host))
      .getOrElse(InetAddress.getLocalHost.getHostName)
  }
  lazy val serverPort: Int = {
    server.map(_.getPort)
      .orElse(uri.port)
      .orElse(headers.get(Host).flatMap(_.port))
      .getOrElse(80) // scalastyle:ignore
  }
  /** Whether the Request was received over a secure medium */
  lazy val isSecure: Option[Boolean] = connectionInfo.map(_.secure)
  def serverSoftware: ServerSoftware = attributes.get(Keys.ServerSoftware).getOrElse(ServerSoftware.Unknown)
  def decodeWith[A](decoder: EntityDecoder[A], strict: Boolean)(f: A => Task[Response]): Task[Response] =
    decoder.decode(this, strict = strict).fold(_.toHttpResponse(httpVersion), f).flatten
  override def toString: String = {
    val newHeaders = headers.redactSensitive()
    s"""Request(method=$method, uri=$uri, headers=$newHeaders)"""
  }
  // A request is idempotent if and only if its method is idempotent and its body
  // is pure.  If true, this request can be submitted multipe times.
  // TODO fs2 port uncomment when isBodyPure is back
  /*
  def isIdempotent: Boolean =
    method.isIdempotent && isBodyPure
   */
}
object Request {
  def apply(
    method: Method = Method.GET,
    uri: Uri = Uri(path = "/"),
    httpVersion: HttpVersion = HttpVersion.`HTTP/1.1`,
    headers: Headers = Headers.empty,
    body: EntityBody = EmptyBody,
    attributes: AttributeMap = AttributeMap.empty
  ) =
    new Request(
      method = method,
      uri = uri,
      httpVersion = httpVersion,
      headers = headers,
      body = body,
      attributes = attributes
    ) {}
  final case class Connection(local: InetSocketAddress, remote: InetSocketAddress, secure: Boolean)
  object Keys {
    val PathInfoCaret = AttributeKey[Int]
    val PathTranslated = AttributeKey[File]
    val ConnectionInfo = AttributeKey[Connection]
    val ServerSoftware = AttributeKey[ServerSoftware]
  }
}
/** Represents that a service either returns a [[Response]] or a [[Pass]] to
  * fall through to another service.
  */
sealed trait MaybeResponse {
  def cata[A](f: Response => A, a: => A): A =
    this match {
      case r: Response => f(r)
      case Pass => a
    }
  def orElse[B >: Response](b: => B): B =
    this match {
      case r: Response => r
      case Pass => b
    }
  def orNotFound: Response =
    orElse(Response(Status.NotFound))
  def toOption: Option[Response] =
    cata(Some(_), None)
}
object MaybeResponse {
  implicit val instance: Monoid[MaybeResponse] =
    new Monoid[MaybeResponse] {
      def empty =
        Pass
      def combine(a: MaybeResponse, b: MaybeResponse) =
        a orElse b
    }
  implicit val taskInstance: Monoid[Task[MaybeResponse]] =
    new Monoid[Task[MaybeResponse]] {
      def empty =
        Pass.now
      def combine(ta: Task[MaybeResponse], tb: Task[MaybeResponse]): Task[MaybeResponse] =
        ta.flatMap(_.cata(Task.now, tb))
    }
}
case object Pass extends MaybeResponse {
  val now: Task[MaybeResponse] =
    Task.now(Pass)
}
/** Representation of the HTTP response to send back to the client
  *
  * @param status [[Status]] code and message
  * @param headers [[Headers]] containing all response headers
  * @param body scalaz.stream.Process[Task,Chunk] representing the possible body
  * of the response
  * @param attributes [[AttributeMap]] containing additional parameters which
  * may be used by the http4s backend for additional processing such as
  * java.io.File object
  */
final case class Response(
  status: Status = Status.Ok,
  httpVersion: HttpVersion = HttpVersion.`HTTP/1.1`,
  headers: Headers = Headers.empty,
  body: EntityBody = EmptyBody,
  attributes: AttributeMap = AttributeMap.empty)
    extends Message with MaybeResponse with ResponseOps {
  type Self = Response
  override def withStatus(status: Status): Self =
    copy(status = status)
  def withBodyStream(body: EntityBody): Response =
    copy(body = body)
  override protected def change(body: EntityBody, headers: Headers, attributes: AttributeMap): Self =
    copy(body = body, headers = headers, attributes = attributes)
  override def toString: String = {
    val newHeaders = headers.redactSensitive()
    s"""Response(status=${status.code}, headers=$newHeaders)"""
  }
  /** Returns a list of cookies from the [[org.http4s.headers.Set-Cookie]]
    * headers. Includes expired cookies, such as those that represent cookie
    * deletion. */
  def cookies: List[Cookie] =
    `Set-Cookie`.from(headers).map(_.cookie)
}
object Response {
  @deprecated("Use Pass.now instead", "0.16")
  val fallthrough: Task[MaybeResponse] =
    Pass.now
  def notFound(request: Request): Task[Response] = {
    val body = s"${request.pathInfo} not found"
    Response(Status.NotFound).withBody(body)
  }
}
    © 2015 - 2025 Weber Informatics LLC | Privacy Policy