moia-oss / scala-pekko-http-client   1.0.0

Apache License 2.0 GitHub

Extends the pekko-http client with retry logic, error handling, logging and signing

Scala versions: 2.13 2.12

Scala-Pekko-HTTP-Client

This is a wrapper around the pekko-http-client that adds

  • handling for domain errors as HTTP 400 returns
  • retry logic
  • deadlines
  • error handling
  • logging
  • AWS request signing

Build & Test Scala 2.13 Scala Steward badge

Usage

libraryDependencies += "io.moia" %% "scala-pekko-http-client" % "1.0.0"
// create the client val httpClient = new HttpClient( config = HttpClientConfig("http", "127.0.0.1", 8888), name = "TestClient", httpMetrics = HttpMetrics.none, retryConfig = RetryConfig.default, clock = Clock.systemUTC(), awsRequestSigner = None ) // make a request val response: Future[HttpClientResponse] = httpClient.request( method = HttpMethods.POST, entity = HttpEntity("Example"), path = "/test", headers = Seq.empty, deadline = Deadline.now + 10.seconds ) // map the response to your model response.flatMap { case HttpClientSuccess(content) => Unmarshal(content).to[MySuccessObject].map(Right(_)) case DomainError(content) => Unmarshal(content).to[DomainErrorObject].map(Left(_)) case failure: HttpClientFailure => throw GatewayException(failure.toString) }

See SimpleExample.scala for a complete example.

HttpClientResponses

The lib outputs the following response objects (see io.moia.scalaHttpClient.HttpClientResponse):

  • HTTP 2xx Success => HttpClientSuccess
  • HTTP 3xx Redirect => not implemented yet
  • HTTP 400 Bad Request with entity => is mapped to DomainError ⚠️
  • HTTP 400 Bad Request without entity => HttpClientError
  • HTTP 4xx, 5xx, others => HttpClientError
  • if the deadline expired => DeadlineExpired
  • if an AwsRequestSigner is given, but the request already includes an "Authorization" header => AlreadyAuthorizedException
  • weird pekko-errors => ExceptionOccurred

Custom Logging

To use a custom logger (for correlation ids etc), you can use the typed LoggingHttpClient. First create a custom LoggerTakingImplicit:

import com.typesafe.scalalogging._ import org.slf4j.LoggerFactory object CustomLogging { final case class LoggingContext(context: String) implicit val canLogString: CanLog[LoggingContext] = new CanLog[LoggingContext] { override def logMessage(originalMsg: String, ctx: LoggingContext): String = ??? override def afterLog(ctx: LoggingContext): Unit = ??? } val theLogger: LoggerTakingImplicit[LoggingContext] = Logger.takingImplicit(LoggerFactory.getLogger(getClass.getName)) }

Then create a LoggingHttpClient typed to the LoggingContext:

// create the client val httpClient = new LoggingHttpClient[LoggingContext]( config = HttpClientConfig("http", "127.0.0.1", 8888), name = "TestClient", httpMetrics = HttpMetrics.none[LoggingContext], retryConfig = RetryConfig.default, clock = Clock.systemUTC(), logger = CustomLogging.theLogger, awsRequestSigner = None ) // create an implicit logging context implicit val ctx: LoggingContext = LoggingContext("Logging Context") // make a request httpClient.request(HttpMethods.POST, HttpEntity.Empty, "/test", Seq.empty, Deadline.now + 10.seconds)

The request function will use the ctx implicitly.

See LoggingExample.scala for a complete example.

Custom Headers

To use custom-defined headers, you can extend ModeledCustomHeader from org.apache.pekko.http.scaladsl.model.headers:

import org.apache.pekko.http.scaladsl.model.headers.{ModeledCustomHeader, ModeledCustomHeaderCompanion} import scala.util.Try final class CustomHeader(id: String) extends ModeledCustomHeader[CustomHeader] { override def renderInRequests(): Boolean = true override def renderInResponses(): Boolean = true override def companion: ModeledCustomHeaderCompanion[CustomHeader] = CustomHeader override def value(): String = id } object CustomHeader extends ModeledCustomHeaderCompanion[CustomHeader] { override def name: String = "custom-header" override def parse(value: String): Try[CustomHeader] = Try(new CustomHeader(value)) }

Then simply send them in the request:

val response: Future[HttpClientResponse] = httpClient.request( method = HttpMethods.POST, entity = HttpEntity("Example"), path = "/test", headers = Seq(new CustomHeader("foobar")), deadline = Deadline.now + 10.seconds )

Note: If you want to access the headers from the response, you can do so from the data inside the HttpClientSuccess:

case HttpClientSuccess(content) => content.headers

See HeaderExample.scala for a complete example.

Publishing

Tag the new version (e.g. v1.0.0) and push the tags (git push origin --tags).

You need a public GPG key with your MOIA email and an account on https://oss.sonatype.org that can access the io.moia group.

Add your credentials to ~/.sbt/sonatype_credential and run

sbt:scala-pekko-http-client> +publishSigned

Then close and release the repository.

sbt:scala-pekko-http-client> +sonatypeRelease 

Afterwards, add the release to GitHub.