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:
@@ -591,6 +591,57 @@ url = http://x.com/s2
|
||||
assert len(mgr.recorders) == 1
|
||||
assert mgr.recorders[0].name == "good_source"
|
||||
|
||||
def test_bad_integer_skips_only_that_source(self, tmp_path):
|
||||
# A non-numeric split_minutes used to raise ValueError out of the
|
||||
# config loop and abort the whole load, taking healthy sources with it.
|
||||
log_file = str(tmp_path / "test.log")
|
||||
config_text = f"""
|
||||
[general]
|
||||
log_level = WARNING
|
||||
log_file = {log_file}
|
||||
|
||||
[broken]
|
||||
type = stream
|
||||
url = http://x.com/s
|
||||
split_minutes = sixty
|
||||
|
||||
[good_source]
|
||||
type = stream
|
||||
url = http://x.com/s2
|
||||
"""
|
||||
cfg_path = tmp_path / "config.ini"
|
||||
cfg_path.write_text(config_text)
|
||||
|
||||
with patch.object(isr, "AudioSystem") as mock_as:
|
||||
mock_as.return_value.available = False
|
||||
mgr = isr.RecorderManager(str(cfg_path))
|
||||
|
||||
assert [r.name for r in mgr.recorders] == ["good_source"]
|
||||
|
||||
def test_zero_split_minutes_is_clamped(self, tmp_path):
|
||||
# split_minutes = 0 reached get_next_split_time() and divided by zero.
|
||||
log_file = str(tmp_path / "test.log")
|
||||
config_text = f"""
|
||||
[general]
|
||||
log_level = WARNING
|
||||
log_file = {log_file}
|
||||
|
||||
[zero_split]
|
||||
type = stream
|
||||
url = http://x.com/s
|
||||
split_minutes = 0
|
||||
"""
|
||||
cfg_path = tmp_path / "config.ini"
|
||||
cfg_path.write_text(config_text)
|
||||
|
||||
with patch.object(isr, "AudioSystem") as mock_as:
|
||||
mock_as.return_value.available = False
|
||||
mgr = isr.RecorderManager(str(cfg_path))
|
||||
|
||||
rec = mgr.recorders[0]
|
||||
assert rec.split_duration == 1
|
||||
rec.get_next_split_time() # must not raise ZeroDivisionError
|
||||
|
||||
def test_missing_config_file_exits(self, tmp_path):
|
||||
with pytest.raises(SystemExit):
|
||||
# main() calls sys.exit(1) when the file is missing
|
||||
|
||||
Reference in New Issue
Block a user