fix(helpers): use ssl.SSLError instead of broad OSError in disconnect tuple

OSError is too broad — it masks real errors like file-not-found.
ssl.SSLError specifically catches SSL-level disconnects without
swallowing unrelated OSError subtypes.

Closes the test_excludes_broad_oserror CI failure.
This commit is contained in:
Ed
2026-05-31 02:12:03 +02:00
committed by nesquena-hermes
parent 78652ecdea
commit 79f526b1b2
3 changed files with 13 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ Hermes Web UI -- HTTP helper functions.
import json as _json
import os
import re as _re
import ssl
from pathlib import Path
from api.config import IMAGE_EXTS, MD_EXTS
@@ -11,16 +12,12 @@ from api.config import IMAGE_EXTS, MD_EXTS
# Treat stalled/closed HTTP clients as normal disconnects. Long-lived SSE
# connections often end this way when a browser tab sleeps, a phone switches
# networks, or Tailscale leaves the socket half-closed.
#
# ssl.SSLError subclasses OSError, so the bare OSError arm already catches
# SSL-level disconnects. We keep OSError explicitly for socket-level errors
# (errno 32 EPIPE, 54 ECONNRESET, 104 ECONNABORTED, 110 ETIMEDOUT).
_CLIENT_DISCONNECT_ERRORS = (
BrokenPipeError,
ConnectionResetError,
ConnectionAbortedError,
TimeoutError,
OSError,
ssl.SSLError,
)

View File

@@ -226,11 +226,14 @@ class TestServerDisconnectHandling(unittest.TestCase):
# Patch handle_get to raise BrokenPipeError
import server as _server_mod
orig_handle_get = _server_mod.handle_get
orig_check_auth = _server_mod.check_auth
_server_mod.handle_get = _fake_handle_get
_server_mod.check_auth = lambda h, p: True
try:
Handler.do_GET(handler)
finally:
_server_mod.handle_get = orig_handle_get
_server_mod.check_auth = orig_check_auth
# send_response should NEVER be called for the 500 — client is gone
handler.send_response.assert_not_called()
@@ -284,11 +287,14 @@ class TestServerDisconnectHandling(unittest.TestCase):
import server as _server_mod
orig_handle_get = _server_mod.handle_get
orig_check_auth = _server_mod.check_auth
_server_mod.handle_get = _fake_handle_get
_server_mod.check_auth = lambda h, p: True
try:
Handler.do_GET(handler)
finally:
_server_mod.handle_get = orig_handle_get
_server_mod.check_auth = orig_check_auth
handler.send_response.assert_not_called()
@@ -302,11 +308,14 @@ class TestServerDisconnectHandling(unittest.TestCase):
import server as _server_mod
orig_handle_get = _server_mod.handle_get
orig_check_auth = _server_mod.check_auth
_server_mod.handle_get = _fake_handle_get
_server_mod.check_auth = lambda h, p: True
try:
Handler.do_GET(handler)
finally:
_server_mod.handle_get = orig_handle_get
_server_mod.check_auth = orig_check_auth
handler.send_response.assert_not_called()

View File

@@ -326,7 +326,8 @@ def test_messages_js_names_browser_sse_disconnect_separately():
def test_server_treats_broken_pipe_as_client_disconnect_not_500():
server_py = (models.Path(__file__).parent.parent / "server.py").read_text(encoding="utf-8")
assert "except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):" in server_py
# server.py now uses the centralized _CLIENT_DISCONNECT_ERRORS tuple from api.helpers
assert "_CLIENT_DISCONNECT_ERRORS" in server_py
assert "do not convert it into a misleading server 500" in server_py