Skip to content

feat: Link-Template (RFC 9652), link sets (RFC 9264) and api-catalog (RFC 9727) - #8468

Open
Spomky wants to merge 3 commits into
api-platform:4.4from
Spomky:feat/link-template-rfc9652
Open

feat: Link-Template (RFC 9652), link sets (RFC 9264) and api-catalog (RFC 9727)#8468
Spomky wants to merge 3 commits into
api-platform:4.4from
Spomky:feat/link-template-rfc9652

Conversation

@Spomky

@Spomky Spomky commented Aug 19, 2026

Copy link
Copy Markdown
Contributor
Q A
Branch? 4.4
Tickets Closes #6924
License MIT
Doc PR api-platform/docs#TODO

Three RFCs, three self-contained commits, so they can be split into separate PRs if preferred.

1. Link-Template header (RFC 9652) — closes #6924

Symfony\Component\WebLink\HttpHeaderSerializer skips every link whose href is a URI template:

foreach ($links as $link) {
    if ($link->isTemplated()) {
        continue;
    }
    // ...
}

So a templated web link declared on an operation was silently dropped:

#[Get(uriTemplate: 'books/{id}', links: [new Link('author', '/books/{id}/author')])]

AddLinkHeaderProcessor now serializes those into a Link-Template header:

Link-Template: "/books/{id}/author"; rel="author"; anchor="#{id}"

Full RFC 9652 support: templated anchor, var-base, target attributes as structured field
parameters, and Display Strings for non-ASCII values (title=%"Bj%c3%b6rn J%c3%a4rnsida").

Each header is now only set when its serializer returns something, so a response whose links are
all templated no longer gets an empty Link header.

2. Link set serializer (RFC 9264)

ApiPlatform\State\Util\JsonLinksetSerializer turns a list of PSR-13 links into an
application/linkset+json document, grouped by link context then by relation type.

3. api-catalog well-known URI (RFC 9727)

RFC 9727 mandates the RFC 9264 link set format for the API catalog document, which makes it the
natural consumer of the serializer above.

GET /.well-known/api-catalog

HTTP/1.1 200 OK
Content-Type: application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"
Link: </.well-known/api-catalog>; rel="api-catalog"
{"linkset": [
  {"anchor": "https://example.com/.well-known/api-catalog", "item": [{"href": "https://example.com/"}]},
  {"anchor": "https://example.com/",
   "service-desc": [{"href": "https://example.com/docs.jsonopenapi", "type": "application/vnd.openapi+json"}],
   "service-doc":  [{"href": "https://example.com/docs", "type": "text/html"}],
   "service-meta": [{"href": "https://example.com/docs.jsonld", "type": "application/ld+json"}],
   "item": [{"href": "https://example.com/books"}, {"href": "https://example.com/reviews"}]}
]}

Everything is derived from metadata API Platform already has, so there is nothing to configure.
The route is registered with the entrypoint, so enable_entrypoint: false disables it too.

Per section 2, a HEAD request answers with the api-catalog link relation. Per section 3, the
entrypoint advertises the catalog with the same relation, which is what lets clients find it when
the API is mounted under a route prefix and the document is therefore not at the host root. On
Laravel the route is registered outside the API prefix, since RFC 8615 roots well-known URIs at the
host.

Notes

  • The two serializers delegate to symfony/web-link 8.2 and later, which gained them in
    [FrameworkBundle][WebLink] Add RFC 9264 link sets and the RFC 9652 Link-Template header symfony/symfony#65428, and fall back to a backport of that implementation otherwise, since
    api-platform supports Symfony 7.4. Every duplicated part is marked with a
    TODO: remove once "symfony/web-link" >= 8.2 is required.
  • Two Laravel tests were updated: the entrypoint now carries the api-catalog link relation, which
    is an additive change to the Link header.
  • Unrelated, noticed while working on this: HttpOperation::$links only reaches the link provider
    through HydraLinkProcessor, so web links declared on an operation are ignored when Hydra is
    disabled. Left untouched here.

Spomky added 3 commits August 19, 2026 10:11
Symfony's HttpHeaderSerializer silently drops every link whose href is a URI
template, so web links declared with a template on an operation never reached
the client. RFC 9652 gives them a home: the Link-Template header field.

AddLinkHeaderProcessor now serializes the templated links of the link provider
into a Link-Template header, and only sets each header when its serializer
produced something.

The serializer delegates to symfony/web-link 8.2 and later, and falls back to a
backport of it otherwise, as api-platform supports Symfony 7.4.

Closes api-platform#6924
Serializes a list of PSR-13 links to an "application/linkset+json" document,
grouped by link context then by relation type. Delegates to symfony/web-link
8.2 and later, and falls back to a backport of it otherwise.
RFC 9727 defines the "api-catalog" well-known URI and link relation, and
mandates the RFC 9264 link set format for the document it returns.

GET /.well-known/api-catalog now returns an "application/linkset+json" document,
carrying the profile of RFC 9727, that anchors on the API entrypoint and
advertises the OpenAPI description ("service-desc"), the human-readable
documentation ("service-doc"), the Hydra documentation ("service-meta") and the
exposed collections ("item"). A HEAD request answers with the "api-catalog" link
relation, as section 2 requires, and the entrypoint advertises the catalog with
the same relation so that clients still find it when the API is mounted under a
route prefix.

On Laravel the route sits outside the API prefix, since RFC 8615 roots
well-known URIs at the host.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant