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
+16 -14
View File
@@ -272,8 +272,9 @@ class BaseRecorder(ABC):
self.current_filename = None
self._clock = clock or datetime.now
# Common settings
self.split_duration = config.get('split_minutes', 60)
# Common settings. split_duration is clamped: 0 would divide by zero
# in get_next_split_time(), negative would split every tick.
self.split_duration = max(1, config.get('split_minutes', 60))
self.output_dir = config.get('output_directory', 'recordings')
self.max_retries = config.get('max_retries', 10)
self.retry_delay = config.get('retry_delay_seconds', 5)
@@ -775,19 +776,20 @@ class RecorderManager:
self.logger.warning(f"Section [{section}] has no 'type', skipping")
continue
# Build source config by merging general with section-specific
source_config = general.copy()
for key, value in config.items(section):
if key == 'type':
continue
if key in ('split_minutes', 'max_retries', 'retry_delay_seconds', 'sample_rate', 'channels'):
source_config[key] = int(value)
else:
source_config[key] = value
# Create recorder
# Build the source config and create the recorder. Both are inside
# the try: a non-numeric split_minutes used to raise out here and
# abort the whole config load instead of skipping one section.
try:
source_config = general.copy()
for key, value in config.items(section):
if key == 'type':
continue
if key in ('split_minutes', 'max_retries', 'retry_delay_seconds', 'sample_rate', 'channels'):
source_config[key] = int(value)
else:
source_config[key] = value
if source_type == 'stream':
recorder = StreamRecorder(section, source_config, self.logger)
self.recorders.append(recorder)