fix: two error paths that made a failure worse than the thing that failed

/api/clip: _clip_wav/_clip_flac send audio headers and then stream frames.
A decode failure mid-stream unwound into _api_clip's handler, which called
_json_err(500) — writing a second status line and a JSON body into a
response that had already promised Content-Type: audio/wav and an exact
Content-Length. Under HTTP/1.1 keep-alive that desyncs the connection for
every subsequent request on it, so one bad clip corrupted unrelated
traffic. Track whether the headers went out; if they did, log and force
the connection closed instead, the same way _copy_to_response already
handles under-delivery.

config.ini: the int() coercion for split_minutes/max_retries/etc sat
outside the try that wraps recorder construction, so `split_minutes =
sixty` aborted the entire config load with an unhandled ValueError rather
than skipping that one section like every other config error. Moved
inside. split_duration is also clamped to >= 1 — 0 divided by zero in
get_next_split_time().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 20:29:54 +02:00
co-authored by Claude Opus 5
parent 1541110ffb
commit 9bb1229aa9
4 changed files with 150 additions and 15 deletions
+14 -1
View File
@@ -590,6 +590,9 @@ class _Handler(BaseHTTPRequestHandler):
min_gap: float = MIN_GAP_SECONDS
min_duration: float = MIN_DURATION_SECONDS
# True once /api/clip has written its audio headers — see _api_clip
_clip_started: bool = False
def do_DELETE(self):
parsed = urlparse(self.path)
p = parsed.path
@@ -962,6 +965,7 @@ class _Handler(BaseHTTPRequestHandler):
return
ext = path.suffix.lower()
self._clip_started = False
try:
if ext == '.wav':
self._clip_wav(path, start, end)
@@ -975,10 +979,19 @@ class _Handler(BaseHTTPRequestHandler):
except (ConnectionError, BrokenPipeError):
raise
except Exception as e:
self._json_err(500, f'Clip failed: {e}')
# Once the audio headers are out, a JSON error body would be a
# second response written into a stream that already promised
# audio/wav and a Content-Length — with keep-alive that desyncs
# the connection for every later request on it. Drop it instead.
if self._clip_started:
print(f'Clip failed mid-stream: {e}', flush=True)
self.close_connection = True
else:
self._json_err(500, f'Clip failed: {e}')
def _send_clip_headers(self, header: bytes, n_frames: int, channels: int,
sampwidth: int):
self._clip_started = True
self.send_response(200)
self.send_header('Content-Type', 'audio/wav')
self.send_header('Content-Length', str(len(header) + n_frames * channels * sampwidth))