-
-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support OpenAPI servers base path & router groups #338
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #338 +/- ##
==========================================
- Coverage 95.29% 95.24% -0.06%
==========================================
Files 19 19
Lines 2827 2838 +11
==========================================
+ Hits 2694 2703 +9
- Misses 97 98 +1
- Partials 36 37 +1 ☔ View full report in Codecov by Sentry. |
Not tested but should work. type Router interface {
Add(method, path string, handler echo.HandlerFunc, middlewares ...echo.MiddlewareFunc) *echo.Route
}
type echoAdapter struct {
http.Handler
router Router
}
// ...
func New(r *echo.Echo, config huma.Config) huma.API {
return huma.NewAPI(config, &echoAdapter{Handler: r, router: r})
}
// NewWithGroup creates a new Huma API using the provided Echo router and group,
// letting you mount the API at a sub-path. Can be used in combination with
// the `OpenAPI.Servers` field to set the correct base URL for the API / docs
// / schemas / etc.
func NewWithGroup(r *echo.Echo, g *echo.Group, config huma.Config) huma.API {
return huma.NewAPI(config, &echoAdapter{Handler: r, router: g})
} import (
// ...
"github.com/gofiber/fiber/v2/middleware/adaptor"
)
// ...
type Router interface {
Add(method, path string, handlers ...fiber.Handler) fiber.Router
}
type fiberAdapter struct {
http.Handler
router Router
}
// ...
func New(r *fiber.App, config huma.Config) huma.API {
return huma.NewAPI(config, &fiberAdapter{Handler: adaptor.FiberApp(r), router: r})
}
func NewWithGroup(r *fiber.App, g *fiber.Group, config huma.Config) huma.API {
return huma.NewAPI(config, &fiberAdapter{Handler: adaptor.FiberApp(r), router: g})
} PS: I like this changes because |
@x-user I adapted your suggestions to the latest commit. Unfortunately the Fiber adaptor doesn't actually implement the underlying request connection's |
It's happens because func (ctx *RequestCtx) Init(req *Request, remoteAddr net.Addr, logger Logger) {
if remoteAddr == nil {
remoteAddr = zeroTCPAddr
}
c := &fakeAddrer{
laddr: zeroTCPAddr,
raddr: remoteAddr,
}
if logger == nil {
logger = defaultLogger
}
ctx.Init2(c, logger, true)
req.CopyTo(&ctx.Request)
} PS: Actually SetReadDeadline does nothing in current fiber adapter. |
Sadly, this approach won't cover my use-case where I need to have 2 routers each with their own middlewares: rootMx, apiMx := chi.NewMux(), chi.NewMux()
rootMx.Use(httplog.RequestLogger(logger))
apiMx.Use(middleware.JWT)
apiMx.NotFound(jsonNotFound)
rootMx.Mount("/api", apiMx)
cfg := huma.DefaultConfig("My API", "1.0.0")
cfg.Prefix = "/api" // missing
api := humachi.New(apiMx, cfg)
huma.AutoRegister(api, struct{}{})
huma.AutoRegister() |
Can you describe what the problem? I can't understand. package main
import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"slices"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humachi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httplog/v2"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/v2/jwt"
)
type (
TestOutput struct {
Body TestOutputBody
}
TestOutputBody struct {
Message string `json:"message"`
}
humaMiddleware func(ctx huma.Context, next func(huma.Context))
)
var (
tokenAuth *jwtauth.JWTAuth
port = flag.Uint("port", 8080, "")
logger = httplog.NewLogger("testapp", httplog.Options{
JSON: true,
LogLevel: slog.LevelDebug,
Concise: true,
RequestHeaders: true,
MessageFieldName: "message",
})
)
func main() {
flag.Parse()
slog.SetDefault(logger.Logger)
rootMu := chi.NewMux()
rootMu.Use(middleware.RealIP)
rootMu.Use(middleware.RequestID)
rootMu.Use(httplog.RequestLogger(logger))
rootMu.Use(middleware.Recoverer)
rootMu.Route("/api", func(apiMu chi.Router) {
apiMu.Use(jwtauth.Verifier(tokenAuth))
apiMu.Use(middleware.Timeout(10 * time.Second))
apiMu.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"title": "Not Found","status": 404, "detail": "Endpoint Not Found"}`)
})
config := huma.DefaultConfig("API", "0.1.0")
config.Servers = []*huma.Server{{URL: fmt.Sprintf("http://localhost:%d/api", *port)}}
config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
"JWTAuth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
In: "header",
Name: "Authorization",
},
}
api := humachi.New(apiMu, config)
api.UseMiddleware(jwtAuthMiddleware(api, tokenAuth))
huma.Register(
api,
huma.Operation{
OperationID: "test",
Path: "/test",
Method: http.MethodGet,
Summary: "Test endpoint",
Security: []map[string][]string{
{"JWTAuth": nil},
},
},
func(ctx context.Context, _ *struct{}) (*TestOutput, error) {
_, claims, _ := jwtauth.FromContext(ctx)
body := TestOutputBody{
Message: fmt.Sprintf("Hello, %v!", claims["user_id"]),
}
return &TestOutput{Body: body}, nil
},
)
})
srv := http.Server{
Addr: fmt.Sprintf(":%d", *port),
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
Handler: rootMu,
}
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.LogAttrs(
context.Background(),
slog.LevelError,
"server error",
slog.Any("error", err),
)
}
}
func init() {
tokenAuth = jwtauth.New("HS256", []byte("secret"), nil)
// For debugging/example purposes, we generate and print
// a sample jwt token with claims `user_id:123` here:
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user_id": 123})
slog.LogAttrs(
context.Background(),
slog.LevelDebug,
"sample jwt",
slog.String("jwt", tokenString),
)
}
func jwtAuthMiddleware(api huma.API, ja *jwtauth.JWTAuth) humaMiddleware {
return func(ctx huma.Context, next func(huma.Context)) {
var anyOfNeededScopes []string
isAuthorizationRequired := false
for _, opScheme := range ctx.Operation().Security {
var ok bool
if anyOfNeededScopes, ok = opScheme["JWTAuth"]; ok {
isAuthorizationRequired = true
break
}
}
if !isAuthorizationRequired {
next(ctx)
return
}
token, claims, err := jwtauth.FromContext(ctx.Context())
if err != nil {
huma.WriteErr(api, ctx, http.StatusUnauthorized, err.Error())
return
}
if token == nil || jwt.Validate(token, ja.ValidateOptions()...) != nil {
huma.WriteErr(api, ctx, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
return
}
if len(anyOfNeededScopes) == 0 {
next(ctx)
return
}
scopes, _ := claims["scopes"]
if scopes, ok := scopes.([]string); ok {
for _, scope := range scopes {
if slices.Contains(anyOfNeededScopes, scope) {
next(ctx)
return
}
}
}
huma.WriteErr(api, ctx, http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
} |
This PR is an alternative approach to #305 by @burgesQ to enable serving the OpenAPI and schemas at the correct path when given a router group / sub-router at some base path like
/api
. It works like this:This creates a sub-router at
/api
and then sets thehttps://example.com/api
server URL including the base path. The sub-router is used to create the Huma instance. The/demo
route registered via Huma then becomes/api/huma
when the service is running, and all the docs/openapi/schemas/etc will be linked correctly.I personally like the idea of using the already existing OpenAPI server base path functionality rather than duplicating it via a prefix/basePath parameter in the config. What do you think?
@spa5k, I believe this should fix #331. Please let me know if this would work for you.
I also did a quick example of how other routers can do this, e.g. for Gin you can use
humagin.NewWithGroup(router, group, config)
(you have to pass both the router and group since*gin.RouterGroup
has noServeHTTP
method).For the Go 1.22+
http.ServeMux
viahumago
there is now a prefix you can set which acts similarly to the other router's groups. Usehumago.NewWithPrefix(mux, prefix, config)
.This should also enable us to support rewriting API gateways, e.g. document the server's URL with base path like
https://example.com/api
but then do not use a sub-router as the incoming request to Go will never see the/api
which was stripped off. Any other cases I'm missing?Fixes #305, #331.