mirror of
https://github.com/langgenius/dify.git
synced 2026-06-19 08:31:07 +08:00
fix(plugin): preserve multi-value HTTP response headers (#35726)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
11c52e90f6
commit
5cf741895f
@ -151,6 +151,12 @@ def deserialize_response(raw_data: bytes) -> Response:
|
||||
|
||||
response = Response(response=body, status=status_code)
|
||||
|
||||
# Replace Flask's default headers (e.g. Content-Type, Content-Length) with the
|
||||
# parsed ones so we faithfully reproduce the original response. Use Headers.add
|
||||
# rather than dict-style assignment so that repeated headers such as Set-Cookie
|
||||
# (and any other multi-valued header per RFC 9110) are preserved instead of
|
||||
# being overwritten.
|
||||
response.headers.clear()
|
||||
for line in lines[1:]:
|
||||
if not line:
|
||||
continue
|
||||
@ -158,6 +164,6 @@ def deserialize_response(raw_data: bytes) -> Response:
|
||||
if ":" not in line_str:
|
||||
continue
|
||||
name, value = line_str.split(":", 1)
|
||||
response.headers[name] = value.strip()
|
||||
response.headers.add(name, value.strip())
|
||||
|
||||
return response
|
||||
|
||||
@ -323,6 +323,50 @@ class TestDeserializeResponse:
|
||||
with pytest.raises(ValueError, match="Invalid status line"):
|
||||
deserialize_response(raw_data)
|
||||
|
||||
def test_deserialize_response_preserves_duplicate_set_cookie_headers(self):
|
||||
# Regression test for https://github.com/langgenius/dify/issues/35722
|
||||
# Multiple Set-Cookie headers must be preserved per RFC 9110, not collapsed
|
||||
# into a single value by dict-style assignment.
|
||||
raw_data = (
|
||||
b"HTTP/1.1 200 OK\r\n"
|
||||
b"Content-Type: text/plain\r\n"
|
||||
b"Set-Cookie: session=abc; Path=/; HttpOnly\r\n"
|
||||
b"Set-Cookie: tracking=xyz; Path=/; Secure\r\n"
|
||||
b"\r\n"
|
||||
b"ok"
|
||||
)
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
cookies = response.headers.getlist("Set-Cookie")
|
||||
assert cookies == [
|
||||
"session=abc; Path=/; HttpOnly",
|
||||
"tracking=xyz; Path=/; Secure",
|
||||
]
|
||||
# Single-valued headers should still be readable normally.
|
||||
assert response.headers.get("Content-Type") == "text/plain"
|
||||
|
||||
def test_deserialize_response_preserves_duplicate_generic_headers(self):
|
||||
# Any header name (not just Set-Cookie) may legitimately repeat; verify the
|
||||
# parser preserves all values rather than overwriting earlier ones.
|
||||
raw_data = b"HTTP/1.1 200 OK\r\nX-Custom: first\r\nX-Custom: second\r\n\r\n"
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
assert response.headers.getlist("X-Custom") == ["first", "second"]
|
||||
|
||||
def test_deserialize_response_does_not_inject_default_content_type(self):
|
||||
# Flask's Response constructor adds a default Content-Type header. When the
|
||||
# raw response has no Content-Type, the parsed response should not silently
|
||||
# gain one from the framework default.
|
||||
raw_data = b"HTTP/1.1 204 No Content\r\nX-Trace-Id: abc\r\n\r\n"
|
||||
|
||||
response = deserialize_response(raw_data)
|
||||
|
||||
header_names = [name for name, _ in response.headers.items()]
|
||||
assert "Content-Type" not in header_names
|
||||
assert response.headers.get("X-Trace-Id") == "abc"
|
||||
|
||||
def test_roundtrip_response(self):
|
||||
# Test that serialize -> deserialize produces equivalent response
|
||||
original_response = Response(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user