From d16bfd169c88999bf85a946c6ef274723eda00d5 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 3 Feb 2026 12:29:40 +0100 Subject: [PATCH 1/4] add test for multivalue headers regression --- tests/gateway/test_headers.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/gateway/test_headers.py b/tests/gateway/test_headers.py index 8e62d69..235e3fb 100644 --- a/tests/gateway/test_headers.py +++ b/tests/gateway/test_headers.py @@ -1,3 +1,4 @@ +import http.client import json import pytest @@ -5,6 +6,7 @@ from rolo import Response from rolo.gateway import Gateway, HandlerChain, RequestContext +from collections import defaultdict @pytest.mark.parametrize("serve_gateway", ["asgi", "twisted"], indirect=True) @@ -14,6 +16,8 @@ def handler(chain: HandlerChain, context: RequestContext, response: Response): response.mimetype = "application/json" response.headers["X-fOO_bar"] = "FooBar" response.headers["content-md5"] = "af5e58f9a7c4682e1b410f2e9392a539" + response.headers.add("multi-value", "value1") + response.headers.add("multi-value", "value2") return response gateway = Gateway(request_handlers=[handler]) @@ -39,3 +43,31 @@ def handler(chain: HandlerChain, context: RequestContext, response: Response): assert "X-fOO_bar" in response_headers # even though it's a standard header, it should be in the original case assert "content-md5" in response_headers + assert response_headers["multi-value"] == "value1, value2" + + +@pytest.mark.parametrize("serve_gateway", ["asgi", "twisted"], indirect=True) +def test_multivalue_header_handling(serve_gateway): + def handler(chain: HandlerChain, context: RequestContext, response: Response): + response.data = json.dumps({"headers": dict(context.request.headers)}) + response.mimetype = "application/json" + response.headers.add("multi-value", "value1") + response.headers.add("multi-value", "value2") + return response + + gateway = Gateway(request_handlers=[handler]) + + srv = serve_gateway(gateway) + + # we need to use a low level HTTP client because `requests` does some header manipulation and concatenation which + # obscures the behavior + conn = http.client.HTTPConnection(host="127.0.0.1", port=srv.port) + + conn.request("GET", url=f"/hello") + response = conn.getresponse() + response_headers = defaultdict(list) + + for k, v in response.headers.items(): + response_headers[k].append(v) + + assert response_headers["multi-value"] == ["value1", "value2"] From b4e792e27728164916e13df1c07d062307173762 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 3 Feb 2026 12:31:42 +0100 Subject: [PATCH 2/4] fix multivalue header handling --- pyproject.toml | 2 +- rolo/serving/twisted.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cccaf3a..c4f9b25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "rolo" authors = [ { name = "LocalStack Contributors", email = "info@localstack.cloud" } ] -version = "0.8.0" +version = "0.8.1" description = "A Python framework for building HTTP-based server applications" dependencies = [ "requests>=2.20", diff --git a/rolo/serving/twisted.py b/rolo/serving/twisted.py index 2ae1f25..5209664 100644 --- a/rolo/serving/twisted.py +++ b/rolo/serving/twisted.py @@ -221,8 +221,9 @@ def writeHeaders( else: # newer twisted versions instead pass the headers object for name, values in headers.getAllRawHeaders(): - line = name + b": " + b",".join(values) + b"\r\n" - headerSequence.append(line) + for value in values: + line = name + b": " + value + b"\r\n" + headerSequence.append(line) headerSequence.append(b"\r\n") self.transport.writeSequence(headerSequence) From ba4edc3653b10b2d6fdeab31337e484249c3fffa Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 3 Feb 2026 12:32:03 +0100 Subject: [PATCH 3/4] bump patch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4f9b25..cccaf3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "rolo" authors = [ { name = "LocalStack Contributors", email = "info@localstack.cloud" } ] -version = "0.8.1" +version = "0.8.0" description = "A Python framework for building HTTP-based server applications" dependencies = [ "requests>=2.20", From 14ee67cfa9b95c281ef216e85f144355798f678a Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 3 Feb 2026 12:48:05 +0100 Subject: [PATCH 4/4] fix lint --- tests/gateway/test_headers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/gateway/test_headers.py b/tests/gateway/test_headers.py index 235e3fb..deaa3cf 100644 --- a/tests/gateway/test_headers.py +++ b/tests/gateway/test_headers.py @@ -1,12 +1,12 @@ import http.client import json +from collections import defaultdict import pytest import requests from rolo import Response from rolo.gateway import Gateway, HandlerChain, RequestContext -from collections import defaultdict @pytest.mark.parametrize("serve_gateway", ["asgi", "twisted"], indirect=True) @@ -63,7 +63,7 @@ def handler(chain: HandlerChain, context: RequestContext, response: Response): # obscures the behavior conn = http.client.HTTPConnection(host="127.0.0.1", port=srv.port) - conn.request("GET", url=f"/hello") + conn.request("GET", url="/hello") response = conn.getresponse() response_headers = defaultdict(list)