Repository: stac-utils/stac-fastapi
Pull request #788 (2025-01-23 15:46:45)

Support forwarded headers that do not contain a host key
**Related Issue(s):**

- follow up from #782

**Description:**

The proxy header middleware doesn't parse the value of the `proto` key in case there is no `host` key as well. Which is the case if `stac-fastapi` is running on GCP as cloud run service behind their load balancer. Additionally it even failed with `UnboundLocalError` since `port_str` was never defined in that execution path but still used later on.

That's a small oversight from the last PR (#782) - I noticed it now after upgrading to v4.0.
I also added a testcase for an exact `forwarded` header that I receive on gcp - so that hopefully this finally fixes the issue.

**PR Checklist:**

- [X] `pre-commit` hooks pass locally
- [X] Tests pass (run `make test`)
- [X] Documentation has been updated to reflect changes, if applicable, and docs build successfully (run `make docs`)
- [X] Changes are added to the [CHANGELOG](https://github.com/stac-utils/stac-fastapi/blob/main/CHANGES.md).



Patch:
diff --git a/CHANGES.md b/CHANGES.md
index c78b9e5..6e26307 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## Unreleased
+
+### Fixed
+
+* support `forwarded` headers in `ProxyHeaderMiddleware` that do not contain a host key ([#788](https://github.com/stac-utils/stac-fastapi/pull/788))
+
 ## [4.0.0] - 2025-01-17
 
 ### Changed
diff --git a/stac_fastapi/api/stac_fastapi/api/middleware.py b/stac_fastapi/api/stac_fastapi/api/middleware.py
index 0b11923..b0965bd 100644
--- a/stac_fastapi/api/stac_fastapi/api/middleware.py
+++ b/stac_fastapi/api/stac_fastapi/api/middleware.py
@@ -4,7 +4,7 @@ import contextlib
 import re
 import typing
 from http.client import HTTP_PORT, HTTPS_PORT
-from typing import List, Tuple
+from typing import List, Optional, Tuple
 
 from starlette.middleware.cors import CORSMiddleware as _CORSMiddleware
 from starlette.types import ASGIApp, Receive, Scope, Send
@@ -95,12 +95,13 @@ class ProxyHeaderMiddleware:
                 domain = header_host_parts[0]
                 port = None
 
+        port_str = None  # make sure it is defined in all paths since we access it later
+
         if forwarded := self._get_header_value_by_name(scope, "forwarded"):
             for proxy in forwarded.split(","):
-                if (proto_expr := _PROTO_HEADER_REGEX.search(proxy)) and (
-                    host_expr := _HOST_HEADER_REGEX.search(proxy)
-                ):
+                if proto_expr := _PROTO_HEADER_REGEX.search(proxy):
                     proto = proto_expr.group("proto")
+                if host_expr := _HOST_HEADER_REGEX.search(proxy):
                     domain = host_expr.group("host")
                     port_str = host_expr.group("port")  # None if not present in the match
 
@@ -115,8 +116,8 @@ class ProxyHeaderMiddleware:
         return (proto, domain, port)
 
     def _get_header_value_by_name(
-        self, scope: Scope, header_name: str, default_value: str = None
-    ) -> str:
+        self, scope: Scope, header_name: str, default_value: Optional[str] = None
+    ) -> Optional[str]:
         headers = scope["headers"]
         candidates = [
             value.decode() for key, value in headers if key.decode() == header_name


Test patch:
diff --git a/stac_fastapi/api/tests/test_middleware.py b/stac_fastapi/api/tests/test_middleware.py
index 45f6551..4ce97ff 100644
--- a/stac_fastapi/api/tests/test_middleware.py
+++ b/stac_fastapi/api/tests/test_middleware.py
@@ -183,6 +183,20 @@ def test_replace_header_value_by_name(
             },
             ("https", "test", 1234),
         ),
+        (
+            {
+                "scheme": "http",
+                "server": ["testserver", 80],
+                "headers": [
+                    (
+                        b"forwarded",
+                        # proto is set, but no host
+                        b'for="85.193.181.55";proto=https,for="85.193.181.55";proto=https',
+                    )
+                ],
+            },
+            ("https", "testserver", 80),
+        ),
     ],
 )
 def test_get_forwarded_url_parts(