Skip to content

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

  1. Install agent-framework-devui 1.0.0b261022 (with agent-framework-core 1.23.0).
  2. 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.
  3. 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.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 26, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 26, 2026
  3. github-actions commented on Oct 26, 2026

    @github-actions
    Contributor

    🤖 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_middleware at lines 473-476 when a loopback-bound server receives Host: [::1] or Host: [::1]:8080; the parser reduces either value to [, causing host_not_allowed. Minimal repro: create DevServer(host="::1", auth_token="s3cret"), request /health through TestClient with either bracketed IPv6 Host header, and observe HTTP 400 instead of 200.

    • Failing test: python/packages/devui/tests/devui/test_server.py::test_loopback_bind_accepts_ipv6_loopback_host_header
    • Files examined: python/packages/devui/agent_framework_devui/_server.py, python/packages/devui/tests/devui/test_server.py, python/packages/devui/pyproject.toml
    • Tests run: test_loopback_bind_rejects_non_allowlisted_host_header, test_loopback_bind_accepts_ipv6_loopback_host_header
    • Reported version: 1.0.0b261022
    • Current version: 1.0.0b261022
  4. added
    devuiUsage: [Issues, PRs], Target: DevUI
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 26, 2026
  5. lisry-1 commented on Oct 26, 2026

    @lisry-1

    I reproduced this from the script in the report on main and opened #9712 with a fix.

    The cause is the host reduction in host_header_middleware:

    hostname = host_header.split(":", 1)[0].lower()

    An IPv6 literal carries colons of its own, so both [::1] and [::1]:8080 become [, which no member of _LOOPBACK_HOSTS matches. 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 ::1 holds two colons, so it is left intact and still matches). The allowlist is untouched, so evil.example is 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 /health with [::1] / [::1]:8080 on binds 127.0.0.1, ::1 and [::1]. I confirmed they catch the bug by restoring the old split line, which fails all three binds with assert 400 == 200.

  6. alphasafal commented on Oct 27, 2026

    @alphasafal
    Author

    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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

devuiUsage: [Issues, PRs], Target: DevUIpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions