The Link response field describes relationships between the current resource and other resources. An API returning JSON can advertise its next page without making clients scrape HTML or invent a query parameter. The field supplies a target and a relation; whether to follow it remains a client decision. It is useful metadata, not a command that every browser or crawler must execute.

A value such as </items?cursor=b>; rel="next" links the response’s context resource to a target URI reference. With no explicit anchor parameter, the context is normally the requested resource. Resolve relative targets against the effective request URI after redirects, not against the client’s current working directory or an assumed API root. A query-only target changes the query while preserving the path.

The rel parameter names the relationship. Registered relations include next, prev, alternate, and canonical; extension relations can use URI identifiers. Additional parameters describe the target, for example type="application/json". They do not change the actual response’s Content-Type, grant access, or prove that the target is trustworthy. An anchor can change the context, so clients that cannot honor that context must not silently attach the link to the original resource.

Relations also have different consumers. A crawler may interpret canonical when selecting a preferred indexing URL, while a browser can use supported preload hints to begin fetching assets. Neither relation redirects the current request. Test the behavior of the intended consumer separately from checking field syntax; an ordinary HTTP library may correctly expose the header and take no further action.

Follow pagination as a server contract

For pagination, the server decides whether a continuation exists and supplies its exact target. A cursor may encode ordering, filters, and a position in a snapshot. Treat it as opaque: guessing the next page number or changing cursor bytes can skip results, repeat rows, or invalidate the request. Preserve the returned query rather than rebuilding it from assumptions about today’s API implementation.

A missing next relation normally terminates that API’s traversal, subject to its documented pagination contract. It does not prove the collection is immutable. Concurrent insertions and deletions can still change membership between requests unless the API provides snapshot semantics. Bound requests and detect repeated continuation targets so a server bug cannot create an endless crawl.

When it breaks

Multiple links can appear in separate Link field lines or a comma-separated field value. Naively splitting on every comma is incorrect: quoted parameter values and URI references can contain commas. Use a parser implementing Web Linking, retain multiple values, and interpret relation tokens correctly. A proxy that overwrites repeated fields can silently remove the pagination link even while the response body remains valid.

Following an absolute target blindly is another failure. An authenticated API client must apply its destination and credential-forwarding policy before requesting a different origin; a link is not authorization to send tokens elsewhere. For browser code, successful CORS access to the body does not automatically expose Link. The server must include it in Access-Control-Expose-Headers when cross-origin JavaScript needs to read it.

Check it quickly

This Python 3 experiment creates a two-page loopback API. The first response has separate next and alternate fields; the terminal response has no links. It examines the wire values and resolves the known fixture target with the standard URL library. It deliberately does not implement a general Link parser.

from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from urllib.parse import urljoin, urlsplit

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Length", "2")
        if self.path == "/items?cursor=a":
            self.send_header("Link", '<?cursor=b>; rel="next"')
            self.send_header("Link", '</items.csv>; rel="alternate"')
        self.end_headers()
        self.wfile.write(b"[]")

    def log_message(self, *args):
        pass

with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server:
    worker = Thread(target=server.serve_forever, daemon=True)
    worker.start()
    client = HTTPConnection(*server.server_address, timeout=3)
    try:
        client.request("GET", "/items?cursor=a")
        response = client.getresponse()
        links = response.headers.get_all("Link")
        assert response.read() == b"[]"
        assert links == ['<?cursor=b>; rel="next"',
                         '</items.csv>; rel="alternate"']
        base = f"http://127.0.0.1:{server.server_port}/items?cursor=a"
        target = urlsplit(urljoin(base, "?cursor=b"))
        client.request("GET", target.path + "?" + target.query)
        response = client.getresponse()
        assert response.read() == b"[]"
        assert response.headers.get_all("Link") is None
        print("two fields preserved; terminal page has no next link")
    finally:
        client.close()
        server.shutdown()
        worker.join()

Expect the final confirmation line without an assertion failure. The query-only target should resolve to /items?cursor=b, and both first-page fields should survive. This verifies response construction, repeated-field retrieval, and relative resolution. A production client additionally needs parser tests for quoted commas, redirects, multiple relation tokens, cross-origin targets, and pagination loops. Browser header exposure must be checked from the actual calling origin; this standard-library client does not enforce CORS.

Sources