Request Lifecycle
Overview
Section titled “Overview”Every HTTP request follows the same path through Crate:
HTTP Request → vibe.d URLRouter → CrateRouter.handler() → CrateRouter.match() → IApiOperation.handle() → Middleware pipeline → Database query → Serialization → HTTP ResponseStep 1: vibe.d Routing
Section titled “Step 1: vibe.d Routing”When CrateRouter is constructed, it registers a catch-all handler with the vibe.d URLRouter:
this(URLRouter router) { this.router = router; router.any("*", requestErrorHandler(&this.handler));}The any("*") pattern matches all HTTP methods and all paths. The requestErrorHandler wrapper catches exceptions and converts them to JSON error responses.
Step 2: CrateRouter.handler()
Section titled “Step 2: CrateRouter.handler()”The handler method is the entry point for all requests:
void handler(HTTPServerRequest request, HTTPServerResponse response) { // Lazy initialization on first request if (!routerReady) { callOnRouterReady(); // Finalizes MCP tool lists, etc. }
// Always set CORS headers this.options(request.requestPath.toString, response);
// Handle OPTIONS preflight if (request.method == HTTPMethod.OPTIONS) { if ("Access-Control-Allow-Origin" !in response.headers) { return; // No matching route — vibe.d returns 404 } response.statusCode = 204; response.writeVoidBody(); return; }
// Parse body for POST/PUT/PATCH (needed for body-matched routing) Json requestBody = Json.init; if (request.method == HTTPMethod.POST || ...) { try { requestBody = request.json; } catch (Exception) {} }
// Find matching operation auto operation = match(request.method, request.requestPath.toString, requestBody); if (operation is null) return; // No match — vibe.d handles 404
// Execute the operation scope storage = new OperationContext; storage.request = request; storage.response = response; operation.handle(storage);}Key details:
callOnRouterReady()runs once on the first request, not during setup. This allows all models to be registered before MCP finalizes its tool list.- The request body is parsed before matching because MCP/GraphQL route based on body content.
- If no operation matches, the handler returns without writing a response. vibe.d’s URLRouter then falls through to other registered handlers or returns 404.
Step 3: Operation Matching
Section titled “Step 3: Operation Matching”CrateRouter.match() iterates through all registered TypeRouters and BlankRouters:
IApiOperation match(HTTPMethod method, string routeName, Json requestBody) { foreach (type; types) { auto matched = type.match(method, routeName, requestBody); if (matched !is null) return matched; } return null;}Each router’s match() method checks its operations:
Path-Based Matching (REST, JSON:API)
Section titled “Path-Based Matching (REST, JSON:API)”Uses matchTemplateRoute() to compare the request path against operation path templates:
Request: GET /users/abc123Template: GET /users/:id → Match! (:id = abc123)Template: GET /users → No match (different segment count)Template: GET /posts/:id → No match (different static segment)Body-Based Matching (MCP, GraphQL)
Section titled “Body-Based Matching (MCP, GraphQL)”For operations with a bodyMatcher, the request body is checked against the matcher pattern:
// MCP operation has bodyMatcher:// {"method": "tools/call", "params": {"name": "list_users"}}
// Request body must contain all matcher fields with matching valuesif (!matchJson(requestBody, operation.rule.request.bodyMatcher)) { continue; // Try next operation}matchJson() recursively checks that every field in the matcher exists in the request body with the same value. Extra fields in the request are ignored.
Step 4: Operation Execution
Section titled “Step 4: Operation Execution”Each operation type follows a similar pattern. Here’s GetItemApiOperation as an example:
void handle(OperationContext storage) { // 1. Prepare: run middleware, fetch item Json item = this.prepareItemOperation!"getItem"(storage);
// 2. Check if middleware already wrote response if (storage.response.headerWritten) return;
// 3. Apply response headers from CrateRule this.applyRule(storage);
// 4. Create response mapper storage.mapper = this.mapper(storage);
// 5. Serialize and write response auto responseData = _rule.response.serializer.toResponseItem(item, storage); storage.response.writeJsonBody(responseData, _rule.response.statusCode);}prepareItemOperation
Section titled “prepareItemOperation”This method (from CrateOperationMixin) handles the common database fetch logic:
- Extract item ID from URL path parameters
- Run non-query middleware (authentication, validation)
- Build query:
crate.get.where("_id").equal(id) - Run query middleware (visibility filters)
- Execute query and fetch item
- Generate ETag (using
hashfield or CRC32 of JSON) - Return the JSON item
prepareListOperation
Section titled “prepareListOperation”Similar but for collections:
- Create empty query:
crate.get() - Run all middleware (filters add
.where()clauses, pagination adds.skip()/.limit()) - Store query in context (execution deferred to serializer for streaming)
Step 5: Middleware Pipeline
Section titled “Step 5: Middleware Pipeline”Middleware runs in two phases:
Phase 1: Non-Query Middleware
Section titled “Phase 1: Non-Query Middleware”Authentication, validation, and request transformation. These run first because:
- Auth failures should short-circuit before touching the database
- Request data needs to be validated before building queries
Phase 2: Query Middleware
Section titled “Phase 2: Query Middleware”Visibility filters, pagination, and sorting. These modify the IQuery that will be executed:
@getListIQuery getList(IQuery selector, HTTPServerRequest request) @safe { selector.where("visibility").equal("public"); selector.limit(20); return selector;}Early Exit
Section titled “Early Exit”If any middleware writes to the response (e.g., 401 Unauthorized), subsequent middleware and the operation itself are skipped:
if (storage.response.headerWritten) return;Step 6: Serialization
Section titled “Step 6: Serialization”The serializer (determined by the policy) converts internal JSON to the protocol format:
REST API
Section titled “REST API”{ "user": { "_id": "abc123", "name": "Alice" }}JSON:API
Section titled “JSON:API”{ "data": { "type": "users", "id": "abc123", "attributes": {"name": "Alice"} }}{ "jsonrpc": "2.0", "result": { "content": [{"type": "text", "text": "{...}"}] }}CORS Handling
Section titled “CORS Handling”CORS headers are set on every response, not just OPTIONS:
void options(string routeName, HTTPServerResponse response) { // Collect allowed methods from all routers matching this path foreach (type; types) { methods ~= type.methods(routeName); headers ~= type.headers(routeName); }
response.headers["Access-Control-Allow-Origin"] = "*"; response.headers["Access-Control-Allow-Headers"] = headers.join(", "); response.headers["Access-Control-Allow-Methods"] = methods.join(", ");}For OPTIONS requests specifically, the response is 204 with an empty body.