Windows users behind a corporate/antivirus TLS-inspecting proxy got a raw `[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE]` on every model install — the TCP connection reaches the server fine, but the handshake fails because the OS trusts the proxy's re-signed root CA and Python's bundled certifi CA list doesn't. A genuinely different failure mode from #984 (that was TCP-level unreachability to a blocked host, before any TLS negotiation). - backend/core/failure.py: new SSL_HANDSHAKE_FAILURE classification (handshake/cert-verify-failed/sslv3_alert/sslcertverificationerror substring markers) with an actionable hint, added to _CONTEXT_FREE_HINT_CLASSES so append_hint() (already called by setup/download.py's install worker) surfaces it without further wiring. - backend/main.py: truststore.inject_into_ssl() at module level, before any huggingface_hub/requests/httpx network I/O — patches ssl.SSLContext to verify against the OS trust store instead of only certifi's bundled CA list. Not platform-gated (correctness improvement everywhere); wrapped in try/except so it never blocks startup. - pyproject.toml/uv.lock: truststore>=0.9 — pure Python, MIT, PyPA- maintained, zero transitive deps, same class of fix as socksio. Verified: uv lock --check + uv sync --frozen clean (lockfile diff is just the one new package); main.py imports cleanly; full backend suite passes; no hiddenimports entry needed (main.py is PyInstaller's direct entry script per backend.spec, so a top-level import traces normally — unlike socksio's case, which was httpx's internal lazy import). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Fable 5
parent
324d417d27
commit
dfe2bd1bc3
@@ -42,6 +42,7 @@ _HINTS: dict[str, str] = {
|
||||
"TRANSFORMERS_IMPORT": "Your transformers install is incomplete. Reinstall it (`uv pip install --reinstall transformers`) or switch ASR to faster-whisper (Settings → Models).",
|
||||
"OS_INVALID_ARGUMENT": "The OS rejected a file operation (Errno 22 / invalid argument) — in the transcribe path this is the temporary WAV write before ASR. It's almost always the temp directory: missing, read-only, on a full or removed drive, or blocked by antivirus. Check that your system TEMP/TMP folder exists and is writable and the drive has free space (add an OmniVoice antivirus exclusion if you use one), then retry.",
|
||||
"SOCKS_PROXY_SUPPORT_MISSING": "A SOCKS proxy is configured in your environment (ALL_PROXY/HTTPS_PROXY=socks5://…) and the backend's HTTP client is missing SOCKS support. Newer OmniVoice builds ship SOCKS support (the socksio package) — update the app. If you still see this, unset ALL_PROXY/HTTPS_PROXY for OmniVoice, or run `uv pip install 'httpx[socks]'` in the backend venv, then restart.",
|
||||
"SSL_HANDSHAKE_FAILURE": "A corporate or antivirus proxy is intercepting HTTPS traffic and re-signing certificates with its own CA — your OS trusts that CA, but Python's bundled certifi CA list doesn't, so the TLS handshake fails even though the connection reached the server. Newer OmniVoice builds trust the OS certificate store at startup (the truststore package), which should already fix this — update the app and retry. If you still see this, add an HTTPS-scanning exclusion for OmniVoice/Python in your antivirus, or ask IT for the proxy's CA bundle and set SSL_CERT_FILE to it, then restart.",
|
||||
"UNSUPPORTED_VIDEO_URL": "This link isn't a directly downloadable video. Paste a direct video page (e.g. a youtube.com/watch?v=… or douyin.com/video/<id> link), not a share/profile/feed link — or download the file and drop it in directly.",
|
||||
"VIDEO_DOWNLOAD_NETWORK": "The connection to the video server dropped mid-download (often a transient CDN/network blip or a regional rate-limit). Just retry — OmniVoice already cleaned up the partial download. If it keeps failing, check your network/VPN.",
|
||||
"BROKEN_VENV": "The Python backend environment was moved or damaged. OmniVoice rebuilds it automatically on the next launch; if it keeps failing, use Clean & Retry on the setup screen.",
|
||||
@@ -177,6 +178,7 @@ def append_hf_mirror_hint(text: str) -> str:
|
||||
# hint on a model-load timeout that leaks through the 500 handler.
|
||||
_CONTEXT_FREE_HINT_CLASSES = frozenset({
|
||||
"SOCKS_PROXY_SUPPORT_MISSING",
|
||||
"SSL_HANDSHAKE_FAILURE",
|
||||
})
|
||||
|
||||
|
||||
@@ -257,6 +259,19 @@ def classify(reason: str) -> str:
|
||||
# a message that also carries HF wording still names this class.
|
||||
if "socks proxy" in low or "socksio" in low:
|
||||
return "SOCKS_PROXY_SUPPORT_MISSING"
|
||||
# #976: a TLS handshake failing AFTER the TCP connection succeeds — the
|
||||
# signature of a corporate/antivirus proxy that TLS-inspects traffic and
|
||||
# re-signs certificates with a CA the OS trusts but Python's bundled
|
||||
# certifi list doesn't (a different failure mode from #984's TCP-level
|
||||
# "can't reach the host at all"). Requires "ssl" plus a handshake/cert-
|
||||
# verify marker so a generic connection error isn't mislabelled.
|
||||
if "ssl" in low and (
|
||||
"handshake" in low
|
||||
or "certificate verify failed" in low
|
||||
or "sslv3_alert" in low
|
||||
or "sslcertverificationerror" in low
|
||||
):
|
||||
return "SSL_HANDSHAKE_FAILURE"
|
||||
if ("huggingface" in low or "hf_token" in low or "401" in low or "unauthorized" in low) and (
|
||||
"token" in low or "auth" in low or "401" in low or "unauthorized" in low
|
||||
):
|
||||
|
||||
@@ -139,6 +139,24 @@ os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
||||
os.environ.setdefault("HF_HUB_ETAG_TIMEOUT", "15")
|
||||
os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "30")
|
||||
|
||||
# ── OS trust store for TLS (#976) ───────────────────────────────────────────
|
||||
# Users behind a corporate/antivirus proxy that TLS-inspects HTTPS traffic get
|
||||
# a raw "[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure"
|
||||
# on every model install — the TCP connection succeeds (a different failure
|
||||
# mode from #984's TCP-level blocked-host case), but the proxy re-signs the
|
||||
# certificate with its own root CA, which the OS trusts (Windows CryptoAPI/
|
||||
# SChannel) and Python's bundled `certifi` CA list does not. `inject_into_ssl`
|
||||
# patches `ssl.SSLContext` process-wide to verify against the OS trust store
|
||||
# instead, which is the actual fix (not just a nicer error message). Must run
|
||||
# here — at MODULE level, before huggingface_hub/requests/httpx do any network
|
||||
# I/O — not inside lifespan(), which runs too late. Not platform-gated: it's a
|
||||
# correctness improvement everywhere. Best-effort: never block startup.
|
||||
try:
|
||||
import truststore
|
||||
|
||||
truststore.inject_into_ssl()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prevent torchaudio from lazy-importing torchcodec (broken on some installs).
|
||||
# Proper fix = exclude torchcodec in pyproject.toml; this is a belt-and-braces guard.
|
||||
|
||||
@@ -154,6 +154,15 @@ dependencies = [
|
||||
# httpx imports it lazily inside try/except, so PyInstaller's tracer
|
||||
# misses it and frozen installers would stay broken without the entry.
|
||||
"socksio>=1.0",
|
||||
# OS trust store for TLS (#976). Users behind a corporate/antivirus proxy
|
||||
# that TLS-inspects traffic get a raw "[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE]"
|
||||
# on every model install — the TCP connection succeeds, but the proxy's
|
||||
# re-signed certificate is trusted by the OS (Windows CryptoAPI/SChannel)
|
||||
# and not by Python's bundled `certifi` CA list. `truststore` patches
|
||||
# `ssl.SSLContext` to verify against the OS trust store instead. Pure-
|
||||
# Python, MIT, PyPA-maintained, zero transitive deps — same class of fix
|
||||
# as socksio above, identical on macOS/Windows/Linux.
|
||||
"truststore>=0.9",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -143,6 +143,37 @@ def test_classify_socks_proxy_support_missing():
|
||||
assert failure.classify("ProxyError: connection refused by 10.0.0.1:8080") == ""
|
||||
|
||||
|
||||
def test_classify_ssl_handshake_failure():
|
||||
# #976: the exact error a Windows user behind a corporate/antivirus
|
||||
# TLS-inspecting proxy sees on every model install — the TCP connection
|
||||
# succeeds, but the handshake fails because the OS trusts the proxy's
|
||||
# re-signed CA and Python's bundled certifi list doesn't. A different
|
||||
# failure mode from #984's TCP-level "host unreachable" fix.
|
||||
reason = (
|
||||
"Install failed: Got: ConnectError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] "
|
||||
"ssl/tls alert handshake failure (_ssl.c:1016)"
|
||||
)
|
||||
assert failure.classify(reason) == "SSL_HANDSHAKE_FAILURE"
|
||||
evt = failure.build_failure(reason, stage="install", include_diagnostic=False)
|
||||
assert evt["docs_topic"] == "SSL_HANDSHAKE_FAILURE"
|
||||
assert evt["hint"], "the SSL-handshake class must carry an actionable hint"
|
||||
# A CERTIFICATE_VERIFY_FAILED-style message (the other common corporate-MITM
|
||||
# shape) must classify the same way.
|
||||
cert_reason = (
|
||||
"requests.exceptions.SSLError: HTTPSConnectionPool(host='huggingface.co', "
|
||||
"port=443): Max retries exceeded with url: / (Caused by SSLError("
|
||||
"SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate "
|
||||
"verify failed: unable to get local issuer certificate')))"
|
||||
)
|
||||
assert failure.classify(cert_reason) == "SSL_HANDSHAKE_FAILURE"
|
||||
# append_hint is the raw-string surface (setup/download.py's install SSE) —
|
||||
# the detail keeps the real error AND gains the hint.
|
||||
out = failure.append_hint(reason)
|
||||
assert out.startswith(reason) and "truststore" in out
|
||||
# A plain, unrelated connection error must NOT be mislabelled as SSL.
|
||||
assert failure.classify("ConnectionError: connection refused") == ""
|
||||
|
||||
|
||||
def test_classify_generic_still_empty():
|
||||
# A genuinely unknown reason must still classify to "" (no false hint).
|
||||
assert failure.classify("some totally unrelated failure") == ""
|
||||
|
||||
@@ -3247,6 +3247,7 @@ dependencies = [
|
||||
{ name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
||||
{ name = "torchaudio", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "transformers" },
|
||||
{ name = "truststore" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "webdataset" },
|
||||
{ name = "websockets" },
|
||||
@@ -3328,6 +3329,7 @@ requires-dist = [
|
||||
{ name = "torchaudio", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = ">=2.4" },
|
||||
{ name = "torchaudio", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = ">=2.4", index = "https://download.pytorch.org/whl/cu128" },
|
||||
{ name = "transformers", specifier = ">=5.3.0" },
|
||||
{ name = "truststore", specifier = ">=0.9" },
|
||||
{ name = "unidecode", marker = "extra == 'eval'" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "webdataset" },
|
||||
@@ -6234,6 +6236,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/63/8cb444ad5cdb25d999b7d647abac25af0ee37d292afc009940c05b82dda0/triton-3.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7936b18a3499ed62059414d7df563e6c163c5e16c3773678a3ee3d417865035d", size = 155659780, upload-time = "2025-07-30T19:58:51.171Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.24.1"
|
||||
|
||||
Reference in New Issue
Block a user