Repository navigation
Python: [Bug]: DevUI rejects every request addressed to the IPv6 loopback ([::1] Host header) #9709
Description
Observed Behavior
A DevUI server bound to a loopback interface returns HTTP 400 host_not_allowed for every request whose Host header is the IPv6 loopback, [::1]:8080 or [::1]. Browsers send exactly that header when you open http://[::1]:8080, so DevUI can't be used over the IPv6 loopback, including when it is bound to ::1 itself.
Expected Behavior
Requests with Host [::1] or [::1]: are accepted on a loopback bind, the same as 127.0.0.1 and localhost. Non-loopback hosts are still rejected.
Steps to Reproduce
- Install agent-framework-devui 1.0.0b261022 (with agent-framework-core 1.23.0).
- Save the script below as repro.py and run python repro.py. It creates the DevUI app for each loopback bind and sends /health requests with different Host headers through FastAPI's TestClient. No server process, model or network is needed.
- Every [::1] Host header gets 400, on every bind.
Minimal Reproduction
"""Repro: DevUI bound to IPv6 loopback rejects its own (valid) Host header."""
from fastapi.testclient import TestClient
from agent_framework_devui._server import DevServer
TOKEN = "t" * 40
rejected_ipv6 = []
for bind in ["127.0.0.1", "::1", "[::1]"]:
server = DevServer(host=bind, port=8080, auth_token=TOKEN)
app = server.create_app()
client = TestClient(app)
for host_header in ["127.0.0.1:8080", "[::1]:8080", "[::1]", "evil.example:8080", ""]:
r = client.get("/health", headers={"host": host_header})
print(f"bind={bind!r:12} Host={host_header!r:22} -> {r.status_code}")
if host_header.startswith("[::1]") and r.status_code == 400:
rejected_ipv6.append((bind, host_header))
print()
print("VERDICT: BUG REPRODUCED (IPv6 loopback Host rejected)" if rejected_ipv6 else "VERDICT: not reproduced")Error Messages and Stack Traces
bind='127.0.0.1' Host='127.0.0.1:8080' -> 200
bind='127.0.0.1' Host='[::1]:8080' -> 400
bind='127.0.0.1' Host='[::1]' -> 400
bind='127.0.0.1' Host='evil.example:8080' -> 400
bind='127.0.0.1' Host='' -> 200
bind='::1' Host='127.0.0.1:8080' -> 200
bind='::1' Host='[::1]:8080' -> 400
bind='::1' Host='[::1]' -> 400
bind='::1' Host='evil.example:8080' -> 400
bind='::1' Host='' -> 200
bind='[::1]' Host='127.0.0.1:8080' -> 200
bind='[::1]' Host='[::1]:8080' -> 400
bind='[::1]' Host='[::1]' -> 400
bind='[::1]' Host='evil.example:8080' -> 400
bind='[::1]' Host='' -> 200
VERDICT: BUG REPRODUCED (IPv6 loopback Host rejected)
Response body for the rejected requests: error code host_not_allowed.
Package Versions
agent-framework-devui 1.0.0b261022, agent-framework-core 1.23.0
Python Version
Python 3.13.5
Operating System
macOS
Regression
Unknown
Additional Context
Cause. host_header_middleware in python/packages/devui/agent_framework_devui/_server.py strips the port with host_header.split(":", 1)[0]. For a bracketed IPv6 literal like [::1]:8080 that yields "[", which isn't in the loopback allowlist, so the request is rejected.
Proposed fix. For Host values that start with "[", take everything up to and including the closing "]" before the allowlist check; keep the current split for everything else. I have a small fix with a regression test covering both loopback binds (including that a non-loopback IPv6 Host such as [2001:db8::1]:8080 is still rejected); the existing DevUI tests pass. I'll wait for maintainer agreement before opening a PR.
Related observation, not part of this fix. Auth can't be disabled on a ::1 bind: the no-auth loopback list (127.0.0.1, localhost) leaves out ::1, while the Host-header loopback list includes it. That may be intentional, so I've left it alone and am just noting it.
AI assistance. The analysis, reproduction and prototype fix were prepared with AI assistance. I reviewed them and personally ran the reproduction before filing.
Acknowledgements
- I searched existing issues and did not find a duplicate.
- I personally verified this behavior and the reproduction details are authentic.
- I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Oct 26, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
The bug reproduces in
python/packages/devui/agent_framework_devui/_server.py::host_header_middlewareat lines 473-476 when a loopback-bound server receivesHost: [::1]orHost: [::1]:8080; the parser reduces either value to[, causinghost_not_allowed. Minimal repro: createDevServer(host="::1", auth_token="s3cret"), request/healththroughTestClientwith either bracketed IPv6 Host header, and observe HTTP 400 instead of 200.python/packages/devui/tests/devui/test_server.py::test_loopback_bind_accepts_ipv6_loopback_host_header1.0.0b2610221.0.0b261022lisry-1 commentedon Oct 26, 2026
I reproduced this from the script in the report on
mainand opened #9712 with a fix.The cause is the host reduction in
host_header_middleware:An IPv6 literal carries colons of its own, so both
[::1]and[::1]:8080become[, which no member of_LOOPBACK_HOSTSmatches. The allowlist itself already contains[::1]and::1; only the parsing in front of it is wrong.The fix adds
DevServer._hostname_from_host_header(), which keeps the brackets ([::1]:8080->[::1]) and strips a port only when the host is not an IPv6 literal (a bare::1holds two colons, so it is left intact and still matches). The allowlist is untouched, soevil.exampleis still rejected on all three loopback binds.With the fix the report's script prints
VERDICT: not reproduced; the six[::1]rows that returned 400 now return 200. New tests cover the parser directly and drive/healthwith[::1]/[::1]:8080on binds127.0.0.1,::1and[::1]. I confirmed they catch the bug by restoring the old split line, which fails all three binds withassert 400 == 200.alphasafal commentedon Oct 27, 2026
Thanks for the PR in #9712. I had a fix and regression test ready for this as well, so happy for whichever you prefer: Eduard van Valkenburg (@eavanvalkenburg), should I open mine or should #9712 go ahead?