{"record":{"id":"b51740b653542900","repo":"sipeed/picoclaw","slug":"gateway-unavailable-v","errorCode":null,"errorMessage":"Gateway unavailable: %v","messagePattern":"Gateway unavailable: (.+?)","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"web/backend/api/pico.go","lineNumber":54,"sourceCode":"\t\t\ttarget := h.gatewayProxyURL()\n\t\t\tr.SetURL(target)\n\t\t\tr.Out.Header.Del(protocolKey)\n\t\t\tif upstreamProtocol != \"\" {\n\t\t\t\tr.Out.Header.Set(protocolKey, upstreamProtocol)\n\t\t\t}\n\t\t},\n\t\tModifyResponse: func(r *http.Response) error {\n\t\t\tif prot := r.Header.Values(protocolKey); len(prot) > 0 {\n\t\t\t\tr.Header.Del(protocolKey)\n\t\t\t\tif origProtocol != \"\" {\n\t\t\t\t\tr.Header.Set(protocolKey, origProtocol)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\tErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {\n\t\t\tlogger.Errorf(\"Failed to proxy WebSocket: %v\", err)\n\t\t\thttp.Error(w, \"Gateway unavailable: \"+err.Error(), http.StatusBadGateway)\n\t\t},\n\t}\n\treturn wsProxy\n}\n\nfunc (h *Handler) createPicoHTTPProxy(token string) *httputil.ReverseProxy {\n\treturn &httputil.ReverseProxy{\n\t\tRewrite: func(r *httputil.ProxyRequest) {\n\t\t\ttarget := h.gatewayProxyURL()\n\t\t\tr.SetURL(target)\n\t\t\tr.Out.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\t},\n\t\tErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {\n\t\t\tlogger.Errorf(\"Failed to proxy Pico HTTP request: %v\", err)\n\t\t\thttp.Error(w, \"Gateway unavailable: \"+err.Error(), http.StatusBadGateway)\n\t\t},\n\t}\n}","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/pico.go#L36-L72","documentation":"Returned as HTTP 502 by the reverse proxy fronting GET /pico/ws (and the Pico media proxy) when the outbound dial or round-trip to the internal gateway fails. The proxy forwards to http://<gatewayBindHost>:<gatewayPort> (default port 18790, resolved from the current config), and its ErrorHandler logs \"Failed to proxy WebSocket\" and returns this message with the underlying error appended. Because the availability check (gatewayAvailableForProxy) passed moments earlier, this usually means the gateway died mid-request, refused the connection, or the configured host/port is wrong.","triggerScenarios":"GET /pico/ws upgrade request where the gateway process crashed between the availability check and the dial; gateway.port in config changed but the gateway is listening on the old port; gateway bound to a different host than gatewayProbeHost(effectiveGatewayBindHost(cfg)) resolves; connection reset by the upstream.","commonSituations":"Gateway OOM-killed or panicked while the web UI held the WebSocket; config drift where the web backend and gateway disagree on port 18790; gateway still booting and not yet listening; firewall/localhost-binding mismatch when the web backend runs in a different container/network namespace than the gateway.","solutions":["Check the gateway is actually running and listening: curl -sv http://127.0.0.1:18790/ (or GET /api/pico/info, which reports status).","Verify gateway.port and bind host in config.json match where the gateway process listens (ss -ltnp | grep 18790).","Restart the gateway, then reconnect the WebSocket — the 502 is per-request; the client should re-open the socket.","Read the appended %v: \"connection refused\" = wrong port/host or dead process; \"context canceled\" = client went away; EOF/reset = gateway crashed mid-stream.","If backend and gateway run in separate containers, make sure they share the network or the bind host is reachable, not 127.0.0.1 of the wrong namespace."],"exampleFix":"// before (frontend)\nconst ws = new WebSocket(wsUrl);\nws.onclose = () => console.error('closed'); // 502 Gateway unavailable: dial tcp ...:18790: connect: connection refused\n\n// after: reconnect with backoff until the gateway returns\nfunction connect(attempt = 0) {\n  const ws = new WebSocket(wsUrl);\n  ws.onclose = () => setTimeout(() => connect(attempt + 1), Math.min(1000 * 2 ** attempt, 30000));\n  ws.onopen = () => console.log('gateway reconnected');\n}\nconnect();","handlingStrategy":"retry","validationCode":"const info = await fetch('/api/pico/info').then(r => r.json());\nif (!info?.ws_url) throw new Error('Pico channel not configured');\n// optional: probe the gateway before opening the socket\nawait fetch('/api/pico/info', {signal: AbortSignal.timeout(2000)});","typeGuard":null,"tryCatchPattern":"function connectWs(url, attempt = 0) {\n  const ws = new WebSocket(url);\n  ws.onclose = (e) => {\n    if (e.code === 1016 || attempt < 5) { // abnormal closure / 502 during upgrade\n      setTimeout(() => connectWs(url, attempt + 1), Math.min(1000 * 2 ** attempt, 30000));\n    }\n  };\n  ws.onopen = () => attempt = 0; // reset backoff on success\n  return ws;\n}","preventionTips":["Check GET /api/pico/info (gateway status) before opening /pico/ws after a backend start.","Keep gateway.port consistent between the web backend config and the gateway process (default 18790).","Web backend and gateway must share a network namespace or use a reachable bind host.","Always reconnect WebSockets with exponential backoff — the proxy 502 is per-upgrade-request."],"tags":["pico","websocket","http-502","gateway","network","reverse-proxy"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}