HandleHLSSegment mapped every non-ErrSegmentNotFound error from the segment-retrieval/recovery path to a generic 500 "Failed to load segment". When a transcode process starts and then exits non-zero, WaitForSegment returns a wrapped playback.ErrTranscodeFailed, which fell through to that 500 — observed in production as repeated 500s on seg_00000.ts that drove an 8x client retry storm and crash-log uploads. The segment will never materialize once its transcode has died, so this is a not-found condition: Jellyfin's DynamicHls handler falls through to a PhysicalFileResult for the absent file, which ASP.NET serves as 404, never 500. Map ErrTranscodeFailed to 404 alongside ErrSegmentNotFound via a small extracted hlsSegmentErrorResponse helper, reserving 500 for genuinely unexpected errors. Adds a unit test pinning the mapping.
51 lines
1.6 KiB
Go
51 lines
1.6 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/playback"
|
|
)
|
|
|
|
// TestHLSSegmentErrorResponse pins the never-500 contract for the HLS segment
|
|
// handler: a segment that will never materialize (absent, or whose transcode
|
|
// process started then died) maps to 404 like Jellyfin, and only genuinely
|
|
// unexpected errors keep the 500.
|
|
func TestHLSSegmentErrorResponse(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
wantStatus int
|
|
wantCode string
|
|
}{
|
|
{"segment not found", playback.ErrSegmentNotFound, http.StatusNotFound, "NotFound"},
|
|
{"transcode failed", playback.ErrTranscodeFailed, http.StatusNotFound, "NotFound"},
|
|
{
|
|
// WaitForSegment wraps the ffmpeg exit error as
|
|
// fmt.Errorf("%w: %v", ErrTranscodeFailed, waitErr) — this is exactly the
|
|
// error that hit the catch-all 500 in production (Fire TV, seg_00000.ts).
|
|
name: "wrapped transcode failed",
|
|
err: fmt.Errorf("%w: exit status 1", playback.ErrTranscodeFailed),
|
|
wantStatus: http.StatusNotFound,
|
|
wantCode: "NotFound",
|
|
},
|
|
{
|
|
name: "unexpected error stays 500",
|
|
err: errors.New("stat segment: permission denied"),
|
|
wantStatus: http.StatusInternalServerError,
|
|
wantCode: "ServerError",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
status, code, _ := hlsSegmentErrorResponse(tt.err)
|
|
if status != tt.wantStatus || code != tt.wantCode {
|
|
t.Fatalf("hlsSegmentErrorResponse(%v) = (%d, %q), want (%d, %q)",
|
|
tt.err, status, code, tt.wantStatus, tt.wantCode)
|
|
}
|
|
})
|
|
}
|
|
}
|