sttp tips and tricks

(, en)

Retry stuff

First check sttp resilience and probably softwaremill/retry and then perhaps Retrying function calls in Scala.

Based on a post on SO, proposing a simple retry function

// src/main/scala/FuncUtil.scala
@annotation.tailrec
def retry[T](n: Int)(fn: => T): Try[T] = {
  Try(fn) match {
    case Failure(_) if n > 1 => retry(n - 1)(fn)
    case fn                  => fn
  }
}

you can build something like

// src/main/scala/Sttp3Client.scala
@annotation.tailrec
private def handleRequest[Result](
    nRetries: Int = 0,
    shouldRetry: HttpError[String] => Boolean = _ => true
)(
    fn: RequestT[Empty, Either[String, String], Any] => Try[
      Response[Either[ResponseException[String, Nothing], Result]]
    ]
): Try[Result] = {
  fn(basicRequest).map(_.body) match {
    // request was successful, but the http status code indicates error
    // retry route
    case Success(Left(err: HttpError[String])) if shouldRetry(err) && nRetries > 0 =>
      handleRequest(nRetries = nRetries - 1, shouldRetry = shouldRetry)(fn)
    // everything worked, yay!
    case Success(Right(data)) =>
      Success(data)
    // request was successful, but the http status code indicates error
    // retries exhausted route
    case Success(Left(HttpError(body, code))) =>
      Failure(
        HttpResponseException(
          s"""Failed HTTP Response with code ${code}. Returned body: "${body}"""",
          code
        )
      )
    // this should not happen, because we do not deserialise json
    // which means we can only get HttpErrors
    case Success(Left(ex: ResponseException[_, _])) => Failure(ex)
    // we have already an exception, just pass it on
    // this is more serious, so no retries here
    case Failure(ex) => Failure(ex)
  }
}

which you can use in this way

// src/main/scala/Sttp3Client.scala
handleRequest(nRetries = 1, shouldRetry = shouldRetryRequest) { req =>
  Try {
    req
      .get(uri"https://www.bargsten.org/lsjkdaf")
      .response(asEmptyResponse)
      .send(backend)
  }
}
// src/main/scala/Sttp3Client.scala
def shouldRetryRequest(error: HttpError[String]): Boolean = {
  error.statusCode match {
    case StatusCode.NotFound => true
    case _                   => false
  }
}

If you are not interested in the return body, but still want to deal with errors (including body), you cannot use the ignore function supplied by sttp. Instead you have to construct your own response description:

// src/main/scala/Sttp3Client.scala
def asEmptyResponse: ResponseAs[Either[ResponseException[String, Nothing], Unit], Any] =
  asEither(asStringAlways.mapWithMetadata((body, meta) => HttpError(body, meta.code)), ignore)

It returns Unit on success, but returns the body and status code as HttpError on failure.