tailscale/tailscale · error

hijacking client connection: %w

Error message

hijacking client connection: %w

What it means

After writing the 101 and calling ResponseWriter.Hijack() to take over the raw connection for the Noise handshake, Hijack returned an error. The earlier branch already handles ResponseWriters that don't implement http.Hijacker at all (e.g. http2); this error means Hijack exists but failed — connection already hijacked/closed, middleware wrappers, or a conn in a bad state.

Source

Thrown at control/controlhttp/controlhttpserver/controlhttpserver.go:84

	init, err := base64.StdEncoding.DecodeString(initB64)
	if err != nil {
		http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest)
		return nil, fmt.Errorf("decoding base64 handshake header: %v", err)
	}

	hijacker, ok := w.(http.Hijacker)
	if !ok {
		http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
		return nil, errors.New("can't hijack client connection")
	}

	w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
	w.Header().Set("Connection", "upgrade")
	w.WriteHeader(http.StatusSwitchingProtocols)

	conn, brw, err := hijacker.Hijack()
	if err != nil {
		return nil, fmt.Errorf("hijacking client connection: %w", err)
	}

	defer func() {
		if retErr != nil {
			conn.Close()
		}
	}()

	if err := brw.Flush(); err != nil {
		return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
	}
	conn = netutil.NewDrainBufConn(conn, brw.Reader)

	cwc := newWriteCorkingConn(conn)

	nc, err := controlbase.Server(ctx, cwc, private, init)
	if err != nil {
		return nil, fmt.Errorf("noise handshake failed: %w", err)

View on GitHub (pinned to 0fd2f14deb)

Solutions

  1. Serve the control endpoint on a plain HTTP/1.1 listener without h2c
  2. Remove or reorder middleware that wraps or hijacks the ResponseWriter before acceptHTTP
  3. Ensure the handler runs exactly once per connection

Example fix

// before
srv := &http.Server{Handler: h2cHandler(ctrlHandler), Addr: ":443"} // http2 conn -> Hijack fails

// after
srv := &http.Server{Handler: ctrlHandler, Addr: ":443"} // HTTP/1.1 only
Defensive patterns

Strategy: validation

Validate before calling

// Reject anything that is not HTTP/1.x before it reaches acceptHTTP
if r.ProtoMajor != 1 {
    http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
    return
}

Prevention

When it happens

Trigger: Serving the control endpoint over h2c/HTTP/2 where hijack semantics break; middleware that already consumed or hijacked the conn; double handling of the same request; ResponseWriter wrapped by logging/tracing middleware that breaks the Hijacker assertion chain.

Common situations: golang.org/x/net/http2 h2c servers, custom middleware stacks, tests with fake ResponseWriters, grpc-gateway style wrappers.

Related errors


AI-assisted analysis of tailscale/tailscale@0fd2f14deb (2026-08-18). Data as JSON: /api/errors/d35c14df1378b431. Report an issue: GitHub.