grpc/grpc-go · error

failed to do connect handshake, status code

Error message

failed to do connect handshake, status code: %s

What it means

Fires in doHTTPConnectHandshake (proxy.go:87) on a double-failure path: the proxy returned a non-200 status to CONNECT, and additionally httputil.DumpResponse failed to serialize the response body for diagnostics. Because the dump failed, gRPC falls back to reporting only the status code (resp.Status) rather than the full response.

Solutions

  1. Treat this like a proxy rejection: the status code in the message tells you why (407 = auth, 403 = forbidden, 502/503 = upstream unavailable).
  2. Supply correct proxy credentials (Proxy-Authorization / proxy URL user:pass) for 407 responses.
  3. Confirm the target host is on the proxy's allowlist for CONNECT.
  4. Reproduce with curl -v --proxy <url> https://<target> to see the full proxy exchange and any body.

Example fix

// before: proxy requires auth, none supplied -> 407, dump fails
//   HTTPS_PROXY=http://proxy:3128

// after: include credentials in the proxy URL
os.Setenv("HTTPS_PROXY", "http://user:pass@proxy:3128")
Defensive patterns

Strategy: retry

Try / catch

if err := dialViaProxy(...); err != nil {
    if strings.Contains(err.Error(), "connect handshake, status code") {
        // proxy refused CONNECT; check creds/allowlist, then retry or fail over
    }
}

Prevention

When it happens

Trigger: The proxy rejects CONNECT (status != 200) and the response body cannot be dumped: e.g. the body read errors mid-dump (connection reset), the body is malformed, or DumpResponse encounters an unexpected state. The message shows the HTTP status text, e.g. '502 Bad Gateway' or '407 Proxy Authentication Required'.

Common situations: A proxy returning an error page whose body stream then breaks; an intermediary that resets the connection right after sending a non-200 status; rare compared to error 339 (which fires when the dump succeeds). The root cause is still a rejected CONNECT; the dump failure just reduces diagnostics.

Understand the failure class

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/64443bd685d78381. Report an issue: GitHub.

Appendix: source

Thrown at internal/transport/proxy.go:87

	if user := opts.User; user != nil {
		u := user.Username()
		p, _ := user.Password()
		req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))
	}
	if err := sendHTTPRequest(ctx, req, conn); err != nil {
		return nil, fmt.Errorf("failed to write the HTTP request: %v", err)
	}

	r := bufio.NewReader(conn)
	resp, err := http.ReadResponse(r, req)
	if err != nil {
		return nil, fmt.Errorf("reading server HTTP response: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		dump, err := httputil.DumpResponse(resp, true)
		if err != nil {
			return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status)
		}
		return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump)
	}
	// The buffer could contain extra bytes from the target server, so we can't
	// discard it. However, in many cases where the server waits for the client
	// to send the first message (e.g. when TLS is being used), the buffer will
	// be empty, so we can avoid the overhead of reading through this buffer.
	if r.Buffered() != 0 {
		return &bufConn{Conn: conn, r: r}, nil
	}
	return conn, nil
}

// proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake.
func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) {
	conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr)
	if err != nil {
		return nil, err

View on GitHub (pinned to 0c51461d27)