Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Request Context (Ctx)

The Ctx<T> extractor provides per-request (or per-connection) typed data that flows through the entire handler lifecycle. Unlike State<T> which is application-global, Ctx<T> is scoped to a single request.

Core Concepts

State<T>Ctx<T>
ScopeApplication-globalPer-request / per-connection
StorageStateMap (set once at startup)RequestCtx (created per request)
LifetimeEntire applicationRequest start β†’ fully finished
Use caseDatabase pool, configRequest ID, auth info, timing
Set byAFast::state()Hooks (before_request, on_connect)

How It Works

Request arrives
    β”‚
    β–Ό
RequestCtx::new()          ← framework creates empty context
    β”‚
    β–Ό
Hook: before_request()     ← hook inserts values: ctx.ctx.insert(RequestId(...))
    β”‚
    β–Ό
Handler executes           ← framework extracts: Ctx<RequestId> from context
    β”‚
    β–Ό
Hook: on_response()        ← hook reads values: ctx.ctx.get::<RequestId>()
    β”‚
    β–Ό
RequestCtx dropped         ← all values freed

For long-connection handlers (WS/TCP), the RequestCtx lives for the entire connection:

Connection established
    β”‚
    β–Ό
RequestCtx::new()          ← created once
    β”‚
    β–Ό
Hook: on_connect()         ← hook inserts connection-scoped data
    β”‚
    β–Ό
Message 1: handler executes  ← reads Ctx<T>
Message 2: handler executes  ← same Ctx<T> (shared across messages)
    ...
    β”‚
    β–Ό
Hook: on_disconnect()      ← hook reads final state
    β”‚
    β–Ό
RequestCtx dropped

Quick Example

1. Define your context data

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct RequestInfo {
    pub request_id: String,
    pub started_at: std::time::Instant,
}
}

The type must be Clone + Send + Sync + 'static (for extraction via Ctx<T>).

2. Create a hook that inserts data

#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard};

struct CtxHook;

impl Hook for CtxHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        ctx.ctx.insert(RequestInfo {
            request_id: format!("req-{:08x}", /* ... */),
            started_at: std::time::Instant::now(),
        });
        None  // No guard needed if only writing context
    }
}
}

3. Use in handlers

#![allow(unused)]
fn main() {
use afast::{Ctx, handler};

#[handler(desc("My handler"))]
async fn my_handler(ctx: Ctx<RequestInfo>) -> afast::Result<MyResp> {
    let elapsed = ctx.0.started_at.elapsed();
    println!("request {} took {:?}", ctx.0.request_id, elapsed);
    // ...
}
}

ctx.0 gives you the inner RequestInfo value.

Supported Handler Types

Ctx<T> works in all handler types:

Handler TypeMacroExample
Binary protocol#[handler]async fn h(ctx: Ctx<Info>) -> Result<T>
HTTP ordinary#[get] / #[post] / etc.async fn h(ctx: Ctx<Info>) -> HttpResult<Json<T>>
WebSocket#[ws]async fn h(ctx: Ctx<Info>, sender: WsSender) -> Result<()>
SSE#[sse]async fn h(ctx: Ctx<Info>, sender: SseSender) -> Result<()>
Long-connection#[handler] + Receiver/Senderasync fn h(ctx: Ctx<Info>, rx: Receiver, tx: Sender)

Ctx<T> does not participate in the binary/ordinary mutual exclusion check, so it can be combined with any other extractor.

Parameter Position

Ctx<T> can be placed at any position in the parameter list. By convention, put it first:

#![allow(unused)]
fn main() {
#[handler(desc("..."))]
async fn my_handler(
    ctx: Ctx<RequestInfo>,          // ← context first
    state: State<AppState>,          // ← then state
    data: Data<MyReq>,               // ← then data
) -> afast::Result<MyResp> {
    // ...
}
}

Reading and Writing in Hooks

Hooks interact with the context via RequestContext::ctx:

#![allow(unused)]
fn main() {
impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // Write
        ctx.ctx.insert(MyData { value: 42 });

        // Read (useful if another hook wrote earlier)
        // ctx.ctx.get::<OtherData>()

        Some(Box::new(MyGuard))
    }
}

impl RequestGuard for MyGuard {
    fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
        // Read what the handler or earlier hooks wrote
        if let Some(data) = ctx.ctx.get::<MyData>() {
            println!("value was {}", data.value);
        }
    }
}
}

Getting Client IP

RequestContext provides two fields for obtaining the client’s IP address:

FieldTypeDescription
client_ipStringTCP peer address (peer_addr)
forwarded_forOption<String>Real IP from X-Forwarded-For / X-Real-IP headers
#![allow(unused)]
fn main() {
impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // Direct connection IP (may be proxy IP)
        let ip = &ctx.client_ip;

        // Real client IP (only available for HTTP/WS)
        let real_ip = ctx.forwarded_for.as_deref().unwrap_or(&ctx.client_ip);

        println!("client: {} (real: {})", ip, real_ip);
        None
    }
}
}

Notes:

  • client_ip is available for all transports (HTTP, WS, TCP, SSE)
  • forwarded_for is only available for HTTP and WebSocket transports; always None for TCP
  • When behind a reverse proxy, prefer using forwarded_for

API Reference

RequestCtx β€” Container

#![allow(unused)]
fn main() {
// Create empty context
let ctx = RequestCtx::new();

// Insert a value (keyed by type)
ctx.insert(my_value);

// Retrieve a cloned value
let val: Option<MyType> = ctx.get::<MyType>();

// Clone (cheap β€” shares Arc)
let ctx2 = ctx.clone();
}

Ctx<T> β€” Extractor

#![allow(unused)]
fn main() {
pub struct Ctx<T>(pub T);

// Access the inner value
let inner: T = my_ctx.0;
}

Performance

  • RequestCtx::new() allocates a single Arc<RwLock<HashMap>> β€” very cheap.
  • insert() and get() acquire a RwLock β€” uncontended in the typical case (sequential write then read), so overhead is negligible.
  • RequestCtx::clone() is an Arc clone β€” O(1).
  • For large context values, wrap in Arc<T> to make get() clone cheap.
  • Handlers that don’t use Ctx<T> pay zero extraction cost β€” the empty context is created but never read.
Copied to clipboard!