mvanhorn/last30days-skill · error · ValueError

Unsupported web backend: {backend!r}

Error message

Unsupported web backend: {backend!r}

What it means

Raised by grounding.py when the configured web_backend string matches none of the known backends (brave, exa, serper, parallel, keyless, none). This is a ValueError (invalid argument), not a missing-key RuntimeError — it fires before any key lookup and echoes the offending value via repr so whitespace and case errors are visible.

Source

Thrown at skills/last30days/scripts/lib/grounding.py:274

    elif backend == "exa":
        key = config.get("EXA_API_KEY")
        if not key:
            raise RuntimeError("EXA_API_KEY is required when web_backend='exa'")
        items, artifact = exa_search(query, date_range, key)
    elif backend == "serper":
        key = config.get("SERPER_API_KEY")
        if not key:
            raise RuntimeError("SERPER_API_KEY is required when web_backend='serper'")
        items, artifact = serper_search(query, date_range, key)
    elif backend == "parallel":
        key = config.get("PARALLEL_API_KEY")
        if not key:
            raise RuntimeError("PARALLEL_API_KEY is required when web_backend='parallel'")
        items, artifact = parallel_search(query, date_range, key)
    elif backend == "keyless":
        items, artifact = web_search_keyless.keyless_search(query, date_range, config)
    elif backend != "none":
        raise ValueError(f"Unsupported web backend: {backend!r}")
    else:
        return [], {}
    if items and not _reddit_excluded(config):
        # Reddit enrichment is a best-effort secondary fetch on already-retrieved
        # web results. Isolate its HTTP failures in a throwaway capture sink so a
        # reddit.com fetch failure (e.g. a 403 on a datacenter IP) is not
        # attributed to the web/grounding source itself — which would otherwise
        # discard the successfully retrieved results and report the source failed.
        with http.capture_failures():
            items = _enrich_reddit_items(items)
    return items, artifact


def _reddit_excluded(config: dict) -> bool:
    """Return True when EXCLUDE_SOURCES contains 'reddit'.

    Respects the same suppression knob the pipeline uses for source gating,
    so a user who set EXCLUDE_SOURCES=reddit doesn't get Reddit content

View on GitHub (pinned to c7460f6114)

Solutions

  1. Set web_backend to one of: brave, exa, serper, parallel, keyless, none.
  2. Strip whitespace/CR from the value (check for trailing newline when read from a file).
  3. Remove the setting entirely to use automatic resolution.

Example fix

# before
WEB_BACKEND=brave-search

# after
WEB_BACKEND=brave
Defensive patterns

Strategy: validation

Validate before calling

ALLOWED_BACKENDS = {"brave", "exa", "serper", "parallel", "keyless", "none"}

backend = str(config.get("web_backend", "")).strip().lower()
assert backend in ALLOWED_BACKENDS, f"web_backend must be one of {sorted(ALLOWED_BACKENDS)}, got {backend!r}"

Type guard

def is_valid_web_backend(value: object) -> bool:
    return isinstance(value, str) and value.strip().lower() in {
        "brave", "exa", "serper", "parallel", "keyless", "none"
    }

Try / catch

try:
    grounding.web_search_with_backend(query, config)
except ValueError as e:
    if 'Unsupported web backend' in str(e):
        # normalize the value (strip().lower()) or unset it
        ...

Prevention

When it happens

Trigger: web_backend set to a typo ('bravesearch', 'Brave', 'googe'), a deprecated/renamed backend name after an upgrade, 'none ' with trailing whitespace or newline from a config file, or a value pasted from marketing copy ('web').

Common situations: Hand-editing .env or a YAML config; a harness passing --web-backend with a shell-quoted typo; version drift after a backend was renamed; values with trailing \r from Windows-edited files.

Related errors


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