grpc/grpc-go · error

failed to do connect handshake, response

Error message

failed to do connect handshake, response: %q

What it means

Fires in doHTTPConnectHandshake (proxy.go:89) when the proxy returns a non-200 status to CONNECT and the response was successfully dumped for diagnostics. gRPC expects HTTP/1.1 200 OK after a CONNECT; any other status means the proxy refused to establish the tunnel, and the full dumped response (status line, headers, body) is included so the reason is visible.

Solutions

  1. Read the dumped response: a 407 means add Proxy-Authorization (set user:pass in the proxy URL); 403 means ask the proxy admin to allowlist the host; 502/503/504 indicate proxy-to-upstream problems.
  2. Provide credentials via the proxy URL: https://user:pass@proxy:3128, or set Proxy-Authorization through grpc.WithContextDialer.
  3. Verify the target address and port are correct and reachable from the proxy's network.
  4. If you don't need the proxy, unset HTTPS_PROXY/HTTP_PROXY for the target host.

Example fix

// before
//   HTTPS_PROXY=http://proxy.corp:3128   // no creds -> 407 Proxy Authentication Required

// after
os.Setenv("HTTPS_PROXY", "http://alice:s3cr3t@proxy.corp:3128")
// or, for hosts that shouldn't use the proxy:
os.Setenv("NO_PROXY", "internal-svc.cluster.local")
Defensive patterns

Strategy: retry

Validate before calling

// Build a proxy URL with credentials if the proxy requires auth.
func proxyWithCreds(host, user, pass string) string {
    u := &url.URL{Scheme: "http", Host: host}
    if user != "" {
        u.User = url.UserPassword(user, pass)
    }
    return u.String()
}

Try / catch

if err := dialViaProxy(...); err != nil {
    s := err.Error()
    if strings.Contains(s, "407") { /* add Proxy-Authorization */ }
    if strings.Contains(s, "403") { /* request allowlist */ }
    if strings.Contains(s, "502") || strings.Contains(s, "503") { /* backoff + retry */ }
}

Prevention

When it happens

Trigger: The proxy replies to CONNECT with a status other than 200. Common: 407 Proxy Authentication Required (missing/bad credentials), 403 Forbidden (host not allowlisted), 502 Bad Gateway / 503 Service Unavailable (proxy couldn't reach upstream), 504 Gateway Timeout. The dumped %q shows headers like Proxy-Authenticate and any error body.

Common situations: Corporate/egress proxy requiring credentials not supplied; proxy ACL blocking the target host/port; proxy can't reach the upstream (network partition); proxy rate-limiting or overloaded; mismatch between the proxy's expectations and the CONNECT target.

Understand the failure class

Related errors


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

Appendix: source

Thrown at internal/transport/proxy.go:89

		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
	}
	return doHTTPConnectHandshake(ctx, conn, grpcUA, opts)

View on GitHub (pinned to 0c51461d27)