fix(dub): name the source language code that was rejected

Closes #1960.

The report was "400 Bad Request: Invalid source language code" and nothing
else. That cannot be acted on or triaged: it does not say which of the ninety
or so codes was wrong, so neither the user nor a maintainer reading the
auto-filed issue can tell whether the picker offered something the backend does
not accept, or a stale preference from an older build is still being sent.

I could not determine the cause from the report, which is exactly the problem.
Naming the code makes the next one answerable instead of guessing at this one.

The value is a language code chosen from a menu, not private data, and the
engine validator a few lines away already echoes its input the same way.

Also adds the check I actually wanted while investigating: a test that reads
the picker's own LANG_CODES and asserts the backend accepts every one of them,
so a code added to the menu cannot silently become a 400. It passes today —
the menu and the allowlist do agree — which is how I ruled that out as the
cause rather than assuming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
This commit is contained in:
Palash Debnath
2026-09-09 22:42:27 -07:00
co-authored by Claude Opus 5
parent f905230fd8
commit d1e4c847cb
3 changed files with 89 additions and 2 deletions
+1
View File
@@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains.
## [Unreleased]
**Highlights**
- A rejected dubbing source language now names the code it rejected (#1960)
- `bun run desktop` reclaims port 3900 from a backend the app itself left running, instead of refusing to start (#1974)
- A dictation shortcut another app already owns now says so, instead of silently doing nothing (#1858)
- Quitting on Windows is no longer reported as a crash on the next launch (#1898)
+20 -2
View File
@@ -540,12 +540,30 @@ _DUB_SOURCE_LANG_CODES = frozenset({
def _source_lang_override(value: str | None) -> str | None:
"""Normalize a user-selected source language; auto/und means detect."""
"""Normalize a user-selected source language; auto/und means detect.
A rejection NAMES the code it rejected. "Invalid source language code" on
its own cannot be acted on or reported usefully: it does not say which of
the ninety-odd codes was wrong, so neither the user nor a maintainer
reading the auto-filed issue can tell whether the picker offered something
the backend does not accept, or a stale preference from an older build is
still being sent (#1960).
The value is a language code the user chose from a menu — not private
data — and the neighbouring engine validator already echoes its input the
same way.
"""
code = (value or "").strip().lower()
if code in {"", "auto", "und"}:
return None
if code not in _DUB_SOURCE_LANG_CODES:
raise HTTPException(status_code=400, detail="Invalid source language code")
raise HTTPException(
status_code=400,
detail=(
f"Invalid source language code: {code!r}. Pick a language from "
"the Dubbing source-language menu, or leave it on auto-detect."
),
)
return code
+68
View File
@@ -0,0 +1,68 @@
"""#1960 — a rejected source language must say WHICH code it rejected.
The report was "400 Bad Request: Invalid source language code" and nothing
else. That cannot be acted on or triaged: it does not say which of the ninety
or so codes was wrong, so neither the user nor a maintainer reading the
auto-filed issue can tell whether the picker offered something the backend
does not accept, or a stale preference from an older build is still being
sent. I could not determine the cause from the report — which is the point.
The value is a language code chosen from a menu, not private data, and the
neighbouring engine validator already echoes its input the same way.
The last test is the durable one: it reads the picker's own list and asserts
the backend accepts all of it, so a code added to the menu cannot silently
become a 400.
"""
import pathlib
import re
import pytest
from fastapi import HTTPException
from api.routers.dub_core import _DUB_SOURCE_LANG_CODES, _source_lang_override
_REPO = pathlib.Path(__file__).resolve().parents[1]
_LANGUAGES_JS = _REPO / "frontend" / "src" / "utils" / "languages.js"
def test_the_rejection_names_the_code():
with pytest.raises(HTTPException) as caught:
_source_lang_override("zz-XX")
detail = caught.value.detail
assert "zz-xx" in detail
assert caught.value.status_code == 400
def test_the_rejection_says_what_to_do():
with pytest.raises(HTTPException) as caught:
_source_lang_override("nope")
assert "auto-detect" in caught.value.detail
@pytest.mark.parametrize("code", ["", " ", "auto", "und", None])
def test_detect_is_still_detect(code):
assert _source_lang_override(code) is None
def test_accepted_codes_are_unchanged():
# This only improved a message; it must not start accepting or rejecting
# anything different.
for code in sorted(_DUB_SOURCE_LANG_CODES):
assert _source_lang_override(code) == code
def test_every_code_the_picker_offers_is_accepted():
src = _LANGUAGES_JS.read_text(encoding="utf-8")
codes = re.findall(r"code:\s*['\"]([A-Za-z-]+)['\"]", src)
assert len(codes) > 50, "LANG_CODES did not parse; update this extraction"
rejected = []
for code in codes:
try:
_source_lang_override(code)
except HTTPException:
rejected.append(code)
assert rejected == [], (
"the Dubbing source-language menu offers codes the backend rejects, so "
f"picking them returns 400: {rejected}"
)