sipeed/picoclaw · error

Gateway unavailable: %v

Error message

Gateway unavailable: %v

What it means

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.

Source

Thrown at web/backend/api/pico.go:54

			target := h.gatewayProxyURL()
			r.SetURL(target)
			r.Out.Header.Del(protocolKey)
			if upstreamProtocol != "" {
				r.Out.Header.Set(protocolKey, upstreamProtocol)
			}
		},
		ModifyResponse: func(r *http.Response) error {
			if prot := r.Header.Values(protocolKey); len(prot) > 0 {
				r.Header.Del(protocolKey)
				if origProtocol != "" {
					r.Header.Set(protocolKey, origProtocol)
				}
			}
			return nil
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			logger.Errorf("Failed to proxy WebSocket: %v", err)
			http.Error(w, "Gateway unavailable: "+err.Error(), http.StatusBadGateway)
		},
	}
	return wsProxy
}

func (h *Handler) createPicoHTTPProxy(token string) *httputil.ReverseProxy {
	return &httputil.ReverseProxy{
		Rewrite: func(r *httputil.ProxyRequest) {
			target := h.gatewayProxyURL()
			r.SetURL(target)
			r.Out.Header.Set("Authorization", "Bearer "+token)
		},
		ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
			logger.Errorf("Failed to proxy Pico HTTP request: %v", err)
			http.Error(w, "Gateway unavailable: "+err.Error(), http.StatusBadGateway)
		},
	}
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the gateway is actually running and listening: curl -sv http://127.0.0.1:18790/ (or GET /api/pico/info, which reports status).
  2. Verify gateway.port and bind host in config.json match where the gateway process listens (ss -ltnp | grep 18790).
  3. Restart the gateway, then reconnect the WebSocket — the 502 is per-request; the client should re-open the socket.
  4. Read the appended %v: "connection refused" = wrong port/host or dead process; "context canceled" = client went away; EOF/reset = gateway crashed mid-stream.
  5. 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.

Example fix

// before (frontend)
const ws = new WebSocket(wsUrl);
ws.onclose = () => console.error('closed'); // 502 Gateway unavailable: dial tcp ...:18790: connect: connection refused

// after: reconnect with backoff until the gateway returns
function connect(attempt = 0) {
  const ws = new WebSocket(wsUrl);
  ws.onclose = () => setTimeout(() => connect(attempt + 1), Math.min(1000 * 2 ** attempt, 30000));
  ws.onopen = () => console.log('gateway reconnected');
}
connect();
Defensive patterns

Strategy: retry

Validate before calling

const info = await fetch('/api/pico/info').then(r => r.json());
if (!info?.ws_url) throw new Error('Pico channel not configured');
// optional: probe the gateway before opening the socket
await fetch('/api/pico/info', {signal: AbortSignal.timeout(2000)});

Try / catch

function connectWs(url, attempt = 0) {
  const ws = new WebSocket(url);
  ws.onclose = (e) => {
    if (e.code === 1016 || attempt < 5) { // abnormal closure / 502 during upgrade
      setTimeout(() => connectWs(url, attempt + 1), Math.min(1000 * 2 ** attempt, 30000));
    }
  };
  ws.onopen = () => attempt = 0; // reset backoff on success
  return ws;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/b51740b653542900. Report an issue: GitHub.