Core Framework

Dispatcher

Understand how GoLazy runs middleware, routes requests, and serves public files.

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.
  • Router middleware.
  • Public static-file middleware.
  • 405 Method Not Allowed responses for public files.
  • Fallthrough to 404 Not Found.

It is the intended home for later request logic:

  • Response buffering.
  • ETag and Last-Modified conditional responses.
  • Request monitoring and tracing hooks.
  • Cookie lifecycle.
  • Session lifecycle.
  • Flash lifecycle.
  • HEAD response adaptation.
  • Action error response conversion.

Routing remains in lazyroutes. Rendering remains in lazycontroller.

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:

application middleware
router middleware
public static-file middleware
404 final handler

That means:

  1. Application middleware sees the request first.
  2. If a registered route owns the path, the router handles the request.
  3. If no route owns the path, public files get a chance to serve it.
  4. 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 public 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/public-file 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 public files.

This is the important split: the router owns application route matching, while dispatch owns fallback behavior.

Static-file middleware

lazyapp.New installs public-file middleware 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")
}

Static middleware checks whether the requested file exists before serving it. Missing files fall through to the final not-found handler. Existing files are served with http.FileServerFS.

For an existing public file:

GET /styles.css

the file server sends the response.

For a missing public file:

GET /missing.txt

dispatch falls through to 404 Not Found.

For unsupported methods on existing files:

POST /styles.css

dispatch returns:

405 Method Not Allowed
Allow: GET

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

Static-file dispatch returns 405 Method Not Allowed for unsupported methods on existing files.

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.

Response buffering will capture status, headers, and body before bytes are committed. This will allow template or view failures to become clean error responses instead of partial HTML.

Conditional responses will use final headers and body state to evaluate If-None-Match and If-Modified-Since, returning 304 Not Modified when appropriate.

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.

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 a router manually:

dispatcher.Use(lazydispatch.Router(router))

Install public files manually:

dispatcher.Use(lazydispatch.Public(publicFS))

Use the dispatcher as a handler:

server := &http.Server{
    Addr:    ":8080",
    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.