Lifecycle Hooks
Enable the hook feature to intercept request lifecycle events for observability, tracing, logging, or custom middleware.
Quick Example
#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard, ConnectionGuard};
struct LoggingHook;
impl Hook for LoggingHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
println!("β {} ({})", ctx.handler_name, ctx.transport);
Some(Box::new(std::time::Instant::now()))
}
fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> {
println!("β connect: {} ({})", ctx.handler_name, ctx.transport);
Some(Box::new(()))
}
}
impl RequestGuard for std::time::Instant {
fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
println!("β {} OK ({:?})", ctx.handler_name, self.elapsed());
}
fn on_error(&mut self, ctx: &RequestContext, err: &afast::Error) {
println!("β {} error: {}", ctx.handler_name, err);
}
}
impl ConnectionGuard for () {
fn on_disconnect(&mut self, ctx: &RequestContext) {
println!("β disconnect: {} ({})", ctx.handler_name, ctx.transport);
}
}
}
Hook Traits
Hook β Entry Point
#![allow(unused)]
fn main() {
pub trait Hook: Send + Sync + 'static {
/// Called before each request for request-response interfaces
/// (HTTP binary, WS binary, TCP binary, ordinary HTTP).
/// Return a `RequestGuard` to observe the response.
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> { None }
/// Called when a connection is established for connection-oriented interfaces
/// (WS long, TCP long, ordinary WS, SSE).
/// Return a `ConnectionGuard` to observe disconnection.
fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> { None }
}
}
RequestGuard β Per-Request Observer
#![allow(unused)]
fn main() {
pub trait RequestGuard: Send + 'static {
/// Called when the handler returns Ok.
fn on_response(&mut self, ctx: &RequestContext, response: &[u8]) {}
/// Called when the handler returns Err.
fn on_error(&mut self, ctx: &RequestContext, error: &afast::Error) {}
}
}
ConnectionGuard β Long Connection Observer
#![allow(unused)]
fn main() {
pub trait ConnectionGuard: Send + 'static {
/// Called when the connection is closed.
fn on_disconnect(&mut self, ctx: &RequestContext) {}
}
}
Global and Service Hooks
#![allow(unused)]
fn main() {
let app = AFast::new()
.hook(LoggingHook) // Global: all handlers
.service(
service!("api" => { h(handler) })
.hook(ApiSpecificHook) // Service: only this service's handlers
);
}
- Global hooks run for every handler across all services.
- Service hooks run only for handlers in that service.
- Both always execute β they never replace each other.
- Execution order: global first, then service (onion model π§ ).
Hook Lifecycle by Transport
Hooks are divided into two categories by interface type:
before_request(request-response): HTTP binary, WS binary, TCP binary, ordinary HTTP.on_connect(connection-oriented): WS long, TCP long, ordinary WS, SSE.
Binary Protocol (HTTP POST /_api, WS /_ws, TCP)
Regular handlers (request-response):
before_request β handler β on_response / on_error
Long-connection handlers (call_stream):
on_connect β handler β on_disconnect
Ordinary HTTP (ordinary-http)
before_request β handler β on_response / on_error
on_connect / on_disconnect are not called for ordinary HTTP (stateless request/response).
Ordinary WebSocket (ordinary-ws)
on_connect β handler β on_disconnect
on_connect: fires after the WebSocket handshake completes.on_disconnect: fires after the handler returns and forwarding tasks are cleaned up.
Ordinary SSE (ordinary-sse)
on_connect β handler (spawned) β on_disconnect
on_connect: fires before the SSE response is sent and the handler is spawned.on_disconnect: fires after the handler task completes.
Request Context Integration
Hooks can read and write per-request data via the ctx field on RequestContext. This data is then available to handlers via the Ctx<T> extractor.
#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext};
#[derive(Clone)]
struct RequestId(pub String);
struct CtxHook;
impl Hook for CtxHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
// Write into the per-request context
ctx.ctx.insert(RequestId(format!("req-{:08x}", /* ... */)));
None
}
}
}
The handler retrieves it automatically:
#![allow(unused)]
fn main() {
#[handler(desc("..."))]
async fn my_handler(ctx: afast::Ctx<RequestId>) -> afast::Result<()> {
println!("request_id = {}", ctx.0 .0);
Ok(())
}
}
The same context is shared across all hooks and the handler for a single request. For long-connection handlers (WS/TCP), the context lives for the entire connection duration.
See Request Context (Ctx) for full documentation.
Accessing Custom Attributes
RequestContext exposes the handlerβs custom attributes via ctx.attrs:
#![allow(unused)]
fn main() {
impl Hook for DeprecationHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
for attr in ctx.attrs {
match attr.key {
"deprecated" => eprintln!("WARNING: {} is deprecated", ctx.handler_name),
"tag" => {
if let AttrValue::Str(v) = attr.value {
eprintln!("tag: {}", v);
}
}
_ => {}
}
}
None
}
}
}
Getting Client IP
RequestContext provides two fields for obtaining the clientβs IP:
#![allow(unused)]
fn main() {
impl Hook for IpHook {
fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
// TCP peer address
let peer_ip = &ctx.client_ip;
// Real client IP (from X-Forwarded-For / X-Real-IP headers)
let real_ip = ctx.forwarded_for.as_deref().unwrap_or(&ctx.client_ip);
println!("client: {}, real: {}", peer_ip, real_ip);
None
}
}
}
client_ip: Available for all transportsforwarded_for: Only available for HTTP/WS;Nonefor TCP
Hook Key β Route Matching
Hooks are matched by "service_name:route_path", not by handler function name. This avoids conflicts when the same function name appears in different groups within the same service:
#![allow(unused)]
fn main() {
service!("admin" => {
group("users" => {
get("info", get_info), // key: "admin:/users/info"
}),
group("posts" => {
get("info", get_info), // key: "admin:/posts/info" β no conflict!
}),
})
}
For merged services (same service name registered multiple times), hook entries are automatically deduplicated.
RequestContext Fields
| Field | Type | Description |
|---|---|---|
handler_name | &'static str | Handler function name |
handler_desc | &'static str | Description from #[handler(desc(...))] |
transport | &'static str | "http-binary", "http", "ws-binary", "ws", "tcp", or "sse" |
is_binary | bool | Whether this is a binary protocol handler |
method | &'static str | HTTP method ("GET", "POST", etc.), empty for non-HTTP |
long_connection | bool | Whether this is a long-connection handler (Receiver/Sender) |
handler_id | usize | Handler offset in binary dispatch table (0 for ordinary routes) |
state | Arc<StateMap> | Shared application state |
ctx | RequestCtx | Per-request context container (hooks write, handlers read via Ctx<T>) |
attrs | &'static [Attr] | Custom handler attributes from #[handler(...)] |
Supported Extractors
All extractors work across all transports:
| Extractor | HTTP | WS | SSE | TCP |
|---|---|---|---|---|
State<T> | β | β | β | β |
Query<T> | β | β | β | β |
Param<T> | β | β | β | β |
Header<T> | β | β | β | β |
Body<T> | β | β | β | β |
Custom<T> | β | β | β | β |
Data | β | β | β | β |
WsSender | β | β | β | β |
WsReceiver | β | β | β | β |
SseSender | β | β | β | β |
Sender | β | β | β | β |
Receiver | β | β | β | β |
Server Example Output
[hook] β connect: chat_ws (ws) β on_connect
[check-svc] βΆ chat_ws β service hook
[ws-chat] client joined room: test β handler runs
[ws-chat] client left room: test β handler returns
[hook] β disconnect: chat_ws (ws) β on_disconnect
[check-svc] β chat_ws done β service hook done