{"record":{"id":"90e461e861899523","repo":"mvanhorn/last30days-skill","slug":"unsupported-web-backend-backend-r","errorCode":null,"errorMessage":"Unsupported web backend: {backend!r}","messagePattern":"Unsupported web backend: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/grounding.py","lineNumber":274,"sourceCode":"    elif backend == \"exa\":\n        key = config.get(\"EXA_API_KEY\")\n        if not key:\n            raise RuntimeError(\"EXA_API_KEY is required when web_backend='exa'\")\n        items, artifact = exa_search(query, date_range, key)\n    elif backend == \"serper\":\n        key = config.get(\"SERPER_API_KEY\")\n        if not key:\n            raise RuntimeError(\"SERPER_API_KEY is required when web_backend='serper'\")\n        items, artifact = serper_search(query, date_range, key)\n    elif backend == \"parallel\":\n        key = config.get(\"PARALLEL_API_KEY\")\n        if not key:\n            raise RuntimeError(\"PARALLEL_API_KEY is required when web_backend='parallel'\")\n        items, artifact = parallel_search(query, date_range, key)\n    elif backend == \"keyless\":\n        items, artifact = web_search_keyless.keyless_search(query, date_range, config)\n    elif backend != \"none\":\n        raise ValueError(f\"Unsupported web backend: {backend!r}\")\n    else:\n        return [], {}\n    if items and not _reddit_excluded(config):\n        # Reddit enrichment is a best-effort secondary fetch on already-retrieved\n        # web results. Isolate its HTTP failures in a throwaway capture sink so a\n        # reddit.com fetch failure (e.g. a 403 on a datacenter IP) is not\n        # attributed to the web/grounding source itself — which would otherwise\n        # discard the successfully retrieved results and report the source failed.\n        with http.capture_failures():\n            items = _enrich_reddit_items(items)\n    return items, artifact\n\n\ndef _reddit_excluded(config: dict) -> bool:\n    \"\"\"Return True when EXCLUDE_SOURCES contains 'reddit'.\n\n    Respects the same suppression knob the pipeline uses for source gating,\n    so a user who set EXCLUDE_SOURCES=reddit doesn't get Reddit content","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/grounding.py#L256-L292","documentation":"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.","triggerScenarios":"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').","commonSituations":"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.","solutions":["Set web_backend to one of: brave, exa, serper, parallel, keyless, none.","Strip whitespace/CR from the value (check for trailing newline when read from a file).","Remove the setting entirely to use automatic resolution."],"exampleFix":"# before\nWEB_BACKEND=brave-search\n\n# after\nWEB_BACKEND=brave","handlingStrategy":"validation","validationCode":"ALLOWED_BACKENDS = {\"brave\", \"exa\", \"serper\", \"parallel\", \"keyless\", \"none\"}\n\nbackend = str(config.get(\"web_backend\", \"\")).strip().lower()\nassert backend in ALLOWED_BACKENDS, f\"web_backend must be one of {sorted(ALLOWED_BACKENDS)}, got {backend!r}\"","typeGuard":"def is_valid_web_backend(value: object) -> bool:\n    return isinstance(value, str) and value.strip().lower() in {\n        \"brave\", \"exa\", \"serper\", \"parallel\", \"keyless\", \"none\"\n    }","tryCatchPattern":"try:\n    grounding.web_search_with_backend(query, config)\nexcept ValueError as e:\n    if 'Unsupported web backend' in str(e):\n        # normalize the value (strip().lower()) or unset it\n        ...","preventionTips":["Normalize web_backend values with .strip().lower() at load time.","Beware Windows line endings adding \\r to values read from config files.","After upgrading the engine, re-check the allowed backend list — names change between versions."],"tags":["config","web-search","typo","validation"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}