Usage examples

All of the examples are available in the sources in runnable form.

Use the simple synchronous client

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "core" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.client3.{Request, Response, SimpleHttpClient, UriContext, asStringAlways, basicRequest}

import java.util.UUID

object SimpleClientGetAndPost extends App {
  val client = SimpleHttpClient()

  try {
    val response: Response[Either[String, String]] = client.send(basicRequest.get(uri"https://httpbin.org/get"))

    response.body match {
      case Left(body)  => println(s"Non-2xx response to GET with code ${response.code}:\n$body")
      case Right(body) => println(s"2xx response to GET:\n$body")
    }

    println("---\n")

    //

    val request2: Request[String, Any] = basicRequest
      .header("X-Correlation-ID", UUID.randomUUID().toString)
      .response(asStringAlways)
      .body("Hello, world!")
      .post(uri"https://httpbin.org/post")
    val response2: Response[String] = client.send(request2)

    println(s"Response to POST:\n${response2.body}")

  } finally client.close()
}

POST a form using the synchronous backend

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "core" % "3.10.1")

Example code:

package sttp.client3.examples

object PostFormSynchronous extends App {
  import sttp.client3._

  val signup = Some("yes")

  val request = basicRequest
    // send the body as form data (x-www-form-urlencoded)
    .body(Map("name" -> "John", "surname" -> "doe"))
    // use an optional parameter in the URI
    .post(uri"https://httpbin.org/post?signup=$signup")

  val backend = HttpClientSyncBackend()
  val response = request.send(backend)

  println(response.body)
  println(response.headers)
}

GET and parse JSON using the akka-http backend and json4s

Required dependencies:

libraryDependencies ++= List(
  "com.softwaremill.sttp.client3" %% "akka-http-backend" % "3.10.1",
  "com.softwaremill.sttp.client3" %% "json4s" % "3.10.1",
  "org.json4s" %% "json4s-native" % "3.6.0"
)

Example code:

package sttp.client3.examples

object GetAndParseJsonAkkaHttpJson4s extends App {
  import scala.concurrent.Future

  import sttp.client3._
  import sttp.client3.akkahttp._
  import sttp.client3.json4s._

  import scala.concurrent.ExecutionContext.Implicits.global

  case class HttpBinResponse(origin: String, headers: Map[String, String])

  implicit val serialization = org.json4s.native.Serialization
  implicit val formats = org.json4s.DefaultFormats
  val request = basicRequest
    .get(uri"https://httpbin.org/get")
    .response(asJson[HttpBinResponse])

  val backend: SttpBackend[Future, Any] = AkkaHttpBackend()
  val response: Future[Response[Either[ResponseException[String, Exception], HttpBinResponse]]] =
    request.send(backend)

  for {
    r <- response
  } {
    println(s"Got response code: ${r.code}")
    println(r.body)
    backend.close()
  }
}

GET and parse JSON using the ZIO http-client backend and circe

Required dependencies:

libraryDependencies ++= List(
  "com.softwaremill.sttp.client3" %% "zio" % "3.10.1",
  "com.softwaremill.sttp.client3" %% "circe" % "3.10.1",
  "io.circe" %% "circe-generic" % "0.14.10"
)

Example code:

package sttp.client3.examples

import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._
import sttp.client3.httpclient.zio.{HttpClientZioBackend, send}
import zio._

object GetAndParseJsonZioCirce extends ZIOAppDefault {

  override def run = {
    case class HttpBinResponse(origin: String, headers: Map[String, String])

    val request = basicRequest
      .get(uri"https://httpbin.org/get")
      .response(asJson[HttpBinResponse])

    for {
      response <- send(request)
      _ <- Console.printLine(s"Got response code: ${response.code}")
      _ <- Console.printLine(response.body.toString)
    } yield ()
  }.provideLayer(ZLayer.debug("additional layer") ++ HttpClientZioBackend.layer())

}

GET and parse JSON using the http-client Monix backend and circe, treating deserialization errors as failed effects

Required dependencies:

libraryDependencies ++= List(
  "com.softwaremill.sttp.client3" %% "monix" % "3.10.1",
  "com.softwaremill.sttp.client3" %% "circe" % "3.10.1",
  "io.circe" %% "circe-generic" % "0.14.10"
)

Example code:

package sttp.client3.examples

import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.httpclient.monix.HttpClientMonixBackend
import sttp.client3.circe._

object GetAndParseJsonGetRightMonixCirce extends App {
  import monix.execution.Scheduler.Implicits.global

  case class HttpBinResponse(origin: String, headers: Map[String, String])

  val request: Request[HttpBinResponse, Any] = basicRequest
    .get(uri"https://httpbin.org/get")
    .response(asJson[HttpBinResponse].getRight)

