diff --git a/examples/bottle-app.py b/examples/bottle-app.py index 61ea7ce..3f96df1 100644 --- a/examples/bottle-app.py +++ b/examples/bottle-app.py @@ -27,6 +27,7 @@ def info(): "vcpu_time_ms": vcpu_time, "request_method": request.environ.get("REQUEST_METHOD"), "path_info": request.environ.get("PATH_INFO"), + "request_headers": dict(request.headers), } diff --git a/examples/flask-app.py b/examples/flask-app.py index c84bab6..03dfebc 100644 --- a/examples/flask-app.py +++ b/examples/flask-app.py @@ -28,6 +28,7 @@ def info(): "request_method": request.environ.get("REQUEST_METHOD"), "path_info": request.environ.get("PATH_INFO"), "python_version": sys.version, + "request_headers": dict(request.headers), } diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index 37e3934..56b7629 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -86,6 +86,49 @@ def start_response( "HTTP_HOST": url.netloc or "localhost", } + # Add incoming HTTP headers to environ (WSGI spec requires HTTP_ prefix) + # Use cursor-based iteration to read all header names + cursor = 0 + while True: + header_names_str, next_cursor = req.get_header_names( + max_len=8192, cursor=cursor + ) + if not header_names_str: + break + + # Header names are NUL-separated + header_names_split = ( + header_names_str.rstrip("\0").split("\0") if header_names_str else [] + ) + + for header_name in header_names_split: + if not header_name: + continue + + # Get the header value + header_value_bytes = req.get_header_value(header_name, max_len=8192) + if header_value_bytes is None: + continue + + # See https://peps.python.org/pep-3333/ - ISO-8859-1 encoding + # should be used for bytes values across this boundary. + header_value_str = header_value_bytes.decode("iso-8859-1") + + # Special handling for Content-Type and Content-Length (no HTTP_ prefix) + if header_name.lower() == "content-type": + environ["CONTENT_TYPE"] = header_value_str + elif header_name.lower() == "content-length": + environ["CONTENT_LENGTH"] = header_value_str + else: + # Convert to WSGI format: HTTP_ prefix, uppercase, hyphens to underscores + wsgi_key = "HTTP_" + header_name.upper().replace("-", "_") + environ[wsgi_key] = header_value_str + + # If there are more headers, continue with next cursor + if next_cursor is None: + break + cursor = next_cursor + try: # Call the WSGI app and collect response body chunks for body_chunk in app(environ, start_response): diff --git a/tests/test_bottle_example.py b/tests/test_bottle_example.py index ae48203..eff6277 100644 --- a/tests/test_bottle_example.py +++ b/tests/test_bottle_example.py @@ -57,6 +57,8 @@ def test_custom_headers(self): headers = {"X-Custom-Header": "test-value"} response = self.get("/info", headers=headers) assert response.status_code == 200 + data = response.json() + assert data["request_headers"]["X-Custom-Header"] == "test-value" def test_error_endpoint_handling(self): """Test that the error endpoint returns 500 and triggers viceroy output display.""" diff --git a/tests/test_flask_example.py b/tests/test_flask_example.py index aaa2411..a2eebf6 100644 --- a/tests/test_flask_example.py +++ b/tests/test_flask_example.py @@ -17,7 +17,7 @@ def test_hello_endpoint(self): def test_info_endpoint(self): """Test the info endpoint returns expected JSON with WIT data.""" - response = self.get("/info") + response = self.get("/info", headers={"Test-Header": "test-value"}) assert response.status_code == 200 assert response.headers.get("content-type", "").startswith("application/json") @@ -32,6 +32,7 @@ def test_info_endpoint(self): # Check WIT API data assert "vcpu_time_ms" in data assert isinstance(data["vcpu_time_ms"], int) + assert data["request_headers"]["Test-Header"] == "test-value" def test_error_endpoint_handling(self): """Test that the error endpoint returns 500."""