tinka alpha

Middleware

This guide covers middleware in Tinka. You will learn how to:

Overview

Middleware is code that runs around handlers: it checks a request before the handler sees it, or adjusts the response after. Authentication, CORS, rate limits and timing headers are middleware, because they apply to many routes and have nothing to do with any one handler.

A middleware is a function that receives the HTTP context and next, which runs the rest of the chain and returns its error. The handler's response is written only after every middleware has returned, so code after next can still set headers and the status.

Write middleware

This middleware measures how long the rest of the request takes, and reports it in a Server-Timing header the browser's developer tools display:

timing.go
package main

import (
	"fmt"
	"time"

	"github.com/mucolabs/tinka"
)

func timing(c *tinka.Ctx, next tinka.Next) error {
	start := time.Now()
	err := next(c)
	c.Header().Set("Server-Timing", fmt.Sprintf("app;dur=%d", time.Since(start).Milliseconds()))
	return err
}

Code before next runs on the way in, and code after it on the way out. Return the error next returned, so the App can turn it into a response.

Apply middleware

app.Use adds middleware that runs for every request, before routing, in the order added. Requests that match no route pass through it too, so it suits CORS, security headers and request logging:

main.go
	app := tinka.New(tinka.HTTPConfig{})
	app.Use(timing)
	app.Get("/posts", listPosts)

A router's Use, Group and With add middleware to some routes only, as the Routing guide shows. For a request to /admin/posts, middleware runs from the outside in: the App's, then the group's, then the route's, then the handler. Upstream, after next, the order reverses.

main.go
	app.Use(timing)                     // every request
	app.Route("/admin", func(r *tinka.Router) {
		r.Use(requireAdmin)             // every route in /admin
		r.With(audited).Delete("/posts/{id}", deletePost) // this route
	})

Middleware that takes options is a function that returns one:

auth.go
package main

import (
	"github.com/mucolabs/tinka"
)

func requireRole(role string) tinka.Middleware {
	return func(c *tinka.Ctx, next tinka.Next) error {
		user, err := c.Make[*User]()
		if err != nil {
			return err
		}
		if user.Role != role {
			return tinka.Forbidden("Only %ss can do this.", role)
		}
		return next(c)
	}
}

Registered with r.Use(requireRole("editor")), it reads the signed-in user, then refuses the request or lets it through.

End a request

Return an error without calling next, and nothing after the middleware runs: the error becomes the response, as requireRole above does with tinka.Forbidden. Middleware that ran before it still finish their code after next, so the timing header is set even on a refused request.

Handle errors from handlers

next returns the handler's error, or the error of any middleware after yours. Inspect it with errors.Is and errors.As, wrap it, or replace it. Return it to let the App render it:

metrics.go
package main

import (
	"errors"

	"github.com/mucolabs/tinka"
)

func countFailures(c *tinka.Ctx, next tinka.Next) error {
	err := next(c)
	if e, ok := errors.AsType[*tinka.Error](err); ok && e.Status >= 500 {
		serverErrors.Add(1)
	}
	return err
}

A panic in a handler is not an error: it unwinds through the middleware, skipping their code after next, and the App recovers it and answers 500.

Use net/http middleware

tinka.Std adapts middleware written for net/http, with the func(http.Handler) http.Handler shape, such as a CORS or compression package:

main.go
	app.Use(tinka.Std(cors.Default().Handler)) // github.com/rs/cors

The request and the ResponseWriter it passes on reach the handler, so a compression middleware compresses the response. Unlike Tinka middleware, net/http middleware expects the response to be written when its next returns, so the response is written there: middleware outside it still sees the handler's error, but can no longer change the response.

Time out slow requests

tinka.Timeout cancels the request's context after a duration. A handler that passes its context to slow calls then returns early, and the client gets a 503:

main.go
	app.With(tinka.Timeout(5*time.Second)).Get("/reports/{id}", showReport)

See also: Routing, HTTP context, Exception handling