  HttpClientMonixBackend
    .resource()
    .use { backend =>
      request.send(backend).map { response: Response[HttpBinResponse] =>
        println(s"Got response code: ${response.code}")
        println(response.body)
      }
    }
    .runSyncUnsafe()
}

Log requests & responses using slf4j

Required dependencies:

libraryDependencies ++= List(
  "com.softwaremill.sttp.client3" %% "slf4j-backend" % "3.10.1",
  "com.softwaremill.sttp.client3" %% "circe" % "3.10.1",
  "io.circe" %% "circe-generic" % "0.14.10"
)

Example code:

package sttp.client3.examples

import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._
import sttp.client3.logging.slf4j.Slf4jLoggingBackend

object LogRequestsSlf4j extends App {
  case class HttpBinResponse(origin: String, headers: Map[String, String])

  val request = basicRequest
    .get(uri"https://httpbin.org/get")
    .response(asJson[HttpBinResponse].getRight)

  val backend: SttpBackend[Identity, Any] =
    Slf4jLoggingBackend(
      HttpClientSyncBackend(),
      includeTiming = true,
      logRequestBody = false,
      logResponseBody = false
    )

  try {
    val response: Response[HttpBinResponse] = request.send(backend)
    println("Done! " + response.code)
  } finally backend.close()
}

POST and serialize JSON using the Monix http-client backend and circe

Required dependencies:

libraryDependencies ++= List(
  "com.softwaremill.sttp.client3" %% "monix" % "3.10.1",
  "com.softwaremill.sttp.client3" %% "circe" % "3.10.1",
  "io.circe" %% "circe-generic" % "0.14.10"
)

Example code:

package sttp.client3.examples

object PostSerializeJsonMonixHttpClientCirce extends App {
  import sttp.client3._
  import sttp.client3.circe._
  import sttp.client3.httpclient.monix.HttpClientMonixBackend
  import io.circe.generic.auto._
  import monix.eval.Task

  case class Info(x: Int, y: String)

  val postTask = HttpClientMonixBackend().flatMap { backend =>
    val r = basicRequest
      .body(Info(91, "abc"))
      .post(uri"https://httpbin.org/post")

    r.send(backend)
      .flatMap { response => Task(println(s"""Got ${response.code} response, body:\n${response.body}""")) }
      .guarantee(backend.close())
  }

  import monix.execution.Scheduler.Implicits.global
  postTask.runSyncUnsafe()
}

Test an endpoint which requires multiple query parameters

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "core" % "3.10.1")

Example code:

package sttp.client3.examples

object TestEndpointMultipleQueryParameters extends App {
  import sttp.client3._
  import sttp.client3.testing._

  val backend = SttpBackendStub.synchronous
    .whenRequestMatches(_.uri.paramsMap.contains("filter"))
    .thenRespond("Filtered")
    .whenRequestMatches(_.uri.path.contains("secret"))
    .thenRespond("42")

  val parameters1 = Map("filter" -> "name=mary", "sort" -> "asc")
  println(
    basicRequest
      .get(uri"http://example.org?search=true&$parameters1")
      .send(backend)
      .body
  )

  val parameters2 = Map("sort" -> "desc")
  println(
    basicRequest
      .get(uri"http://example.org/secret/read?$parameters2")
      .send(backend)
      .body
  )
}

Open a websocket using ZIO

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "zio" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.capabilities.WebSockets
import sttp.client3._
import sttp.client3.httpclient.zio.HttpClientZioBackend
import sttp.ws.WebSocket
import zio.{Console, _}

object WebSocketZio extends ZIOAppDefault {
  def useWebSocket(ws: WebSocket[Task]): Task[Unit] = {
    def send(i: Int) = ws.sendText(s"Hello $i!")
    val receive = ws.receiveText().flatMap(t => Console.printLine(s"RECEIVED: $t"))
    send(1) *> send(2) *> receive *> receive
  }

  // create a description of a program, which requires SttpClient dependency in the environment
  def sendAndPrint(backend: SttpBackend[Task, WebSockets]): Task[Response[Unit]] =
    backend.send(basicRequest.get(uri"wss://ws.postman-echo.com/raw").response(asWebSocketAlways(useWebSocket)))

  override def run = {
    // provide an implementation for the SttpClient dependency
    HttpClientZioBackend.scoped().flatMap(sendAndPrint)
  }
}

Open a websocket using FS2 streams

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "fs2" % "3.10.1")

Example code:

package sttp.client3.examples

import cats.effect.IO
import cats.effect.unsafe.IORuntime
import fs2._
import sttp.capabilities.fs2.Fs2Streams
import sttp.client3._
import sttp.client3.httpclient.fs2.HttpClientFs2Backend
import sttp.ws.WebSocketFrame

object WebSocketStreamFs2 extends App {
  implicit val runtime: IORuntime = cats.effect.unsafe.implicits.global

