|
| 1 | +--- |
| 2 | +id: zio-environment |
| 3 | +title: "Request-scoped Context via ZIO Environment" |
| 4 | +sidebar_label: "ZIO Environment" |
| 5 | +--- |
| 6 | + |
| 7 | +ZIO HTTP provides request-scoped context through ZIO's Environment system, which offers type-safe dependency injection and context propagation. The primary mechanism is `[HandlerAspect](../aop/handler_aspect.md)` with output context (`CtxOut`), not a dedicated `[RequestStore](request-store.md)` API. This approach leverages ZIO's `R` type parameter to pass request-specific data through the middleware stack to handlers. |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +Request-scoped context in ZIO HTTP refers to data tied to the lifetime of a single HTTP request that needs to be accessible throughout the request processing pipeline. Common use cases include authentication tokens, user sessions, correlation IDs, and request metadata. ZIO HTTP solves this through `[HandlerAspect](../aop/handler_aspect.md)`, a specialized middleware type that produces typed context values accessible via the ZIO environment. Middleware extracts relevant context from requests and passes it through the `CtxOut` type parameter, which handlers access via `ZIO.service[T]` or `withContext`. |
| 12 | + |
| 13 | +The ZIO Environment approach differs fundamentally from the FiberRef-based pattern called [RequestStore](request-store.md). `HandlerAspect` provides compile-time type safety: the context requirement appears explicitly in handler type signatures, ensuring all dependencies are satisfied before the application compiles. This prevents entire classes of runtime errors where missing context would only be discovered during execution. |
| 14 | + |
| 15 | +## HandlerAspect |
| 16 | + |
| 17 | +`HandlerAspect` is ZIO HTTP's middleware abstraction that can produce typed context values. Its type signature reveals the key insight: |
| 18 | + |
| 19 | +```scala |
| 20 | +final case class HandlerAspect[-Env, +CtxOut]( |
| 21 | + protocol: ProtocolStack[Env, Request, (Request, CtxOut), Response, Response] |
| 22 | +) extends Middleware[Env] |
| 23 | +``` |
| 24 | + |
| 25 | +**The CtxOut type parameter** represents the context produced by middleware. When middleware processes a request, it returns a tuple `(Request, CtxOut)` where CtxOut contains the extracted context. This context then flows through the middleware stack and becomes available to handlers via ZIO's service pattern. |
| 26 | + |
| 27 | +When you compose multiple `HandlerAspects`, their contexts combine as tuples: `HandlerAspect[Env, A] ++ HandlerAspect[Env, B]` produces `HandlerAspect[Env, (A, B)]`. This compositional approach allows building complex context from simple middleware components. |
| 28 | + |
| 29 | +Please note that the ZIO Environment (the R in `ZIO[R, E, A]`) tracks dependencies at the type level. Every effect declares what **services** or **contexts** it requires to execute. ZIO HTTP extends this pattern through `HandlerAspect`, which provides a **bridge between HTTP middleware and the ZIO Environment system**. |
| 30 | + |
| 31 | + |
| 32 | +## Generating Context in HandlerAspect |
| 33 | + |
| 34 | +The `HandlerAspect.interceptIncomingHandler` API creates middleware that processes incoming requests and produces a context. The handler receives the Request and must return `(Request, CtxOut)` or fail with a Response: |
| 35 | + |
| 36 | +```scala |
| 37 | +def interceptIncomingHandler[Env, CtxOut]( |
| 38 | + handler: Handler[Env, Response, Request, (Request, CtxOut)] |
| 39 | +): HandlerAspect[Env, CtxOut] |
| 40 | +``` |
| 41 | + |
| 42 | +For example, the following middleware extracts an Authorization header, authenticates the user, and produces a `User` context. If authentication fails, it returns a 401 Unauthorized response: |
| 43 | + |
| 44 | +```scala mdoc:invisible |
| 45 | +case class User(name: String) |
| 46 | +trait UserService |
| 47 | +``` |
| 48 | + |
| 49 | +```scala mdoc:silent |
| 50 | +import zio._ |
| 51 | +import zio.http._ |
| 52 | + |
| 53 | +def authenticate(header: Header.Authorization): ZIO[UserService, Throwable, User] = ??? |
| 54 | + |
| 55 | +val auth: HandlerAspect[UserService, User] = |
| 56 | + HandlerAspect.interceptIncomingHandler { |
| 57 | + Handler.fromFunctionZIO[Request] { request => |
| 58 | + ZIO |
| 59 | + .fromOption(request.headers.get(Header.Authorization)) |
| 60 | + .orElseFail(Response.unauthorized("No Authorization header")) |
| 61 | + .flatMap(authenticate) |
| 62 | + .map(user => (request, user)) |
| 63 | + .orElseFail(Response.unauthorized("Invalid token")) |
| 64 | + } |
| 65 | + } |
| 66 | +``` |
| 67 | + |
| 68 | +This middleware has a type of `HandlerAspect[UserService, User]`, meaning it requires a `UserService` in the environment to perform authentication and produces a `User` context for downstream handlers. |
| 69 | + |
| 70 | +## Accessing Context in Handlers |
| 71 | + |
| 72 | +Using `ZIO.service` and its variants, handlers can access the context produced by middleware. The important note here is that `ZIO.service` can be used to access both the ZIO environment and the context produced by `HandlerAspect`: |
| 73 | + |
| 74 | +```scala mdoc:compile-only |
| 75 | +val greetRoute: Route[UserService, Nothing] = |
| 76 | + Method.GET / "greet" -> handler { (_: Request) => |
| 77 | + ZIO.serviceWith[User] { user => |
| 78 | + Response.text(s"Hello, $user!") |
| 79 | + } |
| 80 | + } @@ auth |
| 81 | +``` |
| 82 | + |
| 83 | +This handler is of type `Handler[User & UserService, Nothing, Request, Response]`, meaning it requires a `User` and `UserService` in the environment. Let's take a closer look at the type signature of `Handler`: |
| 84 | + |
| 85 | +```scala |
| 86 | +Handler[-R, +Err, -In, +Out] |
| 87 | +// R: Environment/context required |
| 88 | +// Err: Error type |
| 89 | +// In: Input type (typically Request) |
| 90 | +// Out: Output type (typically Response) |
| 91 | +``` |
| 92 | + |
| 93 | +The first type parameter `R` represents the environment or context required by the handler. This can be either a service that can be provided via ZLayer or a context produced by `HandlerAspect`. In this example, the `User` is a request-scoped context produced by the `auth` middleware, while `UserService` is a service that can be provided via ZLayer in upper layers. Therefore, the handler requires both `User` and `UserService` in its environment. |
| 94 | + |
| 95 | +Since the handler is wrapped with the `auth` middleware, it can access the `User` context produced by the `auth` middleware, which has a type of `HandlerAspect[UserService, User]`. By applying the middleware to the handler using the `@@` operator, the `User` context is provided to the handler, and so the handler type becomes `Handler[UserService, Nothing, Request, Response]`, meaning it only requires `UserService` from the environment. |
| 96 | + |
| 97 | +Instead of `ZIO.service`, we can use the helper method `withContext` to access the context: |
| 98 | + |
| 99 | +```scala mdoc:compile-only |
| 100 | +val greetRoute: Route[UserService, Nothing] = |
| 101 | + Method.GET / "greet" -> handler { (_: Request) => |
| 102 | + withContext { (user: User) => |
| 103 | + Response.text(s"Hello, $user!") |
| 104 | + } |
| 105 | + } @@ auth |
| 106 | +``` |
| 107 | + |
| 108 | +## Request Context Alongside Environmental Services Inside Handler |
| 109 | + |
| 110 | +A curious reader might wonder what happens if the handler also requires a service from ZLayer. How can we combine both request context and application services in the same handler? The answer is that we treat them the same way - both are part of the ZIO environment, but the difference is when and who provides them. So in previous examples, the `User` context is provided by the `auth` middleware, while the `UserService` will be provided later. The same applies to any other service that the handler might require. |
| 111 | + |
| 112 | +Let's see what happens when, other than the `auth` middleware, the handler also requires a service from the environment. For example, assume we have a `GreetingService` that generates personalized greetings based on the user information and the current time of day: |
| 113 | + |
| 114 | +```scala mdoc:invisible |
| 115 | +trait GreetingService { |
| 116 | + def greet(user: User): UIO[String] |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +Now we have to use the environment for `User`, `UserService`, and `GreetingService`. The `User` is a context produced by the `auth` middleware, while the `UserService` and `GreetingService` are services provided via ZLayer in upper layers. The handler can access both the `User` context and the `GreetingService` service using `ZIO.service`: |
| 121 | + |
| 122 | +```scala mdoc:compile-only |
| 123 | +val greetRoute: Route[UserService & GreetingService, Nothing] = |
| 124 | + Method.GET / "greet" -> |
| 125 | + handler(ZIO.service[GreetingService]).flatMap { greetingService => |
| 126 | + handler { |
| 127 | + ZIO.serviceWithZIO[User] { user => |
| 128 | + greetingService.greet(user).map(Response.text(_)) |
| 129 | + } |
| 130 | + } @@ auth |
| 131 | + } |
| 132 | +``` |
| 133 | + |
| 134 | +In this example, the handler has a type of `Handler[UserService & GreetingService, Response, Request, Response]`, meaning it requires both `UserService` and `GreetingService` from the environment. The `User` context is already provided by the `auth` middleware, while the provision of `UserService` and `GreetingService` is deferred to upper layers when serving the application. |
| 135 | + |
| 136 | +Again, we can simplify the handler using `withContext`: |
| 137 | + |
| 138 | +```scala mdoc:compile-only |
| 139 | +val greetRoute: Route[UserService & GreetingService, Nothing] = |
| 140 | + Method.GET / "greet" -> |
| 141 | + handler(ZIO.service[GreetingService]).flatMap { greetingService => |
| 142 | + handler { |
| 143 | + withContext { (user: User) => |
| 144 | + greetingService.greet(user).map(Response.text(_)) |
| 145 | + } |
| 146 | + } @@ auth |
| 147 | + } |
| 148 | +``` |
| 149 | + |
| 150 | +## Composing Multiple Contexts |
| 151 | + |
| 152 | +When multiple middleware components provide context, their contexts compose as tuples: |
| 153 | + |
| 154 | +```scala mdoc:invisible |
| 155 | +case class MetricsContext() |
| 156 | +``` |
| 157 | + |
| 158 | +```scala mdoc:compile-only |
| 159 | +val authAspect: HandlerAspect[Any, User] = ??? |
| 160 | +val requestIdAspect: HandlerAspect[Any, String] = ??? |
| 161 | +val metricsAspect: HandlerAspect[Any, MetricsContext] = ??? |
| 162 | + |
| 163 | +// Composed aspect has tuple type |
| 164 | +val composedAspect: HandlerAspect[Any, (User, String, MetricsContext)] = |
| 165 | + authAspect ++ requestIdAspect ++ metricsAspect |
| 166 | + |
| 167 | +// Handler receives all contexts |
| 168 | +val myHandler: Handler[(User, String, MetricsContext), Nothing, Request, Response] = |
| 169 | + handler { (_: Request) => |
| 170 | + ZIO.service[(User, String, MetricsContext)].map { case (user, requestId, metrics) => |
| 171 | + Response.text(s"User: ${user.name}, RequestID: $requestId, metrics: $metrics") |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | +val exampleRoute = |
| 176 | + Method.GET / "example" -> myHandler @@ composedAspect |
| 177 | +``` |
| 178 | + |
| 179 | +Also, we can use `withContext`: |
| 180 | + |
| 181 | +```scala mdoc:compile-only |
| 182 | +val myHandler: Handler[User & String & MetricsContext, Nothing, Request, Response] = |
| 183 | + handler { (_: Request) => |
| 184 | + withContext { (user: User, requestId: String, metrics: MetricsContext) => |
| 185 | + Response.text(s"User: ${user.name}, RequestID: $requestId, metrics: $metrics") |
| 186 | + } |
| 187 | + } |
| 188 | +``` |
0 commit comments