- architecture: introduction, Go modules and workspaces, application lifecycle, request lifecycle - plugins: registration, scheduling, extending, testing - verified Examples for backpack services, towel request context, pact schedules and festival events - TestDocsRequiredPages lists the eight new pages
83 lines
5.0 KiB
Markdown
83 lines
5.0 KiB
Markdown
---
|
|
title: Request lifecycle
|
|
description: "How an HTTP request reaches a plugin handler: route collection, named middleware, constraints, body limits, CORS, recovery and request context values."
|
|
section: architecture
|
|
order: 40
|
|
---
|
|
# Request lifecycle
|
|
|
|
The `serve` command builds one `http.Handler` from every plugin's route declarations and serves it with the Go standard library. This page follows a request from the socket to your handler and back.
|
|
|
|
## Building the router
|
|
|
|
When `serve` starts, `surf.BuildRouter` collects the routes:
|
|
|
|
1. It registers the built-in middleware (`throttle`, `body.limit`, `locale.from-principal` and, when the admin is enabled, `backend`).
|
|
2. It registers the named middleware of every plugin that implements `pact.HasMiddleware`, `pact.HasMiddlewareFactories` or `pact.HasHouseMiddleware`, and the rate-limit buckets of every `surf.BucketProvider`.
|
|
3. It calls `pact.HasRoutes.Routes` on every plugin in activation order, passing a `pact.Router` that records groups, routes, middleware names and constraints.
|
|
4. It mounts the cabana admin API and checks every route.
|
|
|
|
`surf.Assemble` then compiles the routes onto a standard library `http.ServeMux`. A duplicate route, an unknown middleware name or a malformed `throttle` parameter fails here, at boot, and `serve` exits with the error instead of serving a broken router. `./bin/acme route:list` builds the same router without opening the database and prints the route table.
|
|
|
|
## Declaring routes
|
|
|
|
A plugin declares routes the way a WinterCMS `routes.php` file does, with groups that share a prefix and middleware:
|
|
|
|
```php
|
|
Route::group(['prefix' => 'api/blog', 'middleware' => ['auth']], function () {
|
|
Route::get('posts/{id}', 'Acme\Blog\Http\Posts@show')->where('id', '[0-9]+');
|
|
});
|
|
```
|
|
|
|
In Go the same declaration is a `pact.HasRoutes` method. Paths use Go `http.ServeMux` patterns, `pact.Router.Where` and `pact.Router.WhereIn` constrain the last declared route, and middleware names are strings that must be registered by the time the router is built. The [surf](../../modules/surf/README.md) reference has the full route builder with rate-limit buckets and per-route body limits.
|
|
|
|
## The wrapping order
|
|
|
|
surf wraps every non-raw route in the same layers. From the outside in:
|
|
|
|
1. CORS, only for the paths configured under `http.cors.paths`, including preflight requests.
|
|
2. Panic recovery. A panic becomes an opaque JSON 500 from [wire](../../modules/wire/README.md), and because the response is buffered until the handler returns, the client never receives half a body.
|
|
3. The request locale, taken from the `Accept-Language` header and stored in the context.
|
|
4. The body limit: `http.body_limits.default_bytes`, or the route's own `body.limit:<bytes>`.
|
|
5. The route's middleware, in the order you listed them: group middleware first, then the route's own.
|
|
6. The path constraints. A request whose parameter fails `pact.Router.Where` or `pact.Router.WhereIn` gets a 404 before your handler runs.
|
|
7. Your handler.
|
|
|
|
Raw groups, declared with `pact.Router.GroupRaw`, are for webhooks and file streams. They skip the default body limit, refuse house middleware, and a panic in them returns a bare 500.
|
|
|
|
## Request context values
|
|
|
|
WinterCMS reads the current locale and user through facades. SummerCMS carries them on the request's `context.Context`. surf stores the locale with `towel.WithLocale`, the authentication middleware stores the signed-in principal, and your own middleware can add the organization or collection with `towel.WithOrganization` and `towel.WithCollection`. Any code that receives the context reads them back without a global lookup:
|
|
|
|
```go src=modules/towel/example_test.go#ExampleWithLocale
|
|
// A middleware stores the organization once for the whole request.
|
|
withAcme := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := towel.WithOrganization(r.Context(), "acme")
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// The handler reads the values back from its request context.
|
|
listPosts := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
org, _ := towel.Organization(r.Context())
|
|
locale, ok := towel.Locale(r.Context())
|
|
if !ok {
|
|
locale = "en"
|
|
}
|
|
fmt.Fprintf(w, "posts for %s in %s", org, locale)
|
|
})
|
|
|
|
// surf sets the locale from Accept-Language; here the test sets it.
|
|
req := httptest.NewRequest(http.MethodGet, "/api/blog/posts", nil)
|
|
req = req.WithContext(towel.WithLocale(req.Context(), "pl"))
|
|
rec := httptest.NewRecorder()
|
|
withAcme(listPosts).ServeHTTP(rec, req)
|
|
fmt.Println(rec.Body.String())
|
|
// Output: posts for acme in pl
|
|
```
|
|
|
|
## Writing responses
|
|
|
|
Handlers write JSON with `wire.WriteJSON`, which keeps bodies byte-compatible with a Laravel backend: HTML characters are not escaped and there is no trailing newline. `wire.Slice` turns a nil list into `[]`, `wire.Time` marshals timestamps in Carbon's `+00:00` form and `wire.TriBool` models a nullable boolean. Errors that should look like your API's error envelope go through the house middleware a plugin registers with `pact.HasHouseMiddleware`.
|