  def webSocketFramePipe: Pipe[IO, WebSocketFrame.Data[_], WebSocketFrame] = { input =>
    Stream.emit(WebSocketFrame.text("1")) ++ input.flatMap {
      case WebSocketFrame.Text("10", _, _) =>
        println("Received 10 messages, sending close frame")
        Stream.emit(WebSocketFrame.close)
      case WebSocketFrame.Text(n, _, _) =>
        println(s"Received $n messages, replying with $n+1")
        Stream.emit(WebSocketFrame.text((n.toInt + 1).toString))
      case _ => Stream.empty // ignoring
    }
  }

  HttpClientFs2Backend
    .resource[IO]()
    .use { backend =>
      basicRequest
        .response(asWebSocketStream(Fs2Streams[IO])(webSocketFramePipe))
        .get(uri"wss://ws.postman-echo.com/raw")
        .send(backend)
        .void
    }
    .unsafeRunSync()
}

Test Monix websockets

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "monix" % "3.10.1")

Example code:

package sttp.client3.examples

import monix.eval.Task
import sttp.capabilities.WebSockets
import sttp.capabilities.monix.MonixStreams
import sttp.client3._
import sttp.client3.httpclient.monix.HttpClientMonixBackend
import sttp.client3.testing.SttpBackendStub
import sttp.model.StatusCode
import sttp.ws.{WebSocket, WebSocketFrame}
import sttp.ws.testing.WebSocketStub

object WebSocketTesting extends App {
  // the web socket-handling logic
  def useWebSocket(ws: WebSocket[Task]): Task[Unit] = {
    def send(i: Int) = ws.sendText(s"Hello $i!")
    val receive = ws.receiveText().flatMap(t => Task(println(s"RECEIVED [$t]")))
    send(1) *> send(2) *> receive *> receive
  }

  // the request description
  def openWebSocket(backend: SttpBackend[Task, WebSockets]): Task[Unit] = {
    basicRequest
      .response(asWebSocket(useWebSocket))
      .get(uri"wss://echo.websocket.org")
      .send(backend)
      .void
  }

  // the backend stub which we'll use instead of a "real" backend
  val stubBackend: SttpBackendStub[Task, MonixStreams with WebSockets] =
    HttpClientMonixBackend.stub
      .whenRequestMatches(_.uri.toString().contains("echo.websocket.org"))
      .thenRespond(
        WebSocketStub.noInitialReceive.thenRespond {
          case WebSocketFrame.Text(payload, _, _) =>
            List(WebSocketFrame.text(s"response to: $payload"))
          case _ => Nil // ignoring other types of messages
        },
        StatusCode.SwitchingProtocols
      )

  // running the test
  import monix.execution.Scheduler.Implicits.global
  openWebSocket(stubBackend).runSyncUnsafe()
}

Open a websocket using Akka

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "akka-http-backend" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.client3._
import sttp.client3.akkahttp.AkkaHttpBackend
import sttp.ws.WebSocket

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object WebSocketAkka extends App {
  def useWebSocket(ws: WebSocket[Future]): Future[Unit] = {
    def send(i: Int) = ws.sendText(s"Hello $i!")
    def receive() = ws.receiveText().map(t => println(s"RECEIVED: $t"))
    for {
      _ <- send(1)
      _ <- send(2)
      _ <- receive()
      _ <- receive()
    } yield ()
  }

  val backend = AkkaHttpBackend()

  basicRequest
    .response(asWebSocket(useWebSocket))
    .get(uri"wss://ws.postman-echo.com/raw")
    .send(backend)
    .onComplete(_ => backend.close())
}

Open a websocket using Pekko

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "pekko-http-backend" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.client3._
import sttp.client3.pekkohttp.PekkoHttpBackend
import sttp.ws.WebSocket

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object WebSocketPekko extends App {
  def useWebSocket(ws: WebSocket[Future]): Future[Unit] = {
    def send(i: Int) = ws.sendText(s"Hello $i!")
    def receive() = ws.receiveText().map(t => println(s"RECEIVED: $t"))
    for {
      _ <- send(1)
      _ <- send(2)
      _ <- receive()
      _ <- receive()
    } yield ()
  }

  val backend = PekkoHttpBackend()

  basicRequest
    .get(uri"wss://ws.postman-echo.com/raw")
    .response(asWebSocket(useWebSocket))
    .send(backend)
    .onComplete(_ => backend.close())
}

Open a websocket using Monix

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "monix" % "3.10.1")

Example code:

package sttp.client3.examples

import monix.eval.Task
import sttp.client3._
import sttp.client3.httpclient.monix.HttpClientMonixBackend
import sttp.ws.WebSocket

object WebSocketMonix extends App {
  import monix.execution.Scheduler.Implicits.global

