An answer names itself whether or not the app describes itself

self is true of every answer — it is the path the request arrived on, and
nothing has to be installed for it to be so. Only the description relations
depend on a document existing.

Those two rode one condition, and it cost more than the description:
installOpenAPIRoutes returns early for an app with an empty registry, so an app
serving plain routes named nothing at all, not even itself. Measured across the
estate: six services register zero typed ops, so bumping their pin would have
changed nothing about what they emit.

Whether an app happens to use typed ops is not a statement about whether its
answers have addresses.
This commit is contained in:
2026-08-22 14:36:46 -07:00
parent 39f87c747c
commit 4d8253d963
3 changed files with 58 additions and 24 deletions
+1 -3
View File
@@ -183,9 +183,7 @@ func (a *App) materialise(occ []occurrence, ctl []route) *fiber.App {
// description lives by being BUILT, so there is no call to forget. It runs
// after the report so the report still observes every request, and before
// every route so it covers the answers no route matched. See [App.linkHandler].
if h := a.linkHandler(); h != nil {
f.Use(h)
}
f.Use(a.linkHandler())
for _, o := range occ {
switch o.kind() {
case kindMiddleware:
+24 -15
View File
@@ -42,22 +42,30 @@ const (
relDoc = "service-doc"
)
// linkHandler returns the middleware that names this app's own description on
// every answer, or nil when there is nothing to name.
// linkHandler returns the middleware that names an answer's own address, and
// this app's description alongside it when there is one.
//
// The condition is the SAME map installOpenAPIRoutes writes, not a second guess
// at it: the header is present exactly when the address it points at is served,
// so a link can never advertise a document this app 404s. An app whose document
// is Disabled, or which registers no typed op, gets no middleware at all rather
// than one that checks a flag on every request.
// SELF IS TRUE OF EVERY ANSWER. It is the path the request arrived on, and
// nothing has to be installed for it to be so. Only the description relations
// depend on a document existing, so only they are conditional — on the SAME map
// installOpenAPIRoutes writes, not a second guess at it, so a link can never
// advertise a document this app 404s.
//
// The two used to be one condition, and that cost more than the description:
// installOpenAPIRoutes returns early for an app that registers no typed op, so
// every such app — and most of ours are, serving plain routes — named nothing
// at all, not even itself. Whether an app happens to use typed ops is not a
// statement about whether its answers have addresses.
func (a *App) linkHandler() fiber.Handler {
if !a.controls[fiber.MethodGet+" "+SpecPath] {
return nil
}
// Built once, at materialise: these two are constants of the app, and
// Built once, at materialise: these are constants of the app, and
// re-rendering them per request would allocate on every answer.
desc := "<" + SpecPath + `>; rel="` + relDesc + `"`
doc := "<" + DocsPath + `>; rel="` + relDoc + `"`
var described []string
if a.controls[fiber.MethodGet+" "+SpecPath] {
described = []string{
"<" + SpecPath + `>; rel="` + relDesc + `"`,
"<" + DocsPath + `>; rel="` + relDoc + `"`,
}
}
return func(fc fiber.Ctx) error {
// After the handler, so a handler's own Link header is already in place
// and these join it rather than racing it. fiber buffers headers until
@@ -74,8 +82,9 @@ func (a *App) linkHandler() fiber.Handler {
return err
}
h.Add(fiber.HeaderLink, "<"+fc.Path()+`>; rel="`+relSelf+`"`)
h.Add(fiber.HeaderLink, desc)
h.Add(fiber.HeaderLink, doc)
for _, l := range described {
h.Add(fiber.HeaderLink, l)
}
return err
}
}
+33 -6
View File
@@ -103,10 +103,10 @@ func TestLink_SelfIsTheAddressReached(t *testing.T) {
}
}
// AN APP WITH NO DESCRIPTION SAYS NOTHING. The middleware is absent rather than
// present-and-quiet, so an app that disabled its document pays nothing per
// request and advertises nothing that would 404.
func TestLink_AnAppWithNoDocumentAdvertisesNothing(t *testing.T) {
// AN APP WITH NO DESCRIPTION STILL NAMES ITSELF. Disabling the document says
// nothing about whether an answer has an address, so self stays and only the
// relations that would 404 are withheld.
func TestLink_AnAppWithNoDocumentStillNamesItself(t *testing.T) {
a := New(Config{AppName: "silent", DisableStartupMessage: true,
OpenAPI: OpenAPIConfig{Disabled: true}})
Get(a, "/thing", func(ctx context.Context, _ *linkIn) (*linkOut, error) {
@@ -120,8 +120,35 @@ func TestLink_AnAppWithNoDocumentAdvertisesNothing(t *testing.T) {
if err != nil {
t.Fatalf("GET /thing: %v", err)
}
if got := resp.Header.Values("Link"); len(got) != 0 {
t.Errorf("an app serving no document sent %v", got)
got := resp.Header.Values("Link")
if len(got) != 1 || !strings.Contains(got[0], `rel="self"`) {
t.Errorf("want self alone, got %v", got)
}
for _, l := range got {
if strings.Contains(l, "service-desc") || strings.Contains(l, "service-doc") {
t.Errorf("advertised a document it does not serve: %s", l)
}
}
}
// A ROUTE IS NOT A TYPED OP, AND AN ANSWER IS STILL AN ANSWER. installOpenAPIRoutes
// returns early for an app with an empty registry, and self used to ride that
// same condition — so an app serving plain routes, which most of ours do,
// named nothing. This is the case that was silently uncovered.
func TestLink_PlainRoutesAreNamedToo(t *testing.T) {
a := New(Config{AppName: "plain", DisableStartupMessage: true})
a.Get("/plain", func(c *Ctx) error { return c.String(200, "ok") })
a.installOpenAPIRoutes() // returns early: nothing typed to describe
if err := a.Build(); err != nil {
t.Fatalf("build: %v", err)
}
resp, err := a.Fiber().Test(httptest.NewRequest("GET", "/plain", nil))
if err != nil {
t.Fatalf("GET /plain: %v", err)
}
got := resp.Header.Values("Link")
if len(got) != 1 || !strings.Contains(got[0], `</plain>; rel="self"`) {
t.Errorf("a plain route must name its own address, got %v", got)
}
}