{"record":{"id":"1a4f3dd733f83cd8","repo":"koala73/worldmonitor","slug":"dns-recordtype-lookup-failed-http-response-s","errorCode":null,"errorMessage":"DNS ${recordType} lookup failed: HTTP ${response.status}","messagePattern":"DNS (.+?) lookup failed: HTTP (.+?)","errorType":"exception","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":196,"sourceCode":"  if (isBlockedNotificationResolvedAddress(hostname)) {\n    return 'Webhook URL must not point to a private/local address';\n  }\n\n  return null;\n}\n\nasync function resolveDnsJson(hostname: string, recordType: 'A' | 'AAAA'): Promise<string[]> {\n  const url = new URL(DNS_JSON_ENDPOINT);\n  url.searchParams.set('name', hostname);\n  url.searchParams.set('type', recordType);\n  const response = await fetch(url, {\n    headers: {\n      Accept: 'application/dns-json',\n      'User-Agent': 'WorldMonitor-Notification-Webhooks/1.0',\n    },\n    signal: AbortSignal.timeout(DNS_RESOLUTION_TIMEOUT_MS),\n  });\n  if (!response.ok) throw new Error(`DNS ${recordType} lookup failed: HTTP ${response.status}`);\n  const data = await response.json() as { Status?: number; Answer?: Array<{ type?: number; data?: string }> };\n  if (data.Status !== 0) throw new Error(`DNS ${recordType} lookup failed: status ${data.Status}`);\n  const expectedType = recordType === 'A' ? 1 : 28;\n  return (data.Answer ?? [])\n    .filter(answer => answer.type === expectedType && typeof answer.data === 'string')\n    .map(answer => answer.data!);\n}\n\nasync function defaultResolveHostname(hostname: string): Promise<string[]> {\n  const records = await Promise.all([\n    resolveDnsJson(hostname, 'A'),\n    resolveDnsJson(hostname, 'AAAA'),\n  ]);\n  return records.flat();\n}\n\n/**\n * Fail fast at registration when the webhook hostname currently resolves to a","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/api/_notification-webhook-ssrf.ts#L178-L214","documentation":"Thrown by autoVisualize() in the MCP data panel when the widget-agent response stream emits a `data: {\"type\":\"error\"}` event instead of completing. The panel POSTs a 3000-char JSON preview of the MCP tool output to widgetAgentUrl() with {prompt, mode:'create', tier:'pro'} and parses newline-delimited `data:` lines; an error event means the server-side widget generation pipeline reported failure mid-stream. The thrown message is the upstream event.message, falling back to the localized 'mcp.visualizationFailed' string when the event carries none. The local catch (src/components/McpDataPanel.ts:259) clears the cached widget and renders the message via showError().","triggerScenarios":"POST to widgetAgentUrl() returns HTTP 200 with a body, but the stream sends {\"type\":\"error\", message}: upstream LLM/provider failure, rate limiting, prompt safety rejection, tool data the generator cannot chart, or an agent-internal error. Aborts from the 120s timeoutController are filtered (AbortError is ignored at line 260), so only genuine error events surface here.","commonSituations":"No widget/pro/tester keys configured, so getWidgetAgentKey()/getProWidgetKey()/getBrowserTesterKey() return empty and the request goes out without X-Widget-Key/X-Pro-Key/X-WorldMonitor-Key headers; very large or malformed MCP tool JSON; widget-agent deployment outage or upstream model incident; proxies buffering or cutting the SSE stream mid-generation.","solutions":["Read the panel error UI: the thrown Error forwards the upstream event.message, which names the real cause","Retry the visualization once; transient upstream provider failures usually clear","Verify widget credentials so the POST carries X-Widget-Key / X-Pro-Key / X-WorldMonitor-Key headers","Inspect the widgetAgentUrl() POST in the browser Network tab to see the raw error event and confirm the stream shape","Check widget-agent server logs for the generation failure; if the tool data is huge, test with a smaller payload"],"exampleFix":"// before: any stream hiccup shows a generic failure\n} else if (event.type === 'error') {\n  throw new Error(String(event.message ?? t('mcp.visualizationFailed')));\n}\n\n// after: only fire when a widget/pro/tester key exists, keeping the upstream message\nconst hasAgentCreds = getWidgetAgentKey() || getProWidgetKey() || getBrowserTesterKey();\nif (!hasAgentCreds) {\n  this.showError(t('mcp.visualizationUnavailable'));\n  return;\n}\n// ... in the stream loop:\n} else if (event.type === 'error') {\n  throw new Error(String(event.message ?? t('mcp.visualizationFailed')));\n}","handlingStrategy":"try-catch","validationCode":"const hasAgentCreds = !!(getWidgetAgentKey() || getProWidgetKey() || getBrowserTesterKey());\nif (!hasAgentCreds) {\n  panel.showError(t('mcp.visualizationUnavailable'));\n  return;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await autoVisualize(jsonData, hash);\n} catch (err) {\n  if ((err as { name?: string }).name === 'AbortError') return; // 120s timeout or destroy\n  const msg = err instanceof Error && err.message ? err.message : t('mcp.visualizationFailed');\n  panel.showError(msg);\n  panel.cachedWidgetHtml = null;\n}","preventionTips":["Configure widget/pro or tester keys so requests carry X-Widget-Key/X-Pro-Key/X-WorldMonitor-Key","Keep the tool JSON preview small; the panel already slices to 3000 chars, avoid re-sending larger blobs","Mirror the panel's own pattern: filter AbortError before rendering errors","Watch the widget-agent health endpoint when visualization errors cluster"],"tags":["mcp","visualization","sse-stream","widget-agent","streaming"],"backgroundTag":"upstream-service-error","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}