  def useWebSocket(ws: WebSocket[Task]): Task[Unit] = {
    def send(i: Int) = ws.sendText(s"Hello $i!")
    val receive = ws.receiveText().flatMap(t => Task(println(s"RECEIVED: $t")))
    send(1) *> send(2) *> receive *> receive
  }

  HttpClientMonixBackend
    .resource()
    .use { backend =>
      basicRequest
        .response(asWebSocket(useWebSocket))
        .get(uri"wss://ws.postman-echo.com/raw")
        .send(backend)
        .void
    }
    .runSyncUnsafe()
}

Stream request and response bodies using fs2

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "fs2" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.client3._
import sttp.client3.httpclient.fs2.HttpClientFs2Backend
import cats.effect.IO
import cats.instances.string._
import fs2.{Stream, text}
import sttp.capabilities.fs2.Fs2Streams

object StreamFs2 extends App {

  def streamRequestBody(backend: SttpBackend[IO, Fs2Streams[IO]]): IO[Unit] = {
    val stream: Stream[IO, Byte] = Stream.emits("Hello, world".getBytes)

    basicRequest
      .streamBody(Fs2Streams[IO])(stream)
      .post(uri"https://httpbin.org/post")
      .send(backend)
      .map { response => println(s"RECEIVED:\n${response.body}") }
  }

  def streamResponseBody(backend: SttpBackend[IO, Fs2Streams[IO]]): IO[Unit] = {
    basicRequest
      .body("I want a stream!")
      .post(uri"https://httpbin.org/post")
      .response(asStreamAlways(Fs2Streams[IO])(_.chunks.through(text.utf8.decodeC).compile.foldMonoid))
      .send(backend)
      .map { response => println(s"RECEIVED:\n${response.body}") }
  }

  val effect = HttpClientFs2Backend.resource[IO]().use { backend =>
    streamRequestBody(backend).flatMap(_ => streamResponseBody(backend))
  }

  effect.unsafeRunSync()(cats.effect.unsafe.implicits.global)
}

Stream request and response bodies using zio-stream

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "zio" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.capabilities.zio.ZioStreams
import sttp.client3._
import zio.Console._
import zio._
import zio.stream._
import sttp.client3.httpclient.zio.{HttpClientZioBackend, SttpClient, send}

object StreamZio extends ZIOAppDefault {
  def streamRequestBody: RIO[SttpClient, Unit] = {
    val stream: Stream[Throwable, Byte] = ZStream("Hello, world".getBytes.toIndexedSeq: _*)
    send(
      basicRequest
        .streamBody(ZioStreams)(stream)
        .post(uri"https://httpbin.org/post")
    ).flatMap { response => printLine(s"RECEIVED:\n${response.body}") }
  }

  def streamResponseBody: RIO[SttpClient, Unit] =
    send(
      basicRequest
        .body("I want a stream!")
        .post(uri"https://httpbin.org/post")
        .response(asStreamAlways(ZioStreams)(_.via(ZPipeline.utf8Decode).runFold("")(_ + _)))
    ).flatMap { response => printLine(s"RECEIVED:\n${response.body}") }

  override def run =
    (streamRequestBody *> streamResponseBody).provide(HttpClientZioBackend.layer())
}

Retry a request using ZIO

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "zio" % "3.10.1")

Example code:

package sttp.client3.examples

import sttp.client3._
import sttp.client3.httpclient.zio.HttpClientZioBackend
import zio.{Schedule, Task, ZIO, ZIOAppDefault, durationInt}

object RetryZio extends ZIOAppDefault {
  override def run: ZIO[Any, Throwable, Response[String]] = {
    HttpClientZioBackend()
      .flatMap { backend =>
        val localhostRequest = basicRequest
          .get(uri"http://localhost/test")
          .response(asStringAlways)

        val sendWithRetries: Task[Response[String]] = localhostRequest
          .send(backend)
          .either
          .repeat(
            Schedule.spaced(1.second) *>
              Schedule.recurs(10) *>
              Schedule.recurWhile(result => RetryWhen.Default(localhostRequest, result))
          )
          .absolve

        sendWithRetries.ensuring(backend.close().ignore)
      }
  }
}

GET parsed and raw response bodies

Required dependencies:

libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "core" % "3.10.1")

Example code:

package sttp.client3.examples

import io.circe
import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._

object GetRawResponseBodySynchronous extends App {
  case class HttpBinResponse(origin: String, headers: Map[String, String])

  val request = basicRequest
    .get(uri"https://httpbin.org/get")
    .response(asBoth(asJson[HttpBinResponse], asStringAlways))

  val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend()

  try {
    val response: Response[(Either[ResponseException[String, circe.Error], HttpBinResponse], String)] =
      request.send(backend)

    val (parsed, raw) = response.body

    println("Got response - parsed: " + parsed)
    println("Got response - raw: " + raw)

  } finally backend.close()
}