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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user