Serve on a go/transport server¶
transport-openapi mounts onto a plain *http.ServeMux, and a
go/transport HTTP server takes an
http.Handler. Register the docs on the mux you hand to the transport server and
they are served alongside your API — same process, same port, same origin.
Mount the docs on the mux you pass to the server¶
import (
"context"
"net/http"
transporthttp "gitlab.com/phpboyscout/go/transport/http"
openapi "gitlab.com/phpboyscout/go/transport-openapi"
)
//go:embed openapi.yaml
var spec []byte
func newServer(ctx context.Context) (*http.Server, error) {
mux := http.NewServeMux()
// API handlers…
mux.HandleFunc("GET /v1/things", listThings)
// API docs on the same mux.
if err := openapi.Register(mux, spec,
openapi.WithSpecPath("/v1/openapi.yaml"),
openapi.WithDocsPath("/v1/docs/"),
openapi.WithTitle("Things API v1"),
); err != nil {
return nil, err
}
return transporthttp.NewServer(ctx, transporthttp.ServerSettings{Port: 8443}, mux)
}
NewServer returns a configured *http.Server — the standard-library type, not a
transport-specific one:
func NewServer(ctx context.Context, settings ServerSettings, handler http.Handler, opts ...ServerOption) (*http.Server, error)
ServerSettings carries Host, Port and MaxHeaderBytes. Timeouts, TLS and bind
address come from ServerOption values such as WithReadTimeout and
WithServedCertificate.
Put it under the controls lifecycle instead¶
transporthttp.Register builds the same server and registers its start, stop and
status with a go/controls controller, so
graceful shutdown and health reporting are handled for you:
func newSupervised(ctx context.Context, controller *controls.Controller, logger *slog.Logger) (*http.Server, error) {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/things", listThings)
if err := openapi.Register(mux, spec,
openapi.WithSpecPath("/v1/openapi.yaml"),
openapi.WithDocsPath("/v1/docs/"),
); err != nil {
return nil, err
}
return transporthttp.Register(ctx, "api", controller, logger, mux,
transporthttp.ServerSettings{Port: 8443})
}
Your mux becomes the server's / handler, so /v1/docs/ is a first-class route on
it and the transport server's lifecycle covers the docs as well as the API.
Paths the transport server takes for itself¶
transporthttp.Register wraps your handler in an outer mux of its own, which claims
three exact paths before yours are consulted:
| Path | Served by |
|---|---|
/healthz |
transporthttp.HealthHandler |
/livez |
transporthttp.LivenessHandler |
/readyz |
transporthttp.ReadinessHandler |
These are exact matches, so only an exact collision is shadowed. A spec at
WithSpecPath("/healthz") would never be reached; a docs prefix of
WithDocsPath("/healthz/") still works, because /healthz/ is a different pattern
from /healthz. Avoid all three anyway — the confusion is not worth the address
space.
Register also applies a request-body cap of DefaultMaxRequestBodyBytes, 1 MiB,
across the whole surface. The docs routes answer GET and HEAD only, so it never
affects them; it applies to the API calls the try-it console makes.
Why the same server¶
Serving the docs from the transport server rather than a separate static host keeps the spec and the try-it console same-origin with the API they document, so the console can call your endpoints without any CORS configuration. See Same-origin docs by design.
Avoid setting security headers twice¶
openapi.Register applies go/transport's security-header middleware to the docs
and spec handlers by default. If your transport server already wraps the whole mux
in an equivalent chain — a transit http.Chain passed as
transporthttp.WithMiddleware — opt the docs handlers out with
WithoutSecurityHeaders so the values have one
owner.