Repository: falconry/falcon

Issue:
Add support for link-extension to Response.append_link
See also: https://tools.ietf.org/html/rfc5988



Discussion:
Note that extension relation types are REQUIRED to be absolute URIs.

Hi, I am interested to work on this issue. Could you give me some pointers?
Am I correct in understanding that I need to provide an extra argument to the function append_link() to support link extension, and update the corresponding link header using the spec from rfc5988?
Hi @kaichan1201!
And sorry for the delay in getting back to you.

It seems that we need to revise this in the light of [RFC 8288](https://datatracker.ietf.org/doc/html/rfc8288), which obsoletes the referenced [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988). Sorry this issue got a bit more complex than it initially looked like... could you check how `link-extension` is defined in 8288?
Sure thing!
From what I read, the definition of link-extension did not change much from RFC 5988 to RFC 8288. As noted in [Section 3.4.2 of RFC 8288](https://datatracker.ietf.org/doc/html/rfc8288#section-3.4.2):
> Other link-params are link-extensions and are to be considered as
   target attributes.

So it seems we would need to provide support for any (key, value) pair of attributes. I'm not sure if there is any limitation for the extension attributes, RFC 8288 did not seem to set any restrictions. So should I just add a parameter that allows user to add extra (key, value) pairs?

Resolving patch:
diff --git a/falcon/response.py b/falcon/response.py
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -787,6 +787,7 @@ def append_link(
         hreflang=None,
         type_hint=None,
         crossorigin=None,
+        link_extension=None,
     ):
         """Append a link header to the response.
 
@@ -796,11 +797,6 @@ def append_link(
             Calling this method repeatedly will cause each link to be
             appended to the Link header value, separated by commas.
 
-        Note:
-            So-called "link-extension" elements, as defined by RFC 5988,
-            are not yet supported. See also
-            `Issue #288 <https://github.com/falconry/falcon/issues/288>`__.
-
         Args:
             target (str): Target IRI for the resource identified by the
                 link. Will be converted to a URI, if necessary, per
@@ -850,6 +846,10 @@ def append_link(
                 Can take values 'anonymous' or 'use-credentials' or None.
                 (See:
                 https://www.w3.org/TR/html50/infrastructure.html#cors-settings-attribute)
+            link_extension(iterable): Provides additional custom attributes, as
+                described in RFC 8288, Section 3.4.2. Each member of the iterable
+                must be a two-tuple in the form of (*param*, *value*).
+                (See: https://datatracker.ietf.org/doc/html/rfc8288#section-3.4.2)
 
         """
 
@@ -912,6 +912,10 @@ def append_link(
                 # Un-inline in case more values are supported in the future.
                 value += '; crossorigin="use-credentials"'
 
+        if link_extension is not None:
+            value += '; '
+            value += '; '.join([p + '=' + v for p, v in link_extension])
+
         _headers = self._headers
         if 'link' in _headers:
             _headers['link'] += ', ' + value


Test patch:
diff --git a/tests/test_headers.py b/tests/test_headers.py
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -973,6 +973,16 @@ def test_append_link_invalid_crossorigin_value(self, crossorigin):
         with pytest.raises(ValueError):
             resp.append_link('/related/resource', 'next', crossorigin=crossorigin)
 
+    def test_append_link_with_link_extension(self, client):
+        expected_value = '</related/thing>; rel=item; sizes=72x72'
+
+        resource = LinkHeaderResource()
+        resource.append_link(
+            '/related/thing', 'item', link_extension=[('sizes', '72x72')]
+        )
+
+        self._check_link_header(client, resource, expected_value)
+
     def test_content_length_options(self, client):
         result = client.simulate_options()