Skip to content

Commit

Permalink
Merge pull request #382 from danielgtaylor/operation-middleware
Browse files Browse the repository at this point in the history
feat: add per-operation router-agnostic middleware
  • Loading branch information
danielgtaylor authored Apr 15, 2024
2 parents 6ff449e + 6b5a086 commit a3eb57c
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 4 deletions.
5 changes: 4 additions & 1 deletion api.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ type API interface {
// the next Middleware.
UseMiddleware(middlewares ...func(ctx Context, next func(Context)))

// Middlewares returns a slice of middleware handler functions.
// Middlewares returns a slice of middleware handler functions that will be
// run for all operations. Middleware are run in the order they are added.
// See also `huma.Operation{}.Middlewares` for adding operation-specific
// middleware at operation registration time.
Middlewares() Middlewares
}

Expand Down
29 changes: 29 additions & 0 deletions docs/docs/features/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,35 @@ func MyMiddleware(ctx huma.Context, next func(ctx huma.Context)) {

The [`huma.ErrorDetail`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#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`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation) field. This middleware will run after the router-specific middleware and before the operation handler.

```go title="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

- Reference
Expand Down
4 changes: 2 additions & 2 deletions huma.go
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I)

a := api.Adapter()

a.Handle(&op, api.Middlewares().Handler(func(ctx Context) {
a.Handle(&op, api.Middlewares().Handler(op.Middlewares.Handler(func(ctx Context) {
var input I

// Get the validation dependencies from the shared pool.
Expand Down Expand Up @@ -1347,7 +1347,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I)
} else {
ctx.SetStatus(status)
}
}))
})))
}

// AutoRegister auto-detects operation registration methods and registers them
Expand Down
29 changes: 28 additions & 1 deletion huma_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ func TestFeatures(t *testing.T) {
Method: http.MethodGet,
Path: "/middleware",
}, func(ctx context.Context, input *struct{}) (*struct{}, error) {
// This should never be called because of the middleware.
return nil, nil
})
},
Expand All @@ -121,6 +120,34 @@ func TestFeatures(t *testing.T) {
"Cookie": "foo=bar",
},
},
{
Name: "middleware-operation",
Register: func(t *testing.T, api huma.API) {
huma.Register(api, huma.Operation{
Method: http.MethodGet,
Path: "/middleware",
Middlewares: huma.Middlewares{
func(ctx huma.Context, next func(huma.Context)) {
// Just a do-nothing passthrough. Shows that chaining works.
next(ctx)
},
func(ctx huma.Context, next func(huma.Context)) {
// Return an error response, never calling the next handler.
ctx.SetStatus(299)
},
},
}, func(ctx context.Context, input *struct{}) (*struct{}, error) {
// This should never be called because of the middleware.
return nil, nil
})
},
Method: http.MethodGet,
URL: "/middleware",
Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
// We should get the error response from the middleware.
assert.Equal(t, 299, resp.Code)
},
},
{
Name: "params",
Register: func(t *testing.T, api huma.API) {
Expand Down
5 changes: 5 additions & 0 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ type Operation struct {
// functions which generate operations.
Metadata map[string]any `yaml:"-"`

// Middlewares is a list of middleware functions to run before the handler.
// This is useful for adding custom logic to operations, such as logging,
// authentication, or rate limiting.
Middlewares Middlewares `yaml:"-"`

// --- OpenAPI fields ---

// Tags is a list of tags for API documentation control. Tags can be used for
Expand Down

0 comments on commit a3eb57c

Please sign in to comment.