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
- Catch KeyError and treat the datum as unrefreshable, keeping the cached value with a stale flag
- Retry once after a short delay - transient gateway errors are common on the unauth endpoint
- 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
- Retry once after a short delay - Cloudflare/gateway hiccups are common
- Log the raw payload when this fires to catch API contract changes early
- Keep last-known values with a stale flag instead of dropping the datum
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
- Polymarket event was not found
- Polymarket event is closed, unavailable, or malformed
- StockTwits stream has no tagged sentiment
- {status}: {message}
- Polymarket datum {datum_key!r} was not found
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/bb621cd77d8c8fdb.
Report an issue: GitHub.