Core Framework

Dispatcher

Understand how GoLazy runs middleware, routes requests, and serves asset fallbacks.

By Guillermo Alvarez - Published - Updated

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 ETag handling for eligible route responses.
  • Asset/public fallback after route lookup.
  • 405 Method Not Allowed responses 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.
  • HEAD response adaptation.
  • Last-Modified conditional 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:

  1. Route-only response buffering and dynamic ETags are enabled only when the route table owns the path.
  2. Application middleware sees the request.
  3. If a registered route owns the path, the router handles the request.
  4. If no route owns the path, the asset registry gets a chance to serve it.
  5. 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.