{"record":{"id":"6036e1f738e621a9","repo":"xai-org/grok-build","slug":"failed-to-build-web-search-tool-e","errorCode":null,"errorMessage":"Failed to build web search tool: {e}","messagePattern":"Failed to build web search tool: (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-tools/src/implementations/web_search/client.rs","lineNumber":155,"sourceCode":"    /// `excluded_domains` is injected into the tool's `filters` after\n    /// serialization (the backend Responses API accepts it). The request always\n    /// carries exactly one tool (`web_search`) at index 0.\n    fn build_request_json(\n        &self,\n        query: &str,\n        allowed_domains: Option<Vec<String>>,\n        excluded_domains: Option<Vec<String>>,\n    ) -> Result<serde_json::Value, xai_tool_runtime::ToolError> {\n        let err = |msg: String| {\n            xai_tool_runtime::ToolError::execution(\n                xai_tool_protocol::ToolId::new(\"web_search\").expect(\"valid\"),\n                msg,\n            )\n        };\n        let web_search = rs::WebSearchToolArgs::default()\n            .filters(rs::WebSearchToolFilters { allowed_domains })\n            .build()\n            .map_err(|e| err(format!(\"Failed to build web search tool: {e}\")))?;\n        let request = rs::CreateResponseArgs::default()\n            .model(self.model.clone())\n            .input(query.to_string())\n            .tools(vec![rs::Tool::WebSearch(web_search)])\n            .store(false)\n            .temperature(0.1)\n            .top_p(0.95)\n            .max_output_tokens(8192u32)\n            .build()\n            .map_err(|e| err(format!(\"Failed to build request: {e}\")))?;\n        let mut body = serde_json::to_value(&request)\n            .map_err(|e| err(format!(\"Failed to serialize request: {e}\")))?;\n        if let Some(excluded) = excluded_domains.filter(|d| !d.is_empty()) {\n            let tool = body\n                .get_mut(\"tools\")\n                .and_then(|t| t.as_array_mut())\n                .and_then(|arr| arr.first_mut())\n                .and_then(|t| t.as_object_mut());","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-tools/src/implementations/web_search/client.rs#L137-L173","documentation":"The web search client constructs an rs::WebSearchToolArgs with allowed_domains filters and calls .build(); when the underlying builder rejects the arguments it maps the error to \"Failed to build web search tool: {e}\". This happens before any network I/O, so the cause is invalid tool configuration — typically malformed or invalid domain entries in allowed_domains.","triggerScenarios":"Calling search or search_with_titles with allowed_domains containing entries the builder rejects (invalid hostnames, protocols like \"https://\" in a domain, empty strings that pass the filter, or builder-required fields missing due to a version change in the rs crate).","commonSituations":"Passing full URLs instead of bare domains; upgrading the responses/web-search crate where WebSearchToolArgs validation became stricter; config files supplying wildcard or malformed domain patterns.","solutions":["Inspect the wrapped {e} message; it names the exact builder validation that failed.","Normalize allowed_domains to bare hostnames (\"example.com\"), stripping scheme/path/wildcards.","Check the installed rs crate version's WebSearchToolArgs/WebSearchToolFilters docs for changed required fields.","Test with allowed_domains = None/empty to confirm the tool builds without filters, then re-add entries one at a time."],"exampleFix":"// before\nfilters: WebSearchToolFilters { allowed_domains: Some(vec![\"https://docs.example.com\"] ) }\n// after\nfilters: WebSearchToolFilters { allowed_domains: Some(vec![\"docs.example.com\"]) }","handlingStrategy":"validation","validationCode":"function normalizeDomains(domains) {\n  if (!domains) return undefined;\n  const bare = domains.map(d => String(d)\n    .replace(/^https?:\\/\\//, '')\n    .replace(/\\/.*$/, '')\n    .replace(/^\\*\\./, ''));\n  return bare.every(d => /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(d)) ? bare : undefined;\n}","typeGuard":"function isValidDomainList(d: unknown): d is string[] {\n  return Array.isArray(d) && d.length > 0 &&\n    d.every(x => typeof x === 'string' && /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(x));\n}","tryCatchPattern":"try {\n  result = await client.search(query, allowedDomains);\n} catch (e) {\n  if (String(e).startsWith('Failed to build web search tool')) {\n    result = await client.search(query, undefined); // retry without filters\n  } else { throw e; }\n}","preventionTips":["Always pass bare hostnames (no scheme, path, or wildcard) in allowed_domains.","Validate domain lists at config-load time, not at request time.","Pin and review the rs crate version; re-run build tests after upgrades."],"tags":["web-search","request-building","validation"],"backgroundTag":"request-build-validation-failed","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}