| @@ -294,7 +294,7 @@ Push raw audio data into the live stream buffer. Format: **S16LE stereo PCM** at | |||
| Requires `--audio-stdin`, `--audio-http`, or another configured stream source to feed the buffer. | |||
| **Request:** Binary body, `application/octet-stream`, raw S16LE stereo PCM bytes. Set `Content-Type` to `application/octet-stream` or `audio/L16`; other media types are rejected. | |||
| **Request:** Binary body, `application/octet-stream`, raw S16LE stereo PCM bytes. Set `Content-Type` to `application/octet-stream` or `audio/L16`; other media types are rejected. Requests larger than 512 MiB are rejected with `413 Request Entity Too Large`. | |||
| **Response:** | |||
| ```json | |||
| @@ -325,6 +325,7 @@ ffmpeg -i song.mp3 -f s16le -ar 44100 -ac 2 - | \ | |||
| **Errors:** | |||
| - `405` if not POST | |||
| - `415` if Content-Type is missing or unsupported (must be `application/octet-stream` or `audio/L16`) | |||
| - `413` if the upload body exceeds the 512 MiB limit | |||
| - `503` if no audio stream configured | |||
| --- | |||
| @@ -443,12 +443,14 @@ Diese Punkte könnten ggf. vorgezogen werden, auch wenn WS-05 formal nach WS-01/ | |||
| ## WS-05 Entscheidungslog | |||
| - 2026-04-06: `/audio/stream` now enforces a binary `Content-Type` (`application/octet-stream` or `audio/L16`) before queuing any samples. | |||
| - 2026-04-06: `/audio/stream` caps uploads at 512 MiB and rejects larger bodies with `413 Request Entity Too Large` before touching the ring buffer. | |||
| ## WS-05 Verifikation | |||
| | Datum | Fokus | Ergebnis | | |||
| |---|---|---| | |||
| | 2026-04-05 | `/audio/stream` rejects non-POST requests | `TestAudioStreamRejectsNonPost` enforces POST-only access to `/audio/stream` before a stream source is configured | | |||
| | 2026-04-06 | `/audio/stream` enforces binary Content-Type headers | `TestAudioStreamRejectsMissingContentType` and `TestAudioStreamRejectsUnsupportedContentType` confirm 415 when the media type is missing or wrong | | |||
| | 2026-04-06 | `/audio/stream` rejects oversized uploads | `TestAudioStreamRejectsBodyTooLarge` confirms a 413 Request Entity Too Large before buffering when the HTTP body exceeds the 512 MiB guard | | |||
| --- | |||
| @@ -3,6 +3,7 @@ package control | |||
| import ( | |||
| _ "embed" | |||
| "encoding/json" | |||
| "errors" | |||
| "io" | |||
| "mime" | |||
| "net/http" | |||
| @@ -56,6 +57,7 @@ const ( | |||
| configContentTypeHeader = "application/json" | |||
| noBodyErrMsg = "request must not include a body" | |||
| audioStreamContentTypeError = "Content-Type must be application/octet-stream or audio/L16" | |||
| audioStreamBodyLimitDefault = 512 << 20 // 512 MiB | |||
| ) | |||
| var audioStreamAllowedMediaTypes = []string{ | |||
| @@ -63,6 +65,8 @@ var audioStreamAllowedMediaTypes = []string{ | |||
| "audio/l16", | |||
| } | |||
| var audioStreamBodyLimit = int64(audioStreamBodyLimitDefault) // bytes allowed per /audio/stream request; tests may override. | |||
| func isJSONContentType(r *http.Request) bool { | |||
| ct := strings.TrimSpace(r.Header.Get("Content-Type")) | |||
| if ct == "" { | |||
| @@ -284,6 +288,8 @@ func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { | |||
| return | |||
| } | |||
| r.Body = http.MaxBytesReader(w, r.Body, audioStreamBodyLimit) | |||
| // Read body in chunks and push to ring buffer | |||
| buf := make([]byte, 32768) | |||
| totalFrames := 0 | |||
| @@ -296,6 +302,11 @@ func (s *Server) handleAudioStream(w http.ResponseWriter, r *http.Request) { | |||
| if err == io.EOF { | |||
| break | |||
| } | |||
| var maxErr *http.MaxBytesError | |||
| if errors.As(err, &maxErr) { | |||
| http.Error(w, maxErr.Error(), http.StatusRequestEntityTooLarge) | |||
| return | |||
| } | |||
| http.Error(w, err.Error(), http.StatusInternalServerError) | |||
| return | |||
| } | |||
| @@ -388,6 +388,28 @@ func TestAudioStreamRejectsUnsupportedContentType(t *testing.T) { | |||
| } | |||
| } | |||
| func TestAudioStreamRejectsBodyTooLarge(t *testing.T) { | |||
| orig := audioStreamBodyLimit | |||
| t.Cleanup(func() { | |||
| audioStreamBodyLimit = orig | |||
| }) | |||
| audioStreamBodyLimit = 1024 | |||
| limit := int(audioStreamBodyLimit) | |||
| body := make([]byte, limit+1) | |||
| srv := NewServer(cfgpkg.Default()) | |||
| srv.SetStreamSource(audio.NewStreamSource(256, 44100)) | |||
| rec := httptest.NewRecorder() | |||
| req := httptest.NewRequest(http.MethodPost, "/audio/stream", bytes.NewReader(body)) | |||
| req.Header.Set("Content-Type", "application/octet-stream") | |||
| srv.Handler().ServeHTTP(rec, req) | |||
| if rec.Code != http.StatusRequestEntityTooLarge { | |||
| t.Fatalf("expected 413 for oversized body, got %d", rec.Code) | |||
| } | |||
| if !strings.Contains(rec.Body.String(), "request body too large") { | |||
| t.Fatalf("unexpected response body: %q", rec.Body.String()) | |||
| } | |||
| } | |||
| func TestTXStartWithoutController(t *testing.T) { | |||
| srv := NewServer(cfgpkg.Default()) | |||
| rec := httptest.NewRecorder() | |||