denoland/deno · error

upstream /json/list at {host} returned {}

Error message

upstream /json/list at {host} returned {}

What it means

The desktop devtools multiplexer fetches `/json/list` over HTTP from the embedded browser's remote-debugging endpoint to discover the page target's webSocketDebuggerUrl, and bails on any non-success status. The upstream debug server either isn't serving yet, is on a different host/port than expected, or rejected the request.

Source

Thrown at cli/tools/desktop_devtools.rs:722

  let stream = TcpStream::connect(host).await?;
  let io = TokioIo::new(stream);
  let (mut sender, conn) = hyper::client::conn::http1::handshake(io)
    .await
    .map_err(|e| anyhow!("http handshake to {host} failed: {e}"))?;
  tokio::spawn(async move {
    if let Err(err) = conn.await {
      log::trace!("[devtools-mux] upstream conn closed: {err:?}");
    }
  });

  let req = hyper::Request::builder()
    .method(http::Method::GET)
    .uri("/json/list")
    .header(http::header::HOST, host.to_string())
    .body(Empty::<Bytes>::new())?;
  let resp = sender.send_request(req).await?;
  if !resp.status().is_success() {
    bail!("upstream /json/list at {host} returned {}", resp.status());
  }
  let body = resp.collect().await?.to_bytes();
  let value: Value = serde_json::from_slice(&body)
    .with_context(|| format!("upstream /json/list at {host} not JSON"))?;

  let ws_url = value
    .as_array()
    .and_then(|arr| {
      arr.iter().find_map(|v| {
        // Skip targets that are our own DevTools frontend window — when
        // openDevtools() creates a CEF window pointed at inspector.html,
        // CEF registers it as a debuggable target. Connecting to it
        // instead of the real app window would show "DevTools for
        // DevTools".
        let url = v.get("url").and_then(|u| u.as_str()).unwrap_or("");
        if url.contains("/devtools/") || url.contains("devtools://") {
          return None;
        }

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Retry the devtools connection after the app window is fully up — the most common cause is a startup race.
  2. Curl the endpoint yourself: `curl http://<host>:<port>/json/list` — if that fails, the address is wrong or the server isn't listening.
  3. Kill stale instances of the app/browser holding the debugging port, then relaunch.
  4. Verify the remote-debugging port configuration matches what the mux is given, and prefer 127.0.0.1.
Defensive patterns

Strategy: retry

Validate before calling

# Wait until the remote-debugging endpoint answers before opening devtools
for i in $(seq 1 20); do
  curl -fsS "http://127.0.0.1:${PORT}/json/list" >/dev/null && break
  sleep 0.5
done

Try / catch

// Client-side pattern around the mux/devtools attach
async function attachWithRetry(url: string, attempts = 5): Promise<Response> {
  for (let i = 0; i < attempts; i++) {
    try {
      const r = await fetch(`${url}/json/list`);
      if (r.ok) return r;
    } catch { /* not ready */ }
    await new Promise((r) => setTimeout(r, 500 * (i + 1)));
  }
  throw new Error(`devtools endpoint at ${url} not responding`);
}

Prevention

When it happens

Trigger: The mux connects before the CEF/browser debug endpoint is ready (startup race), the configured host/port doesn't match the actual remote-debugging address, another process occupies the port, or the browser process died between connection and the HTTP request. Any 4xx/5xx status lands here.

Common situations: Opening devtools immediately at app launch; a stale port left over from a previous crashed run; a second debuggable instance already holding the endpoint; proxies/firewalls interfering with localhost HTTP.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/ccd028b545f5e816. Report an issue: GitHub.