Entity handling

Why Entity*

Http4s handles HTTP requests and responses in a streaming fashion. Your service will receive a request after the header has been parsed (ok, not 100% streaming), but before the body has been fully received. The same applies to the http client usage, where you can start a connection before the body is fully materialized. You don’t have to load the full body into memory to submit the request either. Taking a look at Request[F] and Response[F], both have a body of type EntityBody[F], which is simply an alias to Stream[F, Byte]. To understand Stream, take a look at the introduction-to-functional-streams.

The EntityDecoder and EntityEncoder help with the streaming nature of the data in a http body, and they also have additional logic to deal with media types. Not all decoders are streaming, depending on the implementation.

Construction and Media Types

Entity*s also encode which media types they correspond to. The EntityDecoders for json expect application/json. To implement this functionality, the constructor EntityDecoder.decodeBy uses MediaRanges. You can pass multiple as needed. You can also append functionality to an existing one via EntityDecoder[F, T].map - however, you can’t change the media type in that case.

When you encode a body with the EntityEncoder for json, it appends the Content-Type: application/json header. You can construct new encoders via EntityEncoder.encodeBy or reuse an already existing one via EntityEncoder[F, T].contramap and withContentType.

See the MediaRange companion object for ranges, and MediaType for specific types. Because of the implicit conversions, you can also use (String, String) for a MediaType.

By default, decoders content types are ignored since it could lead to unexpected runtime errors.

Chaining Decoders

Decoders’ content types are used when chaining decoders with orElse in order to determine which of the chained decoders are to be used.

import org.http4s._
import org.http4s.headers.`Content-Type`
import org.http4s.dsl.io._
import cats._, cats.effect._, cats.implicits._, cats.data._

sealed trait Resp
case class Audio(body: String) extends Resp
case class Video(body: String) extends Resp
val response = Ok("").map(_.withContentType(`Content-Type`(MediaType.audio.ogg)))
// response: IO[Response[IO]] = Map(
//   Pure(
//     Response(
//       Status(200),
//       HttpVersion(1, 1),
//       Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 0),
//       Stream(..),
//       io.chrisdavenport.vault.Vault@3382b9d0
//     )
//   ),
//   <function1>,
//   StackTrace(
//     List(
//       cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//       cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//       cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//       cats.effect.IO.map(IO.scala:106),
//       repl.MdocSession$App.<init>(entity.md:32),
//       repl.MdocSession$.app(entity.md:3),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withErr(Console.scala:196),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withOut(Console.scala:167),
//       mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//       mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//       mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:103)
//     )
//   )
// )
val audioDec = EntityDecoder.decodeBy(MediaType.audio.ogg) { (m: Media[IO]) =>
  EitherT {
    m.as[String].map(s => Audio(s).asRight[DecodeFailure])
  }
}
// audioDec: EntityDecoder[IO, Audio] = org.http4s.EntityDecoder$$anon$7@2326c424
val videoDec = EntityDecoder.decodeBy(MediaType.video.ogg) { (m: Media[IO]) =>
  EitherT {
    m.as[String].map(s => Video(s).asRight[DecodeFailure])
  }
}
// videoDec: EntityDecoder[IO, Video] = org.http4s.EntityDecoder$$anon$7@b906692
implicit val bothDec = audioDec.widen[Resp] orElse videoDec.widen[Resp]
// bothDec: EntityDecoder[IO, Resp] = org.http4s.EntityDecoder$$anon$5$$anon$6@38ba0a1b
println(response.flatMap(_.as[Resp]).unsafeRunSync())
// Audio()

Presupplied Encoders/Decoders

The EntityEncoder/EntityDecoders shipped with http4s.

Raw Data Types

These are already in implicit scope by default, e.g. String, File, Future[_], and InputStream. Consult EntityEncoder and EntityDecoder for a full list.

JSON

With jsonOf for the EntityDecoder, and jsonEncoderOf for the EntityEncoder:

  • argonaut: "org.http4s" %% "http4s-argonaut" % http4sVersion
  • circe: "org.http4s" %% "http4s-circe" % http4sVersion
  • json4s-native: "org.http4s" %% "http4s-json4s-native" % http4sVersion
  • json4s-jackson: "org.http4s" %% "http4s-json4s-jackson" % http4sVersion

XML

For scala-xml (xml literals), import org.http4s.scalaxml. No direct naming required here, because there is no Decoder instance for String that would cause conflicts with the builtin Decoders.

  • scala-xml: "org.http4s" %% "http4s-scala-xml" % http4sVersion

Support for Twirl and Scalatags

If you’re working with either twirl or scalatags you can use our bridges:

  • scala-twirl: "org.http4s" %% "http4s-twirl" % http4sVersion
  • scala-scalatags: "org.http4s" %% "http4s-scalatags" % http4sVersion