---
id: client
title: Client
---

`ZClient` is an HTTP client that enables us to make HTTP requests and handle responses in a purely functional manner. ZClient leverages the ZIO library's capabilities to provide a high-performance, asynchronous, and type-safe HTTP client solution.

## Key Features

**Purely Functional**: ZClient is built on top of the ZIO library, enabling a purely functional approach to handling HTTP requests and responses. This ensures referential transparency and composability, making it easy to build and reason about complex HTTP interactions.

**Type-Safe**: ZClient's API is designed to be type-safe, leveraging Scala's type system to catch errors at compile time and provide a seamless development experience. This helps prevent common runtime errors and enables developers to write robust and reliable HTTP client code.

**Asynchronous & Non-blocking**: ZClient is fully asynchronous and non-blocking, allowing us to perform multiple HTTP requests concurrently without blocking threads. This ensures optimal resource utilization and scalability, making it suitable for high-performance applications.

**Middleware Support**: ZClient provides support for middleware, allowing us to customize and extend its behavior to suit our specific requirements. We can easily plug in middleware to add functionalities such as logging, debugging, caching, and more.

**Flexible Configuration**: ZClient offers flexible configuration options, allowing us to fine-tune its behavior according to our needs. We can configure settings such as SSL, proxy, connection pooling, timeouts, and more to optimize the client's performance and behavior.

**WebSocket Support**: In addition to traditional HTTP requests, ZClient also supports WebSocket communication, enabling bidirectional, full-duplex communication between client and server over a single, long-lived connection.

**SSL Support**: ZClient provides built-in support for SSL (Secure Sockets Layer) connections, allowing secure communication over the network. Users can configure SSL settings such as certificates, trust stores, and encryption protocols to ensure data confidentiality and integrity.

## Making HTTP Requests

We can think of a `ZClient` as a function that takes a `Request` and returns a `ZIO` effect that calls the server with the given request and returns the response that the server sends back.
Requests can be executed in 2 modes:
- `batched`: The entire body of the request is materialized in memory, and the connection lifecycle is managed automatically by the client.
- `streaming`: The body of the request _might be_ streaming, and the connection lifecycle is managed through the `Scope` in the effect's environment.

The `Client`'s companion object contains methods that reflect the 2 modes of request execution:

```scala
object Client {
  def batched(request: Request): ZIO[Client, Throwable, Response] = ???
  def streaming(request: Request): ZIO[Client & Scope, Throwable, Response] = ???
}
```

### "Streaming" Client

The `streaming` mode is the default mode for executing HTTP requests. It requires the `Client` and `Scope` environments to perform the request and handle the response. The `Client` environment is used to make the request, while the `Scope` environment is used to manage the lifecycle of resources such as connections, sockets, and other I/O-related resources that are acquired and released during the request-response operation.

When making a request in the `streaming` mode, we need to explicitly close the `Scope` once we've collected the response body:

```scala
import zio._
import zio.http._

// OK
val good =
  ZIO.scoped {
    Client
      .streaming(Request.get("http://jsonplaceholder.typicode.com/todos"))
      .flatMap(_.body.asString)
  }.flatMap(???)
  
// BAD: The server might be streaming the response body, and we've forcefully closed the connection before it finishes
val bad1 =
  ZIO.scoped {
    Client
      .streaming(Request.get("http://jsonplaceholder.typicode.com/todos"))
      .map(_.headers)
  }
    .flatMap(???)

// BAD: We're closing the scope before collecting the response body
val bad2 =
  ZIO.scoped {
      Client
        .streaming(Request.get("http://jsonplaceholder.typicode.com/todos"))
    }
    .flatMap(_.body.asString)
    .flatMap(???)

// VERY BAD: The connection will not be closed until the application exits, which will lead to resource leaks!
val bad3 =
  Client
    .streaming(Request.get("http://jsonplaceholder.typicode.com/todos"))
    .flatMap(_.body.asString)
    .flatMap(???)
    .provideSomeLayer[Client](Scope.default)
```

:::note
As a rule of thumb, you should **never** use `Scope.default` with Client!

