Core Framework
Dispatcher
Understand how GoLazy runs middleware, routes requests, and serves asset fallbacks.
Dispatcher responsibilities
golazy.dev/lazydispatch owns request dispatch inside a lazyapp
application.
In the normal application flow, you do not create a dispatcher directly.
lazyapp.New creates it, installs the framework middleware, and returns one
application http.Handler.
Dispatch currently owns:
- Middleware registration and execution order.
- Route-only middleware gating.
- Router middleware.
- Response buffering for registered application routes.
- Dynamic
ETaghandling for eligible route responses. - Asset/public fallback after route lookup.
405 Method Not Allowedresponses for assets.- Fallthrough to
404 Not Found.
It is the intended home for later request logic:
- Request monitoring and tracing hooks.
- Cookie lifecycle.
- Session lifecycle.
- Flash lifecycle.
HEADresponse adaptation.Last-Modifiedconditional responses.- Action error response conversion.
Routing remains in lazyroutes. Rendering remains in lazycontroller. Asset
registration, hashing, and cache policy remain in lazyassets.
Dispatch in lazyapp
Applications configure dispatch through lazyapp.Config:
func App() *lazyapp.App {
return lazyapp.New(lazyapp.Config{
Name: "sample_app",
Drawer: Draw,
Public: app.Public,
Views: app.Views,
Context: Context,
Middlewares: []lazydispatch.Middleware{
requestIDMiddleware,
},
})
}
lazyapp.New builds this chain:
route-only response buffer and ETag handling
application middleware
router middleware
asset/public fallback middleware
404 final handler
That means:
- Route-only response buffering and dynamic ETags are enabled only when the route table owns the path.
- Application middleware sees the request.
- If a registered route owns the path, the router handles the request.
- If no route owns the path, the asset registry gets a chance to serve it.
- If neither handles it, dispatch returns
404 Not Found.
Middleware interface
Middleware implements one interface:
type Middleware interface {
Handler(next http.Handler) http.Handler
}
This is deliberately close to standard library handler composition. A middleware receives the next handler and returns the wrapped handler.
For simple middleware, MiddlewareFunc adapts a function:
var requestIDMiddleware = lazydispatch.MiddlewareFunc(
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(
w http.ResponseWriter,
r *http.Request,
) {
next.ServeHTTP(w, r)
})
},
)
Then add it through lazyapp.Config.Middlewares.
Middleware order
Application middlewares run in the order they are listed:
Middlewares: []lazydispatch.Middleware{
first,
second,
},
The request enters first, then second, then the framework router and asset
fallback middleware. The response unwinds in the opposite direction.
Middleware should normally call next.ServeHTTP. If it does not, it has
handled the request and the router/asset fallback will not run.
Router middleware
lazyapp.New installs the route scope as dispatch router middleware.
The router middleware expects:
type RouteHandler interface {
http.Handler
HandlesPath(path string) bool
}
lazyroutes.Scope satisfies this interface.
For each request, dispatch asks whether any registered route owns the path. If yes, the request is sent to the route scope. The standard library mux then performs method matching, path-value extraction, and method-not-allowed behavior.
If no route owns the path, dispatch continues to the asset fallback.
This is the important split: the router owns application route matching, while dispatch owns fallback behavior.
Route-only response middleware
lazyapp.New applies response buffering and dynamic ETags only to registered
application route paths:
dispatcher.Use(lazydispatch.RouteOnly(
router,
lazydispatch.ResponseBuffer(),
lazydispatch.ETag(),
))
ResponseBuffer delays committing a route response until the action returns,
which lets controller and rendering errors reset the buffered response instead
of sending partial HTML. ETag uses the final buffered body to add a SHA-256
validator to eligible GET and HEAD 200 OK responses.
The route gate matters: public assets already have their own validators and
cache policy, so lazyapp does not buffer asset responses through the dynamic
route response layer.
Asset fallback
lazyapp.New registers public files with lazyassets when
lazyapp.Config.Public is set:
Public: app.Public,
Public returns the embedded public filesystem:
func Public() (fs.FS, error) {
return fs.Sub(Files, "public")
}
The asset registry checks whether the requested asset exists before serving it.
Missing assets fall through to the final not-found handler. Existing assets are
served with content type, ETag, cache policy, and optional immutable
permanent URLs.
For an existing public file, both the logical and permanent paths can work:
GET /styles.css
GET /styles-<hash>.css
The logical path uses revalidation headers. The permanent path uses the immutable cache policy.
For a missing public file:
GET /missing.txt
dispatch falls through to 404 Not Found.
For unsupported methods on existing assets:
POST /styles.css
dispatch returns:
405 Method Not Allowed
Allow: GET, HEAD
Application routes are checked before public files, so a controller route can use a path that also looks like a file path.
Method not allowed
Asset dispatch returns 405 Method Not Allowed for unsupported methods on
existing assets.
Application route method handling currently comes from the standard library
mux. For example, a POST to a GET route is handled by the route scope's
embedded mux.
Planned request middlewares
The dispatcher is where request-wide behavior will move.
Request monitoring will record method, path, route name, controller/action, status, response size, duration, and allocation deltas.
Cookie, session, and flash support will use request-local dispatch state so controllers and views can read and update them without writing directly to the response.
HEAD support will preserve GET headers while suppressing the response body
at commit time.
Form method override
lazyapp installs route-scoped method override middleware. It allows HTML forms
to submit POST with:
<input type="hidden" name="_method" value="patch">
The middleware accepts put, patch, and delete. It only runs for routed
POST form requests, skips upgrade requests, reads a bounded prefix of the
body, and replays that body for the controller.
Cross-origin request rejection is separate from forms. Use
lazydispatch/middlewares.CrossOriginProtection when an application should
reject unsafe browser requests from other origins.
How to use without lazyapp
Most applications should configure dispatch through lazyapp.New. Use
lazydispatch directly only when testing middleware or building a custom
application assembly.
Create a dispatcher manually:
dispatcher := lazydispatch.NewDispatcher()
Register middleware manually:
dispatcher.Use(first)
dispatcher.Use(second)
Install the route-only response middleware manually:
dispatcher.Use(lazydispatch.RouteOnly(
router,
lazydispatch.ResponseBuffer(),
lazydispatch.ETag(),
))
Install a router manually:
dispatcher.Use(lazydispatch.Router(router))
Install an asset registry manually:
assets := lazyassets.New()
if err := assets.AddFS(publicFS); err != nil {
return err
}
dispatcher.Use(lazydispatch.MiddlewareFunc(func(next http.Handler) http.Handler {
return assets.Handler(next)
}))
The lower-level static-file middleware still exists for tests or custom assemblies that intentionally do not need hashing, cache helpers, or generated assets:
dispatcher.Use(lazydispatch.Public(publicFS))
Use the dispatcher as a handler:
server := &http.Server{
Addr: ":3000",
Handler: dispatcher,
}
Or build an explicit handler chain:
handler := dispatcher.Handler(http.NotFoundHandler())
Manual assembly means you are responsible for creating the route scope, initializing views, initializing application context, and ordering middleware correctly.