Skip to content

Commit e313525

Browse files
committed
fix(xui): a client with no token yet sent a bare Bearer and failed early
Caught the moment the import ran against a real box. The client sets `Authorization: Bearer <token>` unconditionally, so the bootstrap client — the one that logs in precisely BECAUSE it has no token — sent `Bearer ` with nothing after it. httpx refuses to encode that, so the request died as an illegal header before reaching the panel, and the error mentioned a header rather than the missing token. The header is now set only when there is a token to put in it. The failure underneath was also unreadable: httpx leaves `str(exc)` empty on a plain connect failure, so the message read "transport error: " and stopped. That is the error an operator gets when the port or the base path is wrong, which is most of the time — it now names the exception and the URL it could not reach.
1 parent 208bd75 commit e313525

2 files changed

Lines changed: 41 additions & 8 deletions

File tree

backend/app/core/xui_api.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,22 @@ def _ensure_client(self) -> httpx.AsyncClient:
182182
# api/inbounds/list` collapses to `https://h:port/panel/...`,
183183
# missing the basepath and getting a 307 redirect back from
184184
# the panel. Building full URLs in `_request` instead.
185+
headers: Dict[str, str] = {
186+
# The panel responds JSON either way, but being explicit
187+
# avoids any future content-negotiation surprises if
188+
# upstream adds an HTML fallback.
189+
"Accept": "application/json",
190+
}
191+
# Only when there is one. A client that has yet to obtain a token
192+
# — the bootstrap that logs in to fetch it — would otherwise send
193+
# a bare `Bearer `, which httpx refuses to encode at all: the
194+
# request fails as an illegal header before it reaches the panel,
195+
# and the error says nothing about the missing token.
196+
if self.api_token:
197+
headers["Authorization"] = f"Bearer {self.api_token}"
198+
185199
client_kwargs: Dict[str, Any] = dict(
186-
headers={
187-
"Authorization": f"Bearer {self.api_token}",
188-
# The panel responds JSON either way, but being
189-
# explicit avoids any future content-negotiation
190-
# surprises if upstream adds an HTML fallback.
191-
"Accept": "application/json",
192-
},
200+
headers=headers,
193201
verify=self.verify_tls,
194202
timeout=self.timeout,
195203
# Follow 307/308 redirects belt-and-suspenders — if a
@@ -318,8 +326,14 @@ async def _ensure_cookie_session(self) -> str:
318326
try:
319327
r = await client.get(url, headers={"X-Requested-With": "XMLHttpRequest"})
320328
except httpx.HTTPError as exc:
329+
# httpx leaves `str(exc)` empty on a plain connect failure, and
330+
# "transport error: " with nothing after it tells the operator
331+
# nothing — this is the error they hit when the port or the base
332+
# path is wrong, which is most of the time.
321333
raise XuiAPIError(
322-
f"csrf-token transport error: {exc}", kind="transport",
334+
f"csrf-token transport error: "
335+
f"{str(exc) or type(exc).__name__} ({url})",
336+
kind="transport",
323337
) from exc
324338
if not r.is_success:
325339
raise XuiAPIError(

backend/tests/test_xui_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,3 +812,22 @@ def test_neither_mount_answering_is_an_error(self, monkeypatch):
812812
client, _ = self._client(monkeypatch, {})
813813
with pytest.raises(XuiAPIError, match="Could not obtain an API token"):
814814
asyncio.run(client.ensure_api_token())
815+
816+
817+
class TestClientWithoutAToken:
818+
"""The bootstrap client that logs in to FETCH the token has none yet."""
819+
820+
def test_no_authorization_header_when_there_is_no_token(self):
821+
from app.core.xui_api import XuiClient
822+
c = XuiClient(base_url="http://198.51.100.7:2053/abc", api_token="",
823+
panel_user="admin", panel_pass="pw")
824+
http = c._ensure_client()
825+
# httpx refuses to encode a bare `Bearer `: the request fails as an
826+
# illegal header before it leaves the process, and the error says
827+
# nothing about a missing token.
828+
assert "Authorization" not in http.headers
829+
830+
def test_the_header_is_there_once_a_token_is(self):
831+
from app.core.xui_api import XuiClient
832+
c = XuiClient(base_url="http://198.51.100.7:2053/abc", api_token="tok")
833+
assert c._ensure_client().headers["Authorization"] == "Bearer tok"

0 commit comments

Comments
 (0)