@@ -1034,8 +1034,9 @@ func TestGetVideoAPI_NoStreamURL(t *testing.T) {
10341034func TestGetVideoAPI_InvalidJSON (t * testing.T ) {
10351035 t .Parallel ()
10361036
1037+ // Non-JSON, non-HTML body still surfaces a JSON decode error.
10371038 srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
1038- fmt .Fprint (w , `<html>Error</html> ` )
1039+ fmt .Fprint (w , `{not json ` )
10391040 }))
10401041 defer srv .Close ()
10411042
@@ -1955,3 +1956,143 @@ func TestRegexPatterns(t *testing.T) {
19551956 assert .Equal (t , "https://subs.example.com/pt.vtt" , match [2 ])
19561957 })
19571958}
1959+
1960+ // =============================================================================
1961+ // Regression tests (added 2026-04-30)
1962+ //
1963+ // Context: SuperFlix moved from `superflixapi.rest` to `superflixapi.online`
1964+ // using a server-side 301 redirect. Go's http.Client follows the redirect but
1965+ // downgrades the POST to a GET (dropping the body), so /player/bootstrap
1966+ // returned an HTML 404 page. The JSON decoder then surfaced the cryptic
1967+ // `invalid character '<' looking for beginning of value`, breaking playback.
1968+ // These tests pin (a) the canonical base URL and (b) that an HTML/non-2xx
1969+ // response from the player API produces a clear, actionable error rather
1970+ // than the cryptic JSON decode error.
1971+ // =============================================================================
1972+
1973+ func TestSuperFlixBase_PointsToOnlineHost_2026_04_30 (t * testing.T ) {
1974+ t .Parallel ()
1975+ // Pinning the canonical host. If this needs to change in the future,
1976+ // also update internal/api/providers/metadata/metadata.go.
1977+ assert .Equal (t , "https://superflixapi.online" , SuperFlixBase )
1978+ }
1979+
1980+ func TestBootstrap_HTMLResponseSurfacesActionableError_2026_04_30 (t * testing.T ) {
1981+ t .Parallel ()
1982+
1983+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
1984+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
1985+ w .WriteHeader (http .StatusNotFound )
1986+ fmt .Fprint (w , `<!DOCTYPE html><html><head><title>Not Found</title></head><body>404</body></html>` )
1987+ }))
1988+ defer srv .Close ()
1989+
1990+ client := newTestSuperFlixClient (srv .URL )
1991+ tokens := & SuperFlixTokens {CSRF : "a" , PageToken : "b" , ContentID : "1" , ContentType : "filme" }
1992+ _ , err := client .Bootstrap (context .Background (), tokens )
1993+
1994+ require .Error (t , err )
1995+ // Must NOT leak the cryptic JSON decode error.
1996+ assert .NotContains (t , err .Error (), "invalid character '<'" )
1997+ // Must surface the real cause: HTML body with status code in context.
1998+ assert .Contains (t , err .Error (), "bootstrap" )
1999+ assert .Contains (t , err .Error (), "HTML" )
2000+ assert .Contains (t , err .Error (), "404" )
2001+ }
2002+
2003+ func TestGetSourceURL_HTMLResponseSurfacesActionableError_2026_04_30 (t * testing.T ) {
2004+ t .Parallel ()
2005+
2006+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
2007+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
2008+ w .WriteHeader (http .StatusForbidden )
2009+ fmt .Fprint (w , `<html><body>blocked</body></html>` )
2010+ }))
2011+ defer srv .Close ()
2012+
2013+ client := newTestSuperFlixClient (srv .URL )
2014+ tokens := & SuperFlixTokens {CSRF : "a" , PageToken : "b" }
2015+ _ , err := client .GetSourceURL (context .Background (), "vid" , tokens )
2016+
2017+ require .Error (t , err )
2018+ assert .NotContains (t , err .Error (), "invalid character '<'" )
2019+ assert .Contains (t , err .Error (), "source" )
2020+ assert .Contains (t , err .Error (), "HTML" )
2021+ assert .Contains (t , err .Error (), "403" )
2022+ }
2023+
2024+ func TestGetVideoAPI_HTMLResponseSurfacesActionableError_2026_04_30 (t * testing.T ) {
2025+ t .Parallel ()
2026+
2027+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
2028+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
2029+ fmt .Fprint (w , `<html><body>captcha</body></html>` )
2030+ }))
2031+ defer srv .Close ()
2032+
2033+ client := newTestSuperFlixClient (srv .URL )
2034+ _ , _ , err := client .GetVideoAPI (context .Background (), srv .URL , "hash" , srv .URL + "/" )
2035+
2036+ require .Error (t , err )
2037+ assert .NotContains (t , err .Error (), "invalid character '<'" )
2038+ assert .Contains (t , err .Error (), "video API" )
2039+ assert .Contains (t , err .Error (), "HTML" )
2040+ }
2041+
2042+ // Some upstream players (firevideoplayer.com behind llanfairpwllgwyngy.com)
2043+ // serve real JSON with `Content-Type: text/html; charset=utf-8`. Trusting the
2044+ // header alone would reject these valid responses. The body sniff is the
2045+ // source of truth.
2046+ func TestGetVideoAPI_AcceptsJSONBodyWithHTMLContentType_2026_04_30 (t * testing.T ) {
2047+ t .Parallel ()
2048+
2049+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
2050+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
2051+ fmt .Fprint (w , `{"hls":true,"securedLink":"https://example.com/master.m3u8","videoSource":"https://example.com/master.txt","videoImage":"https://example.com/thumb.jpg"}` )
2052+ }))
2053+ defer srv .Close ()
2054+
2055+ client := newTestSuperFlixClient (srv .URL )
2056+ streamURL , thumb , err := client .GetVideoAPI (context .Background (), srv .URL , "hash" , srv .URL + "/" )
2057+
2058+ require .NoError (t , err )
2059+ assert .Equal (t , "https://example.com/master.m3u8" , streamURL )
2060+ assert .Equal (t , "https://example.com/thumb.jpg" , thumb )
2061+ }
2062+
2063+ func TestBootstrap_AcceptsJSONBodyWithHTMLContentType_2026_04_30 (t * testing.T ) {
2064+ t .Parallel ()
2065+
2066+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
2067+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
2068+ fmt .Fprint (w , `{"data":{"options":[{"ID":"sv1","name":"Server 1"}]}}` )
2069+ }))
2070+ defer srv .Close ()
2071+
2072+ client := newTestSuperFlixClient (srv .URL )
2073+ tokens := & SuperFlixTokens {CSRF : "a" , PageToken : "b" , ContentID : "1" , ContentType : "filme" }
2074+ servers , err := client .Bootstrap (context .Background (), tokens )
2075+
2076+ require .NoError (t , err )
2077+ require .Len (t , servers , 1 )
2078+ assert .Equal (t , "Server 1" , servers [0 ].Name )
2079+ }
2080+
2081+ func TestEnsureJSONResponse_BlankBodyWithBadStatus_2026_04_30 (t * testing.T ) {
2082+ t .Parallel ()
2083+
2084+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
2085+ w .Header ().Set ("Content-Type" , "application/json" )
2086+ w .WriteHeader (http .StatusInternalServerError )
2087+ // Empty body — JSON decode would also fail with EOF; ensure the
2088+ // status-code path produces a useful error first.
2089+ }))
2090+ defer srv .Close ()
2091+
2092+ client := newTestSuperFlixClient (srv .URL )
2093+ tokens := & SuperFlixTokens {CSRF : "a" , PageToken : "b" , ContentID : "1" , ContentType : "filme" }
2094+ _ , err := client .Bootstrap (context .Background (), tokens )
2095+
2096+ require .Error (t , err )
2097+ assert .Contains (t , err .Error (), "500" )
2098+ }
0 commit comments