mvanhorn/last30days-skill · error · KeyError

StockTwits symbol stream was not returned

Error message

StockTwits symbol stream was not returned

What it means

KeyError raised inside StockTwits refetch_datum when a paginated GET to the symbol stream returns a payload that is not a dict or has no 'messages' list. The StockTwits public API normally always includes messages (possibly empty), so this signals a malformed/proxy/error response masquerading as data.

Source

Thrown at skills/last30days/scripts/lib/stocktwits.py:323

    if datum_key != "pct_bullish":
        raise KeyError(f"Unsupported StockTwits datum: {datum_key}")
    symbol = str(item.metadata.get("symbol") or item.container or "").strip().upper()
    if not symbol:
        raise ValueError("StockTwits item has no symbol")
    url = _STREAM_URL.format(symbol=urllib.parse.quote(symbol))
    window = item.metadata.get("freshness_window") or {}
    depth = str(window.get("depth") or "default")
    target = _DEPTH.get(depth, _DEPTH["default"])
    messages: list[dict[str, Any]] = []
    cursor_max = None
    while len(messages) < target:
        request_kwargs: dict[str, Any] = {"timeout": 10, "retries": 2}
        if cursor_max:
            request_kwargs["params"] = {"max": cursor_max}
        data = http.request("GET", url, **request_kwargs)
        if not isinstance(data, dict) or not isinstance(data.get("messages"), list):
            raise KeyError("StockTwits symbol stream was not returned")
        batch = data["messages"]
        if not batch:
            break
        messages.extend(batch)
        cursor = data.get("cursor") or {}
        if not cursor.get("more") or not cursor.get("max"):
            break
        cursor_max = cursor["max"]
    messages = _filter_by_date(
        messages,
        window.get("from_date"),
        window.get("to_date"),
    )
    aggregate = aggregate_sentiment(messages)
    value = aggregate.get("pct_bullish")
    if value is None:
        raise KeyError("StockTwits stream has no tagged sentiment")
    newest = max(

View on GitHub (pinned to c7460f6114)

Solutions

  1. Catch KeyError and treat the datum as unrefreshable, keeping the cached value with a stale flag
  2. Retry once after a short delay - transient gateway errors are common on the unauth endpoint
  3. Log the raw payload shape when this fires to distinguish outage from contract change

Example fix

# before
data = refetch_datum(item, "pct_bullish")  # KeyError on bad envelope

# after
try:
    data = refetch_datum(item, "pct_bullish")
except KeyError:
    data = {"value": cached_value, "stale": True}
Defensive patterns

Strategy: try-catch

Try / catch

try:
    datum = refetch_datum(item, "pct_bullish")
except KeyError as e:
    if "stream was not returned" in str(e):
        datum = retry_once_with_delay(item, delay=5) or cached_with_stale_flag(item)

Prevention

When it happens

Trigger: http.request('GET', stream_url) returning an HTML error page parsed as non-dict, a JSON error envelope without 'messages', or None/empty body - any of these fail the isinstance check and raise immediately inside the pagination loop.

Common situations: StockTwits API outages or Cloudflare challenge pages; rate limiting returning a JSON error body with 200; corporate proxies injecting block pages; API contract changes dropping the messages field.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/bb621cd77d8c8fdb. Report an issue: GitHub.