Context properties
The Middleware page has an API key check that answers 401 to callers it doesn't know. Once a middleware like that has decided who is calling, the handler usually wants to know too. But a handler receives nothing except the Request, so whatever the middleware learned has to travel on the request. A context property is a typed slot on every request, made for exactly that.
A property for the current user
Define the property once, at the top level, with the type of the value it holds:
class User {
final String name;
User(this.name);
}
final userProperty = ContextProperty<User>('user');
The middleware looks up the API key, and when it knows the caller, it stores a User in the property:
Handler authenticate(Handler next) {
return (req) {
final name = _users[req.headers['X-API-Key']?.first];
if (name == null) {
return Response.unauthorized(body: Body.fromString('Unknown API key\n'));
}
userProperty[req] = User(name);
return next(req);
};
}
userProperty[req] = User(name) sets the value for this request alone. The handler reads it back with get:
Response whoAmI(Request req) {
final user = userProperty.get(req);
return Response.ok(body: Body.fromString('You are ${user.name}\n'));
}
get returns a User, not a User? or an Object. The type of the property carries through, so the handler needs no cast and no null check. The program puts the middleware on /api:
final app = RelicApp()
..use('/api', authenticate)
..get('/api/me', whoAmI)
..get('/me', whoAmI)
..get('/hello', greet);
Two keys get two answers:
curl -H 'X-API-Key: key-alice' http://localhost:8080/api/me
curl -H 'X-API-Key: key-bob' http://localhost:8080/api/me
You are Alice
You are Bob
Requests can be in flight at the same time, and each one sees its own user. That is why the value lives on the request rather than in a global variable, which a second request could overwrite before the first one had read it.
When no value was set
The program also registers whoAmI on /me, outside /api, where the middleware does not run. Nothing sets the property for that request, and get throws:
curl -i http://localhost:8080/me
HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8
date: Fri, 23 Oct 2026 11:54:45 GMT
content-length: 21
Internal Server Error
The server log names the property that was missing:
Bad state: ContextProperty value not found. Property: user. Ensure middleware has set this value for the request.
That name is the string passed when the property was created. Without one, the message names the type instead, User, which says just as much until two properties hold the same type.
When a missing value is normal, read the property with the index operator, which returns null instead of throwing. A small extension on Request makes such reads look like any other request field:
extension on Request {
User? get user => userProperty[this];
}
Response greet(Request req) {
final name = req.user?.name ?? 'stranger';
return Response.ok(body: Body.fromString('Hello, $name\n'));
}
/hello is outside /api too, and gets the default:
Hello, stranger
Rules for properties
Create each property once, as a top-level or static final, as userProperty is here. A property claims a slot on every request for the life of the isolate. One created inside a handler would claim a new slot on every request, and debug builds stop that with an assertion after 1024 properties.
A value lives as long as its request. Assigning null removes it. A copy of the request made with copyWith shares its properties with the original, so a value set on either one is visible on both.
Properties that Relic sets
Relic uses the same mechanism for what routing learns about a request. The routing middleware sets the raw path parameters, the router and the matched path, and the typed parameter accessors are built from them on first read:
request.rawPathParameters- the path parameters of the matched route, as strings.request.pathParameters- typed accessors overrawPathParameters.request.queryParameters- typed accessors over the query of the URL.request.router- theRelicRouterthat routed the request, whichforwardTouses.request.matchedPathandrequest.remainingPath- the part of the path the route matched, and the rest.
Examples & further reading
Examples
- Context property example - The program behind this page.
API documentation
- ContextProperty class - Typed request-scoped data.
- Request class - HTTP request object that holds context properties.