Request Lifecycle

Every HTTP request to a bext server passes through a well-defined pipeline. Understanding this pipeline helps you configure caching, write plugins, and debug performance issues.

Overview

Client
  |
  v
[1] TLS Termination (rustls / ACME)         ~0.5 ms (handshake) / ~0 ms (resumed)
  |
  v
[2] HTTP Parsing (actix-web / h3)            ~0.01 ms
  |
  v
[3] WAF Check                                ~0.05 ms
  |
  v
[4] Middleware Stack (on_request)             ~0.1-0.5 ms
  |  CORS -> Rate Limit -> Auth -> Tenant -> Tracing -> Plugin Middleware
  |
  v
[5] Route Matching                           ~0.01 ms
  |
  v
[6] Cache Lookup (L1 DashMap, L2 Redis)      ~0.01 ms (L1 hit) / ~1 ms (L2 hit)
  |
  v  (cache miss)
[7] Render / Static / Proxy                  ~5-50 ms (SSR) / ~0.01 ms (static)
  |
  v
[8] HTML Transforms                          ~0.1-1 ms
  |
  v
[9] Compression (brotli / gzip / zstd)       ~0.5-5 ms (miss) / ~0 ms (cached)
  |
  v
[10] Middleware Stack (on_response)           ~0.05 ms
  |
  v
[11] Response Delivery                       network-bound
  |
  v
[12] Post-Response Hooks (async)             non-blocking

Stage Details

1. TLS Termination

If TLS is enabled (tls feature), bext terminates TLS using rustls with ring crypto. The SniCertResolver selects the correct certificate based on the SNI hostname, supporting multi-domain setups. OCSP stapling is performed in the background by OcspStapler.

For HTTP/3 (QUIC), the quinn listener handles the TLS 1.3 handshake as part of the QUIC connection setup. HTTP/1.1 and HTTP/2 share the same rustls ServerConfig.

First connections pay the full handshake cost (~0.5 ms). Resumed sessions (TLS session tickets) skip the handshake entirely.

2. HTTP Parsing

actix-web parses the HTTP request (method, path, headers, body). For HTTP/3, the h3 crate decodes QPACK headers and provides the same request structure. The parsed request is wrapped in actix-web's HttpRequest type.

3. WAF Check

When the waf feature is enabled, bext-waf inspects the request for:

- IP filtering -- CIDR allow/deny lists.

- Geo-blocking -- country-level blocks based on IP geolocation.

- Request inspection -- regex-based detection of SQL injection, XSS, path traversal, and scanner fingerprints. All patterns are compiled once via OnceLock.

- Bot detection -- user-agent analysis with configurable modes (allow, challenge, block).

- DDoS mitigation -- connection rate and request rate thresholds.

The WAF returns one of four decisions: Allow, Block (with status code and reason), RateLimit (with Retry-After), or Challenge (HTML JS challenge page). Blocked requests never reach the middleware stack.

4. Middleware Stack (on_request)

The middleware stack runs in priority order. Built-in middleware fires first:

Priority Middleware Action
100 CORS Sets Access-Control headers, handles OPTIONS preflight
200 Rate Limiter Token-bucket per IP, returns 429 if exceeded
300 Auth JWT validation or session lookup
400 Tenant Resolves tenant from hostname or header
500 Tracing Assigns request ID, starts span
600+ Plugin middleware Custom plugin logic

Each middleware receives a RequestContext with method, path, hostname, headers, query string, peer IP, and a mutable extensions map for passing data to downstream handlers and the on_response phase.

A middleware can short-circuit the pipeline by returning RequestAction::Respond(PluginResponse) with a custom status, headers, and body.

5. Route Matching

bext-core::route::process matches the request path against the application's route table. Routes are compiled at build time from the filesystem convention (src/pages/, src/app/) or explicit configuration. Dynamic segments ([id]), catch-all routes ([...slug]), and route groups are supported.

6. Cache Lookup

The ISR cache is a two-tier system:

- L1 (DashMap) -- in-process concurrent hash map. Sub-microsecond lookups. Bounded to 2,000 entries with LRU eviction.

- L2 (Redis) -- optional, enabled with the redis feature. ~1 ms round-trip. Shared across multiple bext instances for horizontal scaling.

A cache hit at L1 skips stages 7-8 entirely. A cache hit at L2 promotes the entry to L1. A stampede guard ensures only one request rebuilds on cache miss -- all other concurrent requests wait for the result.

7. Render / Static / Proxy

On a cache miss, the request is dispatched based on the framework declared in bext.config.toml:

  • PRISM pages ([framework] type = "prism") -- dispatched through ssr_pipeline::prism, which compiles the matched route on first request via bext-turbopack (with the PRISM compile pass folding static JSX subtrees into HTML strings before bundling), then evaluates the resulting JS in the V8 eval pipeline. Subsequent requests hit a warm V8 isolate with a cached page context — typical SSR is 1-5 ms, and as low as 77 µs on a 50-row Tailwind table fixture in release mode. The compile pass is opt-out via BEXT_PRISM_COMPILE=0. See PRISM. - Next.js / RSC pages ([framework] type = "nextjs") -- dispatched to a single-threaded V8 eval pipeline that owns a long-lived isolate, caches a warm page context per route, and drives React 19's renderToReadableStream through a custom event-loop driver for async server components, Suspense, and the use(promise) hook. When [render] rsc = true, bext compiles a per-route RSC bundle with the react-server condition and exposes the Flight payload at /__bext/rsc/<path>. Typical render is 1-5 ms warm and 30-50 ms cold (bundle compile + V8 eval). - Hono / Express / fetch-handler apps -- compiled into the V8 pool and called via the standard fetch(Request) ABI. - Adapter-built sites (Astro, SolidStart, SvelteKit, Qwik) -- served from the framework's built output, with bext SSR for routes that need it and static delivery for the rest. - Static files -- served directly from the build output directory via actix-files. No rendering overhead. - Proxy routes -- forwarded to an upstream server (Bun, Node, PHP) via reqwest. Used in hybrid mode where bext handles SSR and the framework server handles API routes. - PHP routes -- when the php feature is enabled, dispatched to the PhpPool for in-process PHP execution. Worker mode keeps the framework kernel pre-loaded for ~34 µs per request.

8. HTML Transforms

After rendering, bext applies response-time HTML transforms:

- Server island replacement -- <bext-island> elements are replaced with rendered content.

- Preload hint extraction -- <link> and <script> tags in <head> are extracted for HTTP 103 Early Hints.

- Turbo frame injection -- adds data-bext-turbo-root attributes for HTML-over-the-wire navigation.

These are string-based single-pass transforms with minimal overhead.

9. Compression

Response bodies are compressed on first miss and cached in the compression cache (bounded to 2,000 entries with timestamp-based eviction). Both gzip and brotli variants are pre-computed and stored as Arc<Vec<u8>>. Subsequent requests for the same content serve the pre-compressed version with zero CPU cost.

The Accept-Encoding header determines which variant is sent. Brotli is preferred when available.

10. Middleware Stack (on_response)

The middleware stack runs in reverse priority order. Each middleware's on_response receives the RequestContext and a mutable ResponseHeaders struct where it can modify status codes and inject headers.

11. Response Delivery

The final response is written to the client connection. For streaming SSR, chunks are produced by React 19's renderToReadableStream running inside V8 and pumped to completion by bext's custom event-loop driver (collect_async_render_result) before being flushed to the wire.

12. Post-Response Hooks

After the response is fully sent, lifecycle plugins receive an on_request_complete event with path, method, status, cache status, render time, tenant ID, and other metadata. These hooks fire asynchronously and must not block the connection.