Skip to content

Commit edba470

Browse files
authored
Documentation of Request-scoped Context Section (#3752)
* request store documentation. * add index page. * fix page's id.
1 parent 923190f commit edba470

File tree

4 files changed

+324
-0
lines changed

4 files changed

+324
-0
lines changed

docs/reference/contextual/index.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
id: index
3+
title: "Request-scoped Context Management"
4+
sidebar_label: "Overview"
5+
---
6+
7+
When building web applications with ZIO HTTP, you often need to share contextual information across different layers of your request processing pipeline. This might include user authentication data, correlation IDs for distributed tracing, session information, or request metadata. The challenge is making this data available throughout the request lifecycle without threading it through every function parameter or resorting to global mutable state.
8+
9+
ZIO HTTP offers two complementary approaches to managing request-scoped context, each with its own strengths, trade-offs, and use-cases. Understanding when to use each approach will help you build applications that are both maintainable and type-safe:
10+
11+
1. **[ZIO Environment with HandlerAspect](zio-environment.md)** leverages ZIO's type-safe dependency injection system to propagate request-scoped context through the `HandlerAspect` stack and finally the handlers. `HandlerAspect` produces typed context values that become part of the ZIO environment, making them accessible to handlers via `ZIO.service` or with the `withContext` DSL. This approach provides compile-time guarantees that all required context is present, catching missing dependencies before your code ever runs.
12+
13+
The ZIO environment approach excels when compile-time safety is paramount, and it is specifically used for passing context from middlewares to handlers. The type system ensures that handlers explicitly declare their context requirements, making it impossible to forget to apply necessary middleware. This catches entire classes of runtime errors at compile time. The approach also provides better documentation through types—when you see `Handler[User & RequestId, ...]`, you immediately know what context or service the handler requires.
14+
15+
2. **[RequestStore](request-store.md)** provides a FiberRef-based storage mechanism that acts like a request-scoped key-value store. You can store and retrieve typed values at any point during request processing without explicit parameter passing. The data is automatically isolated per request and cleaned up when the request completes.
16+
17+
`RequestStore` shines when you don't need compile-time type safety. Unlike the previous approach, the `RequestStore` is not tied to the middleware stack, allowing you to store and retrieve context at any point of the request lifecycle without changing handler signatures. This is helpful when you don't want to pass the context through every layer explicitly, and you don't need compile-time guarantees about context presence. It's also ideal when you're working with legacy code or integrating with systems where compile-time type safety is less critical than ease of integration. The pattern feels familiar to developers coming from other web frameworks that use context managers with thread-local storage.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
id: request-store
3+
title: "Request-scoped Context with RequestStore"
4+
sidebar_label: "RequestStore"
5+
---
6+
7+
**RequestStore** is a fiber-local storage mechanism in ZIO HTTP that allows you to store and retrieve request-scoped data throughout the lifecycle of an HTTP request. It provides a type-safe way to share context across middleware, handlers, and service layers without explicit parameter passing.
8+
9+
## Overview
10+
11+
RequestStore uses ZIO's `[FiberRef](https://zio.dev/reference/state-management/fiberref/)` under the hood to ensure that data is isolated per request and automatically cleaned up when the request completes. This makes it ideal for storing contextual information that needs to be accessed at various points during request processing without leaking memory. Automatic cleanup is built-in, so there's no manual cleanup needed—data is cleared when the fiber completes.
12+
13+
RequestStore excels at managing request-scoped context throughout your application. A common use case is request context tracking, where you store user IDs, session IDs, timestamps, IP addresses, correlation IDs, trace IDs, and other contextual information extracted from headers or authentication tokens. This makes the data available to all layers of your application without explicit parameter passing.
14+
15+
## API
16+
17+
The core API of RequestStore consists of three main functions:
18+
19+
```scala
20+
object RequestStore {
21+
// Retrieve a value from the store
22+
def get[A: Tag]: UIO[Option[A]]
23+
24+
// Store a value in the store
25+
def set[A: Tag](a: A): UIO[Unit]
26+
27+
// Update a value in the store
28+
def update[A: Tag](f: Option[A] => A): UIO[Unit]
29+
}
30+
```
31+
32+
You can think of `RequestStore` as a type-safe, request-scoped key-value store where the keys are the types of the values you want to store.
33+
34+
The `Tag` context bound ensures type safety by requiring a type tag for the stored type, preventing accidental type mismatches.
35+
36+
## Basic Usage
37+
38+
Assume you have modeled some request-scoped data as `UserId`:
39+
40+
```scala mdoc:silent
41+
case class UserId(value: String)
42+
```
43+
44+
When writing authentication middleware, after validating the user, you can store the `UserId` in the `RequestStore`:
45+
46+
```scala mdoc:silent
47+
import zio._
48+
import zio.http._
49+
50+
def authorizeAndExtractUserId: Header.Authorization => Task[UserId] = ???
51+
52+
val authMiddleware: Middleware[Any] = new Middleware[Any] {
53+
override def apply[Env1 <: Any, Err](routes: Routes[Env1, Err]): Routes[Env1, Err] =
54+
routes.transform { h =>
55+
Handler.scoped[Env1] {
56+
Handler.fromFunctionZIO { (req: Request) =>
57+
{
58+
for {
59+
header <- ZIO.fromOption(req.header(Header.Authorization))
60+
userId <- authorizeAndExtractUserId(header)
61+
_ <- RequestStore.set(userId)
62+
response <- h(req)
63+
} yield response
64+
} orElseFail Response.status(Status.Unauthorized)
65+
}
66+
}
67+
}
68+
}
69+
```
70+
71+
Whenever you need to access the `UserId` later in the request lifecycle, simply call `RequestStore.get`:
72+
73+
74+
```scala mdoc:compile-only
75+
import zio._
76+
import zio.http._
77+
78+
def getProfile(str: UserId): Task[String] = ???
79+
80+
val routes = Routes(
81+
Method.GET / "profile" -> handler { (req: Request) =>
82+
for {
83+
userId <- RequestStore.get[UserId].someOrFail(Response.notFound("No user id found"))
84+
profile <- getProfile(userId)
85+
} yield Response.text(profile)
86+
},
87+
) @@ authMiddleware
88+
```
89+
90+
You can also update existing data in the store using `RequestStore#update`.
91+
92+
93+
## Integration with Other Features
94+
95+
RequestStore is used internally by `forwardHeaders` to store headers that should be forwarded to outgoing requests. For example, the following route forwards the `X-Request-Id` header to the downstream service when calling it:
96+
97+
```scala mdoc:compile-only
98+
val routes = Routes(
99+
Method.GET / "users" -> handler { (req: Request) =>
100+
for {
101+
client <- ZIO.service[Client]
102+
// Authorization header is automatically forwarded via RequestStore
103+
response <- (client @@ ZClientAspect.forwardHeaders)
104+
.batched(Request.get(url"http://user-service/users"))
105+
} yield response
106+
}
107+
) @@ Middleware.forwardHeaders("X-Request-Id")
108+
```
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
```

website/sidebars.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ const sidebars = {
8181
],
8282
},
8383

84+
// Contextual Data Types
85+
{
86+
type: "category",
87+
label: "Request-scoped Context",
88+
link: { type: 'doc', id: "reference/contextual/index" },
89+
items: [
90+
"reference/contextual/request-store",
91+
"reference/contextual/zio-environment",
92+
],
93+
},
94+
8495
// WebSocket subsection
8596
{
8697
type: "category",

0 commit comments

Comments
 (0)