ZIO 2 Example: Making an HTTP GET request with a timeout, using sttp client

As a brief note today, here’s an example of making an HTTP GET request using ZIO 2 and the Scala sttp library. I also let the user specify a “timeout” value, so the request will timeout, rather than hanging.

As a very important note, this is a blocking approach, not a non-blocking approach.

Here’s the source code and Scala 3 + ZIO 2 + sttp function:

package com.alvinalexander.utils

import zio.*
import sttp.client3.*
import scala.concurrent.TimeoutException

object WebUtils:

    /**
     * NOTES:
     *
     *   - `attempt` returns `ZIO[Any, Throwable, A]`
     *   - `timeout` doesn’t make something an exception, it just returns a None
     *   - an empty `url` creates a `java.lang.IllegalArgumentException`
     *   - this is a blocking approach
     *
     * @see See [[https://zio.dev/reference/error-management/recovering/timing-out/]] 
     *      for timeout information.
     * @param url The URL to access.
     * @param timeoutInMs How long to wait before timing-out the request.
     * @return If everything goes well, the HTML body is in the `String`, otherwise
     *         the failure information is in the `Throwable`.
     */
    def getUrlContents(url: String, timeoutInMs: Int = 15_000): ZIO[Any, Throwable, String] =
        ZIO.attempt {
            val backend = HttpURLConnectionBackend()
            val response: Response[Either[String, String]] = basicRequest
                .get(uri"$url")
                .send(backend)
            // might be able to use `ZIO.fromEither(Right(body))`, 
            // but i want to control the exception text:
            response.body match
                case Left(body) => throw new Exception(s"Non-2xx response to GET with code ${response.code}")
                case Right(body) => body
        }.timeoutFail(new TimeoutException)(timeoutInMs.milliseconds)

One thing I do in this code is throw exceptions with the error messages I want. That way (a) I can work with those later, and (b) those exceptions end up in the ZIO “error channel,” i.e., the Throwable value. If you handle things as shown, the function’s return type signature is:

ZIO[Any, Throwable, String]

If you don’t handle the possible errors as shown, you can end up with other undesirable type signatures, something like these:

ZIO[Any, Any, Throwable |String]
ZIO[Any, Option[Throwable], String]