Scala - HTTP Client (sttp/akka-http)
The Scala HTTP client landscape centers on two mature libraries. sttp (Scala The Platform) offers backend-agnostic abstractions, letting you swap implementations without changing client code. Akka...
Key Insights
- sttp provides a unified API with multiple backend implementations including synchronous, Future-based, and effect system integrations, making it the most flexible HTTP client library in the Scala ecosystem
- Akka HTTP’s client API excels at connection pooling and streaming large payloads through its reactive streams foundation, ideal for high-throughput microservices
- Type-safe request building and automatic JSON serialization eliminate entire classes of runtime errors when integrated with circe or spray-json
Choosing Between sttp and Akka HTTP Client
The Scala HTTP client landscape centers on two mature libraries. sttp (Scala The Platform) offers backend-agnostic abstractions, letting you swap implementations without changing client code. Akka HTTP’s client complements its server components with connection pooling and backpressure handling.
Use sttp for applications requiring backend flexibility or integration with effect systems like ZIO or cats-effect. Choose Akka HTTP when building Akka-based systems or requiring advanced streaming capabilities.
Basic HTTP Requests with sttp
sttp’s core abstraction is Request, built through a fluent API. Start by adding dependencies:
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client3" %% "core" % "3.9.1",
"com.softwaremill.sttp.client3" %% "circe" % "3.9.1"
)
Simple synchronous requests use the HttpURLConnectionBackend:
import sttp.client3._
val backend = HttpURLConnectionBackend()
val response = basicRequest
.get(uri"https://api.github.com/users/scala")
.send(backend)
println(response.body)
backend.close()
The basicRequest starting point provides methods for all HTTP verbs. URI interpolation with uri"" handles encoding automatically.
Asynchronous Requests with Future Backend
Production applications require non-blocking operations. The Future backend integrates with Scala’s standard async primitives:
import sttp.client3._
import sttp.client3.akkahttp.AkkaHttpBackend
import akka.actor.ActorSystem
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration._
import scala.util.{Success, Failure}
implicit val system: ActorSystem = ActorSystem()
implicit val ec: ExecutionContext = system.dispatcher
val backend = AkkaHttpBackend()
val request = basicRequest
.get(uri"https://jsonplaceholder.typicode.com/posts/1")
.readTimeout(5.seconds)
val responseFuture: Future[Response[Either[String, String]]] =
request.send(backend)
responseFuture.onComplete {
case Success(response) =>
response.body match {
case Right(body) => println(s"Success: $body")
case Left(error) => println(s"Error: $error")
}
case Failure(exception) =>
println(s"Request failed: ${exception.getMessage}")
}
Response bodies arrive as Either[String, String] where Left contains error responses and Right holds successful bodies.
JSON Serialization with Circe
Manual JSON parsing creates maintenance burden. Integrate circe for automatic serialization:
import sttp.client3._
import sttp.client3.circe._
import io.circe.generic.auto._
import io.circe.parser._
case class User(id: Int, name: String, email: String)
case class CreateUserRequest(name: String, email: String)
val backend = HttpURLConnectionBackend()
// Deserializing responses
val getUser = basicRequest
.get(uri"https://api.example.com/users/1")
.response(asJson[User])
.send(backend)
getUser.body match {
case Right(user) => println(s"User: ${user.name}")
case Left(error) => println(s"Failed: $error")
}
// Serializing request bodies
val newUser = CreateUserRequest("Alice", "alice@example.com")
val createUser = basicRequest
.post(uri"https://api.example.com/users")
.body(newUser)
.response(asJson[User])
.send(backend)
The asJson[T] response handler automatically deserializes JSON into case classes. Request bodies serialize automatically when passed to .body().
Akka HTTP Client Basics
Akka HTTP’s client API builds on Akka Streams. Add the dependency:
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.5.3"
The connection pool API handles request-response cycles efficiently:
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.stream.Materializer
import scala.concurrent.{Future, ExecutionContext}
implicit val system: ActorSystem = ActorSystem()
implicit val ec: ExecutionContext = system.dispatcher
val responseFuture: Future[HttpResponse] =
Http().singleRequest(
HttpRequest(
method = HttpMethods.GET,
uri = "https://api.github.com/users/scala"
)
)
responseFuture.flatMap { response =>
response.entity.dataBytes
.runFold(ByteString(""))(_ ++ _)
.map(_.utf8String)
}.foreach(println)
singleRequest uses a shared connection pool. The response entity streams data through Akka Streams, requiring materialization.
Advanced Akka HTTP: Headers and Authentication
Production APIs require authentication and custom headers:
import akka.http.scaladsl.model.headers._
val authenticatedRequest = HttpRequest(
method = HttpMethods.GET,
uri = "https://api.example.com/protected",
headers = List(
Authorization(OAuth2BearerToken("your-token-here")),
RawHeader("X-Custom-Header", "value")
)
)
Http().singleRequest(authenticatedRequest)
For JSON handling, integrate spray-json or circe:
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._
import spray.json._
case class Post(userId: Int, id: Int, title: String, body: String)
implicit val postFormat: RootJsonFormat[Post] = jsonFormat4(Post)
val postData = Post(1, 0, "New Post", "Content here")
val postRequest = HttpRequest(
method = HttpMethods.POST,
uri = "https://jsonplaceholder.typicode.com/posts",
entity = HttpEntity(
ContentTypes.`application/json`,
postData.toJson.compactPrint
)
)
Http().singleRequest(postRequest).flatMap { response =>
Unmarshal(response.entity).to[Post]
}.foreach(println)
Connection Pooling and Performance
Akka HTTP automatically pools connections. Configure pool settings for high-throughput scenarios:
import akka.http.scaladsl.settings.ConnectionPoolSettings
val poolSettings = ConnectionPoolSettings(system)
.withMaxConnections(50)
.withMaxOpenRequests(256)
val request = HttpRequest(uri = "https://api.example.com/data")
Http().singleRequest(
request,
settings = poolSettings
)
For multiple requests to the same host, use the host-level API:
import akka.stream.scaladsl._
val connectionFlow = Http().cachedHostConnectionPool[Int](
host = "api.example.com"
)
val requests = (1 to 100).map { id =>
(HttpRequest(uri = s"/items/$id"), id)
}
Source(requests)
.via(connectionFlow)
.runForeach {
case (Success(response), id) =>
println(s"Request $id: ${response.status}")
response.discardEntityBytes()
case (Failure(ex), id) =>
println(s"Request $id failed: ${ex.getMessage}")
}
Error Handling and Retries
Robust clients handle transient failures. Implement retry logic with sttp:
import sttp.client3._
import sttp.client3.monad.MonadError
import scala.concurrent.duration._
def withRetry[T](
request: Request[T, Any],
backend: SttpBackend[Future, Any],
maxRetries: Int = 3
)(implicit ec: ExecutionContext): Future[Response[T]] = {
def attempt(retriesLeft: Int): Future[Response[T]] = {
request.send(backend).flatMap { response =>
if (response.code.isSuccess || retriesLeft == 0) {
Future.successful(response)
} else {
akka.pattern.after(1.second)(attempt(retriesLeft - 1))
}
}
}
attempt(maxRetries)
}
For Akka HTTP, implement circuit breaker patterns:
import akka.pattern.CircuitBreaker
import scala.concurrent.duration._
val breaker = new CircuitBreaker(
system.scheduler,
maxFailures = 5,
callTimeout = 10.seconds,
resetTimeout = 1.minute
)
def makeRequest(uri: String): Future[HttpResponse] = {
breaker.withCircuitBreaker(
Http().singleRequest(HttpRequest(uri = uri))
)
}
Both libraries provide production-ready HTTP clients. sttp’s backend abstraction suits applications requiring portability, while Akka HTTP integrates seamlessly with reactive Akka systems. Choose based on your architectural constraints and existing dependencies.