Skip to content

Middleware#

Huma has support for two variants of middleware:

  1. Router-specific - works at the router level, i.e. before router-agnostic middleware. You can use any middleware that is implemented for your router.
  2. Router-agnostic - runs in the Huma processing chain, i.e. after calls to router-specific middleware.
graph LR
	Request([Request])
	RouterSpecificMiddleware[Router-Specific Middleware]
	HumaMiddleware[Huma Middleware]
	OperationHandler[Operation Handler]

	Request --> RouterSpecificMiddleware
	RouterSpecificMiddleware --> HumaMiddleware
	subgraph Huma
		HumaMiddleware --> OperationHandler
	end

Router-specific#

Each router implementation has its own middlewares, you can use these as you normally would before creating the Huma API instance.

Chi router example:

code.go
router := chi.NewMux()
router.Use(jwtauth.Verifier(tokenAuth))
api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

Fiber router example:

code.go
app := fiber.New()
app.Use(logger.New())
api := humafiber.New(app, huma.DefaultConfig("My API", "1.0.0"))

Huma v1

Huma v1 middleware is compatible with Chi v4, so if you use that router with Huma v2 you can continue to use the Huma v1 middleware. See humachi.NewV4.

Router-agnostic#

You can write you own Huma middleware without any dependency to the specific router implementation. This uses the router-agnostic huma.Context interface, which exposes the request and response properties to your middleware.

Example:

code.go
func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
	// Set a custom header on the response.
	ctx.SetHeader("My-Custom-Header", "Hello, world!")

	// Call the next middleware in the chain. This eventually calls the
	// operation handler as well.
	next(ctx)
}

func NewHumaAPI() huma.API {
	// ...
	api := humachi.New(router, config)
	api.UseMiddleware(MyMiddleware)
}

Context Values#

The huma.Context interface provides a Context() method to retrieve the underlying request context.Context value. This can be used to retrieve context values in middleware and operation handlers, such as request-scoped loggers, metrics, or user information.

code.go
if v, ok := ctx.Context().Value("some-key").(string); ok {
	// Do something with `v`!
}

You can also wrap the huma.Context to provide additional or override functionality. Some utilities are provided for this, including huma.WithValue:

code.go
func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
	// Wrap the context to add a value.
	ctx = huma.WithValue(ctx, "some-key", "some-value")

	// Call the next middleware in the chain. This eventually calls the
	// operation handler as well.
	next(ctx)
}

Cookies#

You can use the huma.Context interface along with huma.ReadCookie or huma.ReadCookies to access cookies from middleware, and can also write cookies by adding Set-Cookie headers in the response:

func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
	// Read a cookie by name.
	sessionCookie := huma.ReadCookie(ctx, "session")
	fmt.Println(sessionCookie)

	// Read all the cookies from the request.
	cookies := huma.ReadCookies(ctx)
	fmt.Println(cookies)

	// Set a cookie in the response. Using `ctx.AppendHeader` won't overwrite
	// any existing headers, for example if other middleware might also set
	// headers or if this code were moved after the `next` call and the operation
	// might set the same header. You can also call `ctx.AppendHeader` multiple
	// times to write more than one cookie.
	cookie := http.Cookie{
		Name:  "session",
		Value: "123",
	}
	ctx.AppendHeader("Set-Cookie", cookie.String())

	// Call the next middleware in the chain. This eventually calls the
	// operation handler as well.
	next(ctx)
}

Errors#

If your middleware encounters an error, you can stop the processing of the next middleware or operation handler by skipping the call to next and writing an error response.

The huma.WriteErr(api, ctx, status, message, ...error) function can be used to write nice structured error responses which respect client-driven content negotiation for marshaling:

code.go
func MyMiddleware(ctx huma.Context, next func(ctx huma.Context)) {
	// If there is a query parameter "error=true", then return an error
	if ctx.Query("error") == "true" {
		huma.WriteErr(api, ctx, http.StatusInternalServerError,
			"Some friendly message", fmt.Errorf("error detail"),
		)
		return
	}

	// Otherwise, just continue as normal.
	next(ctx)
})

Error Details

The huma.ErrorDetail struct can be used to provide more information about the error, such as the location of the error and the value which was seen.

Operations#

You can also add router-agnostic middleware to individual operations by setting the huma.Operation.Middlewares field. This middleware will run after the router-specific middleware and before the operation handler.

code.go
func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
	// Call the next middleware in the chain. This eventually calls the
	// operation handler as well.
	next(ctx)
}

func main() {
	// ...
	api := humachi.New(router, config)

	huma.Register(api, huma.Operation{
		OperationID: "demo",
		Method:      http.MethodGet,
		Path:        "/demo",
		Middlewares: huma.Middlewares{MyMiddleware},
	}, func(ctx context.Context, input *MyInput) (*MyOutput, error) {
		// TODO: implement handler...
		return nil, nil
	})
}

It's also possible for global middleware to run only for certain paths by checking the request context's URL within the middleware, or by using something like the huma.Operation.Metadata to trigger the middleware logic using custom settings. It's up to you to decide how to structure your middleware and operations.

Dive Deeper#