Files
silo-server/internal/jellycompat/streams_segment_error_test.go
d3v1l1989andGitHub 3f3cf55002 fix(jellycompat): return 404 for HLS segments of a failed transcode (#163)
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.
2026-06-16 11:14:14 -04:00

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)
}
})
}
}