Skip to content

Request lifecycle

A gisila app is one Shelf handler built by GisilaApp.buildHandler(). It is a fixed stack of framework middleware, your middleware, a router, and for each route a small per-route pipeline that ends in the code the generator wrote for your controller method. Knowing the order tells you what runs before what, which requests reach the database, and where errors are caught.

Read this when you need to decide:

  • where to put a piece of cross-cutting behaviour (global middleware, named middleware, a service);
  • why a request was rejected before your code ran (CORS, rate limit, body size, auth);
  • whether a request opened a database session or was logged;
  • which timeout or limit wins when you set both app-wide and per route.

You don’t need any of this to write ordinary controllers: annotate methods, return values, throw HttpExceptions. Start with Controllers and come back when something surprises you.

socket
│
▼ App pipeline (installed by buildHandler, outermost first)
1 security headers ............ AppConfig.securityHeaders (on by default)
2 request id + RequestContext .. AppConfig.requestIdHeader
3 health check ................ AppConfig.healthCheckPath (answers here)
4 metrics + /metrics .......... AppConfig.metrics
5 gzip ........................ AppConfig.compression
6 ETags ....................... AppConfig.etags
7 cache on ctx ................ AppConfig.cache
8 CORS (answers pre-flights) .. AppConfig.cors
9 app rate limit (429) ........ AppConfig.rateLimit
10 body size limit (413) ....... AppConfig.maxRequestBodyBytes
11 access log .................. AppConfig.logRequests
12 error mapper ................ exceptions → JSON envelope
13 database session ............ AppConfig.database (open … close(ok))
14 your app.use() middleware
15 Server / X-Powered-By headers
16 app timeout (504) ........... AppConfig.requestTimeout
17 router ...................... no match → JSON 404, then static mounts
│
▼ Route pipeline (gisilaRoute, from the merged RouteConfig)
a CORS override
b route rate limit (429)
c content-type whitelist (415)
d route body cap (413)
e RouteConfig.middleware + @UseMiddleware
f route timeout (504)
g auth guard: authenticator → 401, permission resolver → 403
│
▼ Generated handler
bind parameters (path, query, header, body, Form, Service, ctx)
call your method
render the result (Response as-is, void → empty, else JSON)

Some consequences:

  • Early rejections are cheap. Pre-flights, rate-limited requests and oversized bodies never open a database session and never reach your code.
  • Errors are caught in one place. Anything thrown below step 12 (your middleware, services, controllers, binding) becomes a JSON error. The steps above it answer their own errors (429, 413, 403 for a refused pre-flight) and are not routed through the error mapper or errorHandler.
  • Limits stack. An app-wide rate limit, timeout or body cap and a per-route one both apply. The tighter one decides.
  • Authentication is per route. The authenticator runs at step g, only for routes with a guard. On unguarded and @Public routes, ctx.principal is null.
  • The database session wraps your middleware and handler. It is opened before app.use() middleware runs and closed after the response is built, committed when the status is below 500.

Step 2 creates a RequestContext and attaches it to the Shelf request. Later steps add to it: the cache (7), the database session (13), the service and singleton resolvers, the principal (g). Your method receives it by declaring a RequestContext parameter, and services get it as ctx. Use ctx.extra to pass data from your middleware to services.

A timeout at step 16 or f answers 504 immediately, but Dart can’t abort the running handler. The framework cancels ctx.cancellation and records the handler as abandoned; the database session is closed (rolled back) only once the handler finishes. Long handlers should check ctx.cancellation.throwIfCancelled() between steps.

start() calls init() (connect the cache, create factory singletons, run onStart hooks), builds the handler and binds the socket. stop() stops accepting connections, waits up to drainTimeout for in-flight requests, runs onStop hooks in reverse, disposes singletons in reverse, disconnects the cache and disposes the database provider.

A request POST /orders with a JSON body, on a route with @RequireAuth() and config: RouteConfig(rateLimit: ...):

  1. Gets a request id, passes CORS and the app limits.
  2. Opens a database session.
  3. Passes your tenant middleware from app.use().
  4. Matches the route; passes the route rate limit.
  5. The JWT authenticator verifies the token and sets ctx.principal.
  6. The generated handler binds OrderForm (400 on validation errors) and resolves OrderService.
  7. OrderService.place throws Conflict('Duplicate order').
  8. The error mapper renders 409; the session closes with ok: true (409 < 500), so it commits.
  9. The access log writes one line with status 409, and the response leaves with the request id and security headers.