To learn more about resource management and `Scope` in ZIO, refer to the [dedicated guide on this topic](https://zio.dev/reference/resource/scope) in the ZIO Core documentation.
:::

### "Batched" Client

Handling of `Scope` can quickly become cumbersome in cases where we simply want to execute an HTTP request and not handle the lifetime of the HTTP request.
The `batched` mode is simply a sub-implementation of the `streaming` mode where the `Scope` (i.e., connection lifecycle) is managed automatically.

Executing a request via the `batched` method can be done as simply as:

```scala
import zio._
import zio.http._

val good =
  Client
    .batched(Request.get("http://jsonplaceholder.typicode.com/todos"))
    .flatMap(_.body.asString)
    .flatMap(???)
```

::: warning
The `batched` methods will materialize the entire body of the request to memory.
Use this only when you don't need to stream the request body!
:::

We can similarly use the `batched` method on an instance of `Client` to return a new instance where all the methods will be executed in the `batched` mode. Below is a realistic example showcasing the usage of the `batched` client:

```scala
import zio._
import zio.http._
import zio.schema.DeriveSchema
import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec

case class Todo(
  userId: Int,
  id: Int,
  title: String,
  completed: Boolean,
)

object Todo {
  implicit val todoSchema = DeriveSchema.gen[Todo]
}

final class JsonPlaceHolderService(baseClient: Client) {
  private val client = baseClient.batched

  def todos(): ZIO[Any, Throwable, List[Todo]] =
    client
      .request(Request.get("http://jsonplaceholder.typicode.com/todos"))
      .flatMap(_.body.to[List[Todo]])
}
```

ZIO HTTP has several utility methods to create different types of requests, such as `Client#get`, `Client#post`, `Client#put`, `Client#delete`, etc:

| Method                               | Description                                                           |
|--------------------------------------|-----------------------------------------------------------------------|
| `def get(suffix: String)`            | Performs a GET request with the given path suffix.                    |
| `def head(suffix: String)`           | Performs a HEAD request with the given path suffix.                   |
| `def patch(suffix: String)`          | Performs a PATCH request with the given path suffix.                  |
| `def post(suffix: String)(body: In)` | Performs a POST request with the given path suffix and provided body. |
| `def put(suffix: String)(body: In)`  | Performs a PUT request with the given path suffix and provided body.  |
| `def delete(suffix: String)`         | Performs a DELETE request with the given path suffix.                 |

## Performing WebSocket Connections

We can also think of a client as a function that takes a `WebSocketApp` and returns a `ZIO` effect that performs the WebSocket operations and returns a response:

```scala
object ZClient {
  def socket[R](socketApp: WebSocketApp[R]): ZIO[R with Client & Scope, Throwable, Response] = ???
}
```

:::note
The `socket` method is not available on the "Batched" client!
:::

Here is a simple example of how to use the `ZClient#socket` method to perform a WebSocket connection:

```scala
import zio._
import zio.http._
import zio.http.ChannelEvent._

object WebSocketSimpleClient extends ZIOAppDefault {

  val url = "ws://ws.vi-server.org/mirror"

  val socketApp: WebSocketApp[Any] =
    Handler

      // Listen for all websocket channel events
      .webSocket { channel =>
        channel.receiveAll {

          // Send a "foo" message to the server once the connection is established
          case UserEventTriggered(UserEvent.HandshakeComplete) =>
            channel.send(Read(WebSocketFrame.text("foo"))) *>
              ZIO.debug("Connection established and the foo message sent to the server")

          // Send a "bar" if the server sends a "foo"
          case Read(WebSocketFrame.Text("foo")) =>
            channel.send(Read(WebSocketFrame.text("bar"))) *>
              ZIO.debug("Received the foo message from the server and the bar message sent to the server")

          // Close the connection if the server sends a "bar"
          case Read(WebSocketFrame.Text("bar")) =>
            ZIO.debug("Received the bar message from the server and Goodbye!") *>
              channel.send(Read(WebSocketFrame.close(1000)))

          case _ =>
            ZIO.unit
        }
      }

  val app: ZIO[Client, Throwable, Unit] =
    for {
      url    <- ZIO.fromEither(URL.decode("ws://ws.vi-server.org/mirror"))
      client <- ZIO.serviceWith[Client](_.url(url))
      _      <- ZIO.scoped(client.socket(socketApp) *> ZIO.never)
    } yield ()

  val run: ZIO[Any, Throwable, Any] =
    app.provide(Client.default)

}
```

In the above example, we defined a WebSocket client that connects to a mirror server and sends and receives messages. When the connection is established, it receives the `UserEvent.HandshakeComplete` event and then it sends a "foo" message to the server. Consequently, the server sends a "foo" message, and the client responds with a "bar" message. Finally, the server sends a "bar" message, and the client closes the connection.

## Configuring Headers

By default, the client adds the `User-Agent` header to all requests. Additionally, as the `ZClient` extends the `HeaderOps` trait, we have access to all operations that can be performed on headers inside the client.

For example, to add a custom header we can use the `Client#addHeader` method:

```scala
import zio._
import zio.http._
import zio.http.Header.Authorization

val program = for {
  client <- ZIO.serviceWith[Client](_.addHeader(Authorization.Bearer(token = "dummyBearerToken")))
  res    <- client.request(Request.get("http://localhost:8080/users"))
} yield ()
```

:::note
To learn more about headers and how they work, check out our dedicated section called [Header Operations](headers/headers.md#headers-operations) on the headers page.
:::

## Composable URLs

In ZIO HTTP, URLs are composable. This means that if we have two URLs, we can combine them to create a new URL. This is useful when we want to prevent duplication of the base URL in our code. For example, assume we have a base URL `http://localhost:8080` and we want to make several requests to different endpoints and query parameters under this base URL. We can configure the client with this URL using the `Client#url` and then every request will be made can be relative to this base URL:

```scala
import zio._
import zio.http._
import zio.schema.DeriveSchema
import zio.schema.codec.JsonCodec.schemaBasedBinaryCodec

case class User(name: String, age: Int)
object User {
  implicit val schema = DeriveSchema.gen[User]
}

val program: ZIO[Client, Throwable, Unit] =
  for {
    client <- ZIO.serviceWith[Client](_.url(url"http://localhost:8080").batched)
    _      <- client.post("/users")(Body.from(User("John", 42)))
    res    <- client.get("/users")
    _      <- client.delete("/users/1")
    _      <- res.body.asString.debug
  } yield ()
```

The following methods are available for setting the base URL:

| Method Signature                | Description                                    |
|---------------------------------|------------------------------------------------|
| `Client#url(url: URL)`          | Sets the URL directly.                         |
| `Client#uri(uri: URI)`          | Sets the URL from the provided URI.            |
| `Client#path(path: String)`     | Sets the path of the URL from a string.        |
| `Client#path(path: Path)`       | Sets the path of the URL from a `Path` object. |
| `Client#port(port: Int)`        | Sets the port of the URL.                      |
| `Client#scheme(scheme: Scheme)` | Sets the scheme (protocol) for the URL.        |

The `Scheme` is a sealed trait that represents the different schemes (protocols) that can be used in a request. The available schemes are `HTTP` and `HTTPS` for HTTP requests, and `WS` and `WSS` for WebSockets.

Here is the list of methods that are available for adding URL, Path, and QueryParams to the client's configuration:

| Methods                                            | Description                                                            |
|----------------------------------------------------|------------------------------------------------------------------------|
| `Client#addUrl(url: URL)`                          | Adds another URL to the existing one.                                  |
| `Client#addPath(path: String)`                     | Adds a path segment to the URL.                                        |
| `Client#addPath(path: Path)`                       | Adds a path segment from a `Path` object to the URL.                   |
| `Client#addLeadingSlash`                           | Adds a leading slash to the URL path.                                  |
| `Client#addTrailingSlash`                          | Adds a trailing slash to the URL path.                                 |
| `Client#addQueryParam(key: String, value: String)` | Adds a query parameter with the specified key-value pair to the URL.   |
| `Client#addQueryParams(params: QueryParams)`       | Adds multiple query parameters to the URL from a `QueryParams` object. |

## Client Aspects/Middlewares

Client aspects are a powerful feature of ZIO HTTP, enabling us to intercept, modify, and extend client behavior. The `ZClientAspect` is represented as a function that takes a `ZClient` and returns a new `ZClient` with customized behavior. We apply aspects to a client using the `ZClient#@@` method, allowing modification of various execution aspects such as metrics, tracing, encoding, decoding, and debugging.

### Debugging Aspects

To debug the client, we can use the `ZClientAspect.debug` aspect, which logs the request details to the console. This is useful for debugging and troubleshooting client interactions, as it provides visibility into the low-level details of the HTTP requests and responses:

```scala
import zio._
import zio.http._

object ClientWithDebugAspect extends ZIOAppDefault {
  val program =
    for {
      client <- ZIO.service[Client].map(_ @@ ZClientAspect.debug)
      _      <- client.batched(Request.get("http://jsonplaceholder.typicode.com/todos"))
    } yield ()

  override val run = program.provide(Client.default)
}
```

The `ZClientAspect.debug` also takes a partial function from `Response` to `String`, which enables us to customize the logging output based on the response. This is useful for logging specific details from the response, such as status code, headers, and body:

```scala
val debugResponse = ZClientAspect.debug { case res: Response => res.headers.mkString("\n") }

val program =
  for {
    client <- ZIO.service[Client].map(_ @@ debugResponse)
    _      <- client.request(Request.get("http://jsonplaceholder.typicode.com/todos"))
  } yield ()
```

### Logging Aspects

To log the client interactions, we can use the `ZClientAspect.requestLogging` which logs the request details such as method, duration, url, user-agent, status code and request size.

Let's try an example:

```scala
import zio._
import zio.http._

val loggingAspect =
  ZClientAspect.requestLogging(
    loggedRequestHeaders = Set(Header.UserAgent),
    logResponseBody = true,
  )

val program =
  for {
    client <- ZIO.service[Client].map(_ @@ loggingAspect)
    _      <- client.request(Request.get("http://jsonplaceholder.typicode.com/todos"))
  } yield ()
```

### Follow Redirects

To follow redirects, we can apply the `ZClientAspect.followRedirects` aspect, which takes the maximum number of redirects to follow and a callback function that allows us to customize the behavior when a redirect is encountered:

```scala
import zio._
import zio.http._

val followRedirects = ZClientAspect.followRedirects(3)((resp, message) => ZIO.logInfo(message).as(resp))

for {
  client   <- ZIO.service[Client].map(_ @@ followRedirects)
  response <- client.request(Request.get("http://google.com"))
  _        <- response.body.asString.debug
} yield ()
```

## Configuring ZIO HTTP Client

The ZIO HTTP Client provides a flexible configuration mechanism through the `ZClient.Config` class. This class allows us to customize various aspects of the HTTP client, including SSL settings, proxy configuration, connection pool size, timeouts, and more. The `ZClient.Config.default` provides a default configuration that can be customized using `copy` method or by using the utility methods provided by the `ZClient.Config` class.

Let's take a look at the available configuration options:

- **SSL Configuration**: Allows us to specify SSL settings for secure connections.
- **Proxy Configuration**: Enables us to configure a proxy server for outgoing HTTP requests.
- **Connection Pool Configuration**: Defines the size of the connection pool.
- **Max Initial Line Length**: Sets the maximum length of the initial line in an HTTP request or response. The default is set to 4096 characters.
- **Max Header Size**: Specifies the maximum size of HTTP headers in bytes. The default is set to 8192 bytes.
- **Request Decompression**: Specifies whether the client should decompress the response body if it's compressed.
- **Local Address**: Specifies the local network interface or address to use for outgoing connections. It's set to None, indicating that the client will use the default local address.
- **Add User-Agent Header**: Indicates whether the client should automatically add a User-Agent header to outgoing requests. It's set to true in the default configuration.
- **WebSocket Configuration**: Configures settings specific to WebSocket connections. In this example, the default WebSocket configuration is used.
- **Idle Timeout**: Specifies the maximum idle time for persistent connections in seconds. The default is set to 50 seconds.
- **Connection Timeout**: Specifies the maximum time to wait for establishing a connection in seconds. By default, the client has no connection timeout.

Here are some of the above configuration options in more detail:

### Configuring SSL

The default SSL configuration of `ZClient.Config.default` is `None`. To enable and configure SSL for the client, we can use the `ZClient.Config#ssl` method. This method takes a config of type `ClientSSLConfig` which supports different SSL configurations such as `Default`, `FromCertFile`, `FromCertResource`, `FromTrustStoreFile`, and `FromTrustStoreResource.

Let's see an example of how to configure SSL for the client:

```scala title="zio-http-example/src/main/scala/example/HttpsClient.scala" 
package example

import zio._

import zio.http._
import zio.http.netty.NettyConfig
import zio.http.netty.client.NettyClientDriver

object HttpsClient extends ZIOAppDefault {
  val url     = URL.decode("https://jsonplaceholder.typicode.com/todos/1").toOption.get
  val headers = Headers(Header.Host("jsonplaceholder.typicode.com"))

  val sslConfig = ClientSSLConfig.FromTrustStoreResource(
    trustStorePath = "truststore.jks",
    trustStorePassword = "changeit",
  )

  val clientConfig = ZClient.Config.default.ssl(sslConfig)

  val program = for {
    data <- ZClient.batched(Request.get(url).addHeaders(headers))
    _    <- Console.printLine(data)
  } yield ()

  val run =
    program.provide(
      ZLayer.succeed(clientConfig),
      Client.customized,
      NettyClientDriver.live,
      DnsResolver.default,
      ZLayer.succeed(NettyConfig.default),
    )

}
```

### Configuring Proxy

To configure a proxy for the client, we can use the `Client#proxy` method. This method takes a `Proxy` and updates the client's configuration to use the specified proxy for all requests:

```scala
import zio._
import zio.http._

val program = for {
  proxyUrl <- ZIO.fromEither(URL.decode("http://localhost:8123"))
  client   <- ZIO.serviceWith[Client](_.proxy(Proxy(url = proxyUrl)))
  res      <- client.request(Request.get("https://jsonplaceholder.typicode.com/todos"))
} yield ()
```

### Connection Pooling

Connection pooling is a crucial mechanism in ZIO HTTP for optimizing the management of HTTP connections. By default, ZIO HTTP uses a fixed-size connection pool with a capacity of 10 connections. This means that the client can maintain up to 10 idle connections to the server for reuse. When the client makes a request, it checks the connection pool for an available connection to the server. If a connection is available, it reuses it for the request. If no connection is available, it creates a new connection and adds it to the pool.

To configure the connection pool, we have to update the `ZClient.Config#connectionPool` field with the preferred configuration. The `ConnectionPoolConfig` trait serves as a base trait for different connection pool configurations. It is a sealed trait with five different implementations:

- `Disabled`: Indicates that connection pooling is disabled.
- `Fixed`: Takes a single parameter, `size`, which specifies a fixed size connection pool.
- `FixedPerHost`: Takes a map of `URL.Location.Absolute` to `Fixed` to specify a fixed size connection pool per host.
- `Dynamic`: Takes three parameters, `minimum`, `maximum`, and `ttl`, to configure a dynamic connection pool with minimum and maximum sizes and a time-to-live (TTL) duration.
- `DynamicPerHost`: Similar to Dynamic, but with configurations per host.

Also the `ZClient.Config` has some utility methods to update the connection pool configuration, e.g. `ZClient.Config#fixedConnectionPool` and `ZClient.Config#dynamicConnectionPool`. Let's see an example of how to configure the connection pool:

```scala title="zio-http-example/src/main/scala/example/ClientWithConnectionPooling.scala" 
package example

import zio._

import zio.http._
import zio.http.netty.NettyConfig

object ClientWithConnectionPooling extends ZIOAppDefault {
  val program = for {
    url    <- ZIO.fromEither(URL.decode("http://jsonplaceholder.typicode.com/posts"))
    client <- ZIO.serviceWith[Client](_.addUrl(url))
    _      <- ZIO.foreachParDiscard(Chunk.fromIterable(1 to 100)) { i =>
      client.batched(Request.get(i.toString)).flatMap(_.body.asString).debug
    }
  } yield ()

  val config = ZClient.Config.default.dynamicConnectionPool(10, 20, 5.second)

  override val run =
    program.provide(
      ZLayer.succeed(config),
      Client.live,
      ZLayer.succeed(NettyConfig.default),
      DnsResolver.default,
    )
}
```

### Enabling Response Decompression

When making HTTP requests using a client, such as a web browser or a custom HTTP client, it's essential to optimize data transfer for efficiency and performance.

By default, most HTTP clients do not advertise compression support when making requests to web servers. However, servers often compress response bodies when they detect that the client supports compression. To enable response compression, we need to add the `Accept-Encoding` header to our HTTP requests. The `Accept-Encoding` header specifies the compression algorithms supported by the client. Common values include `gzip` and `deflate`. When a server receives a request with the `Accept-Encoding` header, it may compress the response body using one of the specified algorithms.

Here's an example of an HTTP request with the Accept-Encoding header:

```http
GET https://example.com/
Accept-Encoding: gzip, deflate
```

When a server responds with a compressed body, it includes the Content-Encoding header to specify the compression algorithm used. The client then needs to decompress the body before processing its contents.

For example, a compressed response might look like this:

```http
200 OK
content-encoding: gzip
content-type: application/json; charset=utf-8

<compressed-body>
```

To decompress the response body with `ZClient`, we need to enable response decompression by using the `ZClient.Config#requestDecompression` method:

```scala title="zio-http-example/src/main/scala/example/ClientWithDecompression.scala" 
package example

import zio._

import zio.http.Header.AcceptEncoding
import zio.http._
import zio.http.netty.NettyConfig

object ClientWithDecompression extends ZIOAppDefault {

  val program = for {
    url    <- ZIO.fromEither(URL.decode("https://jsonplaceholder.typicode.com"))
    client <- ZIO.serviceWith[Client](_.addUrl(url))
    res    <-
      client
        .addHeader(AcceptEncoding(AcceptEncoding.GZip(), AcceptEncoding.Deflate()))
        .batched(Request.get("/todos"))
    data   <- res.body.asString
    _      <- Console.printLine(data)
  } yield ()

  val config       = ZClient.Config.default.requestDecompression(true)
  override val run =
    program.provide(
      ZLayer.succeed(config),
      Client.live,
      ZLayer.succeed(NettyConfig.default),
      DnsResolver.default,
    )

}
```

## Customizing `ClientDriver` and `DnsResolver`

Rather than utilizing the default layer, `Client.default`, we have the option to employ the `Client.customized` layer. This layer requires `ClientDriver`, `DnsResolver`, and the `Client.Config` layers:

```scala
object Client {
  val customized: ZLayer[Config with ClientDriver with DnsResolver, Throwable, Client] = ???
}
```

This empowers us to interchange the client driver with alternatives beyond the default Netty driver or to customize it to our specific requirements. Also, we can customize the DNS resolver to use a different DNS resolution mechanism.

## Examples

### Simple Client Example

```scala title="zio-http-example/src/main/scala/example/SimpleClient.scala" 
package example

import zio._

import zio.http._

object SimpleClient extends ZIOAppDefault {
  val url = URL.decode("https://jsonplaceholder.typicode.com/todos").toOption.get

  val program = for {
    client <- ZIO.service[Client]
    res    <- client.url(url).batched(Request.get("/"))
    data   <- res.body.asString
    _      <- Console.printLine(data)
  } yield ()

  override val run = program.provide(Client.default)

}
```

### ClientServer Example

```scala title="zio-http-example/src/main/scala/example/ClientServer.scala" 
package example

import zio.ZIOAppDefault

import zio.http._

object ClientServer extends ZIOAppDefault {
  val url = URL.decode("http://localhost:8080/hello").toOption.get

  val app = Routes(
    Method.GET / "hello" -> handler(Response.text("hello")),
    Method.GET / ""      -> handler(ZClient.batched(Request.get(url))),
  ).sandbox

  val run =
    Server.serve(app).provide(Server.default, Client.default).exitCode
}
```

### Authentication Client Example

This example code demonstrates accessing a protected route in an [authentication server](https://github.com/zio/zio-http/blob/main/zio-http-example/src/main/scala/example/AuthenticationClient.scala) by first obtaining a JWT token through a login request and then using that token to access the protected route:

```scala title="zio-http-example/src/main/scala/example/AuthenticationClient.scala" 
package example

import zio._

import zio.http._

object AuthenticationClient extends ZIOAppDefault {

  /**
   * This example is trying to access a protected route in AuthenticationServer
   * by first making a login request to obtain a jwt token and use it to access
   * a protected route. Run AuthenticationServer before running this example.
   */
  val url = "http://localhost:8080"

  val loginUrl = URL.decode(s"${url}/login").toOption.get
  val greetUrl = URL.decode(s"${url}/profile/me").toOption.get

  val program = for {
    client   <- ZIO.service[Client]
    // Making a login request to obtain the jwt token. In this example the password should be the reverse string of username.
    token    <- client
      .batched(
        Request
          .get(loginUrl)
          .withBody(
            Body.fromMultipartForm(
              Form(
                FormField.simpleField("username", "John"),
                FormField.simpleField("password", "nhoJ"),
              ),
              Boundary("boundary123"),
            ),
          ),
      )
      .flatMap(_.body.asString)
    // Once the jwt token is procured, adding it as a Bearer token in Authorization header while accessing a protected route.
    response <- client.batched(Request.get(greetUrl).addHeader(Header.Authorization.Bearer(token)))
    body     <- response.body.asString
    _        <- Console.printLine(body)
  } yield ()

  override val run = program.provide(Client.default)

}
```

### Reconnecting WebSocket Client Example

This example represents a WebSocket client application that automatically attempts to reconnect upon encountering errors or disconnections. It uses the `Promise` to notify about WebSocket errors:

```scala title="zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala" 
package example.websocket

import zio._

import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTriggered}
import zio.http._

object WebSocketReconnectingClient extends ZIOAppDefault {

  val url = "ws://ws.vi-server.org/mirror"

  // A promise is used to be able to notify application about websocket errors
  def makeSocketApp(p: Promise[Nothing, Throwable]): WebSocketApp[Any] =
    Handler

      // Listen for all websocket channel events
      .webSocket { channel =>
        channel.receiveAll {

          // On connect send a "foo" message to the server to start the echo loop
          case UserEventTriggered(UserEvent.HandshakeComplete) =>
            channel.send(ChannelEvent.Read(WebSocketFrame.text("foo")))

          // On receiving "foo", we'll reply with another "foo" to keep echo loop going
          case Read(WebSocketFrame.Text("foo")) =>
            ZIO.logInfo("Received foo message.") *>
              ZIO.sleep(1.second) *>
              channel.send(ChannelEvent.Read(WebSocketFrame.text("foo")))

          // Handle exception and convert it to failure to signal the shutdown of the socket connection via the promise
          case ExceptionCaught(t) =>
            ZIO.fail(t)

          case _ =>
            ZIO.unit
        }
      }.tapErrorZIO { f =>
        // signal failure to application
        p.succeed(f)
      }

  val app: ZIO[Client & Scope, Throwable, Unit] = {
    (for {
      p <- zio.Promise.make[Nothing, Throwable]
      _ <- makeSocketApp(p).connect(url).catchAll { t =>
        // convert a failed connection attempt to an error to trigger a reconnect
        p.succeed(t)
      }
      f <- p.await
      _ <- ZIO.logError(s"App failed: $f")
      _ <- ZIO.logError(s"Trying to reconnect...")
      _ <- ZIO.sleep(1.seconds)
    } yield {
      ()
    }) *> app
  }

  val run =
    ZIO.scoped(app).provide(Client.default)

}
```
