Skip to main content
The /movie and /tv endpoints stream via SSE — error handling looks different from a standard JSON API. This page covers every failure mode and the recommended pattern for each.

Error Response Shape

Non-streaming endpoints (/health, /subtitles, /downloads, /test) return standard JSON errors:
{ "error": "description of what went wrong" }
HTTP StatusWhen
400Missing or invalid request parameters
404No data found (subtitles, downloads)
500Unexpected server error
For /movie and /tv, HTTP 200 is returned immediately when the SSE stream opens. Failures surface as zero source events followed by a done event with total: 0 — not as HTTP error codes.

Handling No Sources

The most common failure — all providers failed to return a playable stream. Detect it by checking total in the done event.
const BASE = 'https://missourimonster-vyla.hf.space';

const res     = await fetch(`${BASE}/movie?id=550`);
const reader  = res.body!.getReader();
const decoder = new TextDecoder();
let buffer    = '';
let started   = false;

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop()!;

  for (const line of lines) {
    if (!line.startsWith('data: ')) continue;
    const event = JSON.parse(line.slice(6));

    if (event.type === 'source') { started = true; playSource(event.source.url); }

    if (event.type === 'done' && !started) {
      showError('This title is currently unavailable. Try again later.');
    }
  }
}

Source Fallback Queue

Sources stream in as providers resolve. Queue every source event URL and fall back to the next one if playback fails mid-stream:
import Hls from 'hls.js';

const BASE = 'https://missourimonster-vyla.hf.space';

async function playWithFallback(
  videoEl: HTMLVideoElement,
  tmdbId: number,
  season?: number,
  episode?: number,
  onSourceChange?: (label: string) => void
) {
  const endpoint = season && episode
    ? `${BASE}/tv?id=${tmdbId}&season=${season}&episode=${episode}`
    : `${BASE}/movie?id=${tmdbId}`;

  const queue: { url: string; label: string }[] = [];
  let started = false;
  let hls: Hls | null = null;

  function tryNext() {
    if (!queue.length) {
      console.error('All sources exhausted');
      return;
    }

    const { url, label } = queue.shift()!;
    onSourceChange?.(label);

    hls?.destroy();
    hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 });

    hls.on(Hls.Events.ERROR, (_, err) => {
      if (err.fatal) {
        console.warn(`Source "${label}" failed:`, err.type);
        tryNext();
      }
    });

    hls.loadSource(url);
    hls.attachMedia(videoEl);
    hls.on(Hls.Events.MANIFEST_PARSED, () => videoEl.play());
  }

  const res     = await fetch(endpoint);
  const reader  = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer    = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop()!;

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const event = JSON.parse(line.slice(6));

      if (event.type === 'source') {
        queue.push({ url: event.source.url, label: event.source.label });
        if (!started) { started = true; tryNext(); }
      }
    }
  }

  return () => hls?.destroy();
}

Network Timeouts

The SSE stream can stay open 20+ seconds while slow providers resolve. Set a client-side timeout using AbortController:
async function fetchStreamWithTimeout(
  url: string,
  timeoutMs = 20000
): Promise<ReadableStreamDefaultReader<Uint8Array>> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timer);
    return res.body!.getReader();
  } catch (err: any) {
    clearTimeout(timer);
    if (err.name === 'AbortError') throw new Error('Stream timed out');
    throw err;
  }
}

Handling Missing Subtitles

Subtitles arrive in the meta event. Always guard against an empty array:
if (event.type === 'meta') {
  if (!event.subtitles.length) {
    subtitleButton.style.display = 'none';
  } else {
    attachSubtitles(videoEl, event.subtitles);
  }
}

Retry Logic for Stream Failures

Add a top-level retry for complete stream failures (network errors, server 5xx before the stream opens):
async function fetchStreamWithRetry(
  url: string,
  attempts = 3,
  delayMs = 2000
): Promise<Response> {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(url);
      if (res.ok) return res;
      if (i === attempts - 1) throw new Error(`HTTP ${res.status}`);
    } catch (err) {
      if (i === attempts - 1) throw err;
      await new Promise(r => setTimeout(r, delayMs * (i + 1)));
    }
  }
  throw new Error('Max retries exceeded');
}

Checking Health Before Requests

If you’re building a dashboard, check provider health before opening a stream to give users an early warning:
async function checkAndStream(tmdbId: number) {
  const health = await fetch(
    'https://missourimonster-vyla.hf.space/health'
  ).then(r => r.json());

  if (health.status === 'degraded') {
    const down = Object.entries(health.sources)
      .filter(([, v]: any) => !v.ok)
      .map(([k]) => k);
    console.warn(`Degraded providers: ${down.length}`);
  }

  // Proceed — degraded just means fewer sources will stream
  return fetch(
    `https://missourimonster-vyla.hf.space/movie?id=${tmdbId}`
  );
}

Full Error Boundary (React)

import { useState, useCallback, useRef } from 'react';
import Hls from 'hls.js';

const BASE = 'https://missourimonster-vyla.hf.space';

type PlayerState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'playing'; sourceLabel: string }
  | { status: 'error'; message: string };

export function useStreamPlayer(tmdbId: number) {
  const [state, setState] = useState<PlayerState>({ status: 'idle' });
  const hlsRef = useRef<Hls | null>(null);

  const load = useCallback(async (videoEl: HTMLVideoElement) => {
    setState({ status: 'loading' });

    const queue: { url: string; label: string }[] = [];
    let started = false;

    function tryNext() {
      if (!queue.length) {
        setState({ status: 'error', message: 'All sources failed.' });
        return;
      }

      const { url, label } = queue.shift()!;
      hlsRef.current?.destroy();

      const hls = new Hls();
      hlsRef.current = hls;

      hls.on(Hls.Events.MANIFEST_PARSED, () => {
        setState({ status: 'playing', sourceLabel: label });
        videoEl.play();
      });
      hls.on(Hls.Events.ERROR, (_, err) => {
        if (err.fatal) { hls.destroy(); tryNext(); }
      });

      hls.loadSource(url);
      hls.attachMedia(videoEl);
    }

    try {
      const res     = await fetch(`${BASE}/movie?id=${tmdbId}`);
      const reader  = res.body!.getReader();
      const decoder = new TextDecoder();
      let buffer    = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop()!;

        for (const line of lines) {
          if (!line.startsWith('data: ')) continue;
          const event = JSON.parse(line.slice(6));

          if (event.type === 'source') {
            queue.push({ url: event.source.url, label: event.source.label });
            if (!started) { started = true; tryNext(); }
          }

          if (event.type === 'done' && !started) {
            setState({ status: 'error', message: 'No sources available for this title.' });
          }
        }
      }
    } catch {
      setState({ status: 'error', message: 'Network error. Check your connection.' });
    }
  }, [tmdbId]);

  return { state, load };
}