Hmbown/CodeWhale · error

Mobile stream ticket was unavailable

Error message

Mobile stream ticket was unavailable

What it means

refreshStreamTicket fetches a new stream ticket via POST /__codewhale/mobile/stream-ticket and validates the result. If the response is empty or `stream_ticket` is not a string, the runtime cannot resume/authenticate the event stream, so it throws.

Solutions

  1. Retry refreshStreamTicket(); transient backend errors often resolve.
  2. Check the Runtime version serving the page matches the endpoint's response schema.
  3. Inspect the endpoint's raw response (curl with the same auth) to see what is actually returned.

Example fix

// before
const ticket = await refreshStreamTicket();
// after
let ticket;
try { ticket = await refreshStreamTicket(); }
catch (e) { ticket = await refreshStreamTicket(); /* retry once */ }
Defensive patterns

Strategy: retry

Validate before calling

if (result && typeof result.stream_ticket !== 'string') {
  throw new Error('bad ticket payload');
}

Type guard

function hasStreamTicket(r) {
  return !!r && typeof r.stream_ticket === 'string' && r.stream_ticket.length > 0;
}

Try / catch

try {
  ticket = await refreshStreamTicket();
} catch {
  await new Promise((r) => setTimeout(r, 500));
  ticket = await refreshStreamTicket();
}

Prevention

When it happens

Trigger: The stream-ticket endpoint returns 204/null, a body without `stream_ticket`, or a non-string ticket value (server schema change or backend failure).

Common situations: Runtime backend failing to mint tickets; version mismatch between mobile page and server response shape; transient server error already surfaced through api() and a malformed fallback body.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/64d7dbcf2b620520. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_mobile.html:403

      }
      if (!res.ok) {
        let detail = await res.text();
        try {
          const parsed = JSON.parse(detail);
          detail = parsed.error?.message || detail;
        } catch (_) {}
        throw new Error(detail || ("HTTP " + res.status));
      }
      useMobileSession(await res.json());
    }

    async function refreshStreamTicket() {
      const result = await api("/__codewhale/mobile/stream-ticket", {
        method: "POST",
        body: "{}"
      });
      if (!result || typeof result.stream_ticket !== "string") {
        throw new Error("Mobile stream ticket was unavailable");
      }
      state.streamTicket = result.stream_ticket;
      saveMobileSession();
      return state.streamTicket;
    }

    async function takeStreamTicket() {
      if (!state.requestProof) return "";
      const ticket = state.streamTicket || await refreshStreamTicket();
      state.streamTicket = "";
      saveMobileSession();
      return ticket;
    }

    function escapeHtml(raw) {
      return String(raw).replace(/[&<>"']/g, (char) => ({
        "&": "&amp;",
        "<": "&lt;",

View on GitHub (pinned to 73e0f67d83)