Link headers
editA Link header (RFC 8288) lists links related to a response: the next page of a collection, a resource to preload, the canonical URL of a document. LinkHeaderValue, in the Meziantou.Framework.Http package, parses it.
dotnet package add Meziantou.Framework.HttpRead the links of a response
EnumerateLinkHeaders parses every Link header of a response, or of any HttpHeaders, and returns one LinkHeaderValue per link:
using var response = new HttpResponseMessage();
response.Headers.Add("Link", "</items?page=2>; rel=next, </items?page=5>; rel=last");
var links = new List<string>();
foreach (var item in response.Headers.EnumerateLinkHeaders())
{
links.Add($"{item.Rel}: {item.Url}");
}
Assert.Equal(["next: /items?page=2", "last: /items?page=5"], links);LinkHeaderValue.Parse does the same from an HttpResponseMessage, an HttpHeaders, a string or a ReadOnlySpan<char>. A header can hold several links separated by commas, and a response can have several Link headers: the links of all of them are returned, in order.
Find a link by relation type
GetLink returns the first link of a relation type, and GetLinkUrl its URL, or null when there is none:
// Follow pagination links
var nextUrl = response.Headers.EnumerateLinkHeaders().GetLinkUrl("next");
Assert.Equal("/items?page=2", nextUrl);The relation type is compared case-insensitively. A link can declare several relation types separated by spaces (rel="next last"): it matches each of them, but not a part of one.
Resolve the URL of a link
Url is the target as written between < and >. It is often relative: resolve it against the URI of the request before following it:
using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/items?page=1");
using var response = new HttpResponseMessage { RequestMessage = request };
response.Headers.Add("Link", "</items?page=2>; rel=next");
// Url is the target as written in the header. Resolve a relative target against the request URI.
var next = response.Headers.EnumerateLinkHeaders().GetLinkUrl("next");
Assert.NotNull(next);
var nextUri = new Uri(response.RequestMessage.RequestUri!, next);
Assert.Equal(new Uri("https://api.example.com/items?page=2"), nextUri);Read the parameters of a link
Parameters lists the parameters of a link in order, and GetParameterValue returns the value of one, or null when the link does not have it:
var links = LinkHeaderValue.Parse("</style.css>; rel=preload; as=style; fetchpriority=\"high\"");
var link = links.GetLink("preload");
Assert.Equal("style", link?.GetParameterValue("as"));- Parameter names are lowercased, and GetParameterValue compares them case-insensitively.
- A quoted value is unquoted and unescaped (
\"becomes"). An unquoted value is trimmed. - A parameter without a value (
; crossorigin) has an empty value. - Rel is the value of the
relparameter, or an empty string when there is none.
var links = LinkHeaderValue.Parse("<https://example.com/chapter2>; REL=\"next last\"; title=\"Chapter \\\"2\\\"\"; hreflang=en");
var link = Assert.Single(links);
// The names of the parameters are lowercased, and quoted values are unescaped
Assert.Equal(["rel", "title", "hreflang"], link.Parameters.Select(parameter => parameter.Key));
Assert.Equal("Chapter \"2\"", link.GetParameterValue("Title"));
// Rel holds the whole value; GetLink matches each of its space-separated relation types
Assert.Equal("next last", link.Rel);
Assert.Same(link, links.GetLink("last"));Malformed headers
Parsing never throws on a malformed value. An element that does not start with a <target> is skipped, and the links that follow it are still returned:
// An element that is not a link-value is skipped, and the following ones are still read
var links = LinkHeaderValue.Parse("not-a-link, </a>; rel=first, , </b>; rel=second");
Assert.Equal(["/a", "/b"], links.Select(link => link.Url));
// A header without a link-value yields no link
Assert.Empty(LinkHeaderValue.Parse("rel=next"));A comma inside the target (<https://example.com/a,b>) or inside a quoted value does not split the link.