huma icon indicating copy to clipboard operation
huma copied to clipboard

Route groups break docs

Open bilus opened this issue 11 months ago • 2 comments

I followed the docs about Route Groups & Base URLs to group my API under /v1/sports. I'm using Chi and identical structure to the example.

The relevant part of my code:

	app := NewApp(client)
	api := API{app: app}

	// Create a new router & API.
	router := chi.NewMux()
	router.Route("/v1/sports", func(router chi.Router) {
		config := huma.DefaultConfig("My API", "1.0.0")
		config.CreateHooks = nil // Do not add $schema links.
		// config.OpenAPIPath = "/v1/sports/openapi"

		routes := humachi.New(router, config)
		// My routes.
	})
	chi.Walk(router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
		fmt.Printf("[%s]: '%s'\n", method, route)
		return nil
	})

It generates these routes:

...
[GET]: '/v1/sports/docs'
[GET]: '/v1/sports/openapi-3.0.json'
[GET]: '/v1/sports/openapi-3.0.yaml'
[GET]: '/v1/sports/openapi.json'
[GET]: '/v1/sports/openapi.yaml'

The problem is the docs HTML at /v1/sports/docs accesses the openapi.yaml using the following path: /openapi.yaml producing:

image

Setting OpenAPIPath doesn't help. If I uncomment the following line:

		config.OpenAPIPath = "/v1/sports/openapi"

...this is the routes it generates:

...
[GET]: '/v1/sports/docs'
[GET]: '/v1/sports/v1/sports/openapi-3.0.json'
[GET]: '/v1/sports/v1/sports/openapi-3.0.yaml'
[GET]: '/v1/sports/v1/sports/openapi.json'
[GET]: '/v1/sports/v1/sports/openapi.yaml'

So now, when I'm accessing /v1/sports/docs, the error becomes:

image

It's because it expects the "correct" path based on the following code in api.go:

    <elements-api
      apiDescriptionUrl="` + openAPIPath + `.yaml"
      router="hash"
  apiDescriptionUrl="` + openAPIPath + `.yaml"

bilus avatar Dec 18 '24 11:12 bilus

@bilus yes I don't think this is supported at the moment. You can get around it by providing your own docs route, for example setting config.DocsPath = "" and then doing something like:

router.Get("/docs", func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	w.Write([]byte(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="referrer" content="same-origin" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Sports API Docs</title>
<!-- Embed elements Elements via Web Component -->
<link href="https://unpkg.com/@stoplight/[email protected]/styles.min.css" rel="stylesheet" />
<script src="https://unpkg.com/@stoplight/[email protected]/web-components.min.js"
				integrity="sha256-985sDMZYbGa0LDS8jYmC4VbkVlh7DZ0TWejFv+raZII="
				crossorigin="anonymous"></script>
</head>
<body style="height: 100vh;">

<elements-api
	apiDescriptionUrl="/v1/sports/openapi.yaml"
	router="hash"
	layout="sidebar"
	tryItCredentialsPolicy="same-origin"
/>

</body>
</html>`))
})

This is copied from https://github.com/danielgtaylor/huma/blob/main/api.go#L451-L489

danielgtaylor avatar Dec 19 '24 16:12 danielgtaylor

Hi @danielgtaylor thanks for the great library. What is the recommended way to setup the route groups without breaking docs for humachi? I realized humafiber has a NewWithGroup method, but that doesn't seem to exist for humachi.

I'm doing something where I use the same Huma config for the whole API, but have separate structs which take in a prefix variable and append it to the path when registering to Huma.

	baseRouter.Route("/api", func(r chi.Router) {
		humaCfg := huma.DefaultConfig("Something API", "1.0.0")
		humaCfg.Servers = []*huma.Server{
			{
				URL:         "https://something.com",
				Description: "Production server",
			},
		}

		api := humachi.New(r, humaCfg)
		authClient := auth.NewAuthClient(config)
		authMiddleware := auth.GetAuthMiddleware(api, authClient)

		// Users
		userRouter := usersRg.NewRouteGroup(userService)
		userRouter.RegisterRouter("/users", api, authMiddleware)
	})
func (rg *RouteGroup) RegisterRouter(basePath string, api huma.API, authMiddleware common.Middleware) {
	huma.Register(api, huma.Operation{
		OperationID: "getUserProfileByUsername",
		Method:      http.MethodGet,
		Path:        basePath + "/{username}",
		Security:    auth.SecurityScheme,
	}, rg.GetUserProfileByUsername)

	huma.Register(api, huma.Operation{
		OperationID: "updateUsername",
		Method:      http.MethodPut,
		Path:        basePath + "/current/username",
		Security:    auth.SecurityScheme,
		Middlewares: huma.Middlewares{authMiddleware},
	}, rg.UpdateUsername)
}

jamesleeht avatar Dec 21 '24 01:12 jamesleeht