When the client speaks binary and the server replies in text
You write a Go client to fetch data from a local development server or a legacy internal API. You run the program. Instead of a response, you get a panic or an error that reads http: server gave HTTP response to HTTPS client. You didn't ask for HTTPS. You didn't even ask for HTTP/2. You just wanted to make a request.
The error happens because Go's HTTP client is eager. It tries to use HTTP/2 by default. HTTP/2 is a binary protocol. If the server is running plain HTTP/1.1, it replies with text. The client's HTTP/2 parser sees text where it expects binary frames and gives up. The error message mentions HTTPS because HTTP/2 is tightly coupled with TLS in the standard library, but the root cause is a protocol mismatch, not encryption.
How Go's HTTP client chooses a protocol
Go's net/http package includes a transport that automatically negotiates HTTP/2. When you use http.Get or http.DefaultClient, you get this behavior for free. The client checks if the server supports HTTP/2. If the URL starts with https, the client uses ALPN (Application-Layer Protocol Negotiation) during the TLS handshake to ask the server if it speaks HTTP/2. If the server says yes, the client switches to HTTP/2 frames.
If the URL starts with http, the client usually sticks to HTTP/1.1. However, if your code imports the golang.org/x/net/http2 package and calls http2.ConfigureTransport, or if you are using a custom setup that forces HTTP/2, the client may attempt to speak HTTP/2 over a plain TCP connection. This is called h2c. If the server does not support h2c, it responds with an HTTP/1.1 text response. The client is still expecting HTTP/2 binary data. The parser fails. You get the error.
The error message http: server gave HTTP response to HTTPS client can be confusing. It implies the client thought it was talking to an HTTPS server. In reality, the HTTP/2 transport layer is reporting that it received an HTTP response when it expected HTTP/2 frames. The transport treats the mismatch as a fatal protocol error.
Minimal example that triggers the error
This example shows how importing http2 and configuring the transport can force HTTP/2 on a connection that only supports HTTP/1.1.
package main
import (
"fmt"
"net/http"
"golang.org/x/net/http2"
)
func main() {
// Create a transport and enable HTTP/2 integration.
// This allows HTTP/2 over TLS and potentially cleartext depending on config.
transport := &http.Transport{}
http2.ConfigureTransport(transport)
client := &http.Client{
Transport: transport,
}
// Request a plain HTTP endpoint that only supports HTTP/1.1.
// Many local dev servers or legacy APIs fall into this category.
resp, err := client.Get("http://localhost:8080/data")
if err != nil {
// If the server replies with HTTP/1.1 text while the client expects HTTP/2,
// you see: http: server gave HTTP response to HTTPS client
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
The http2.ConfigureTransport call wraps the transport with HTTP/2 logic. If the target server does not support HTTP/2, the negotiation fails or the client sends an HTTP/2 preface that the server cannot understand. The server replies with HTTP/1.1. The client crashes the request with the protocol error.
Walkthrough of the failure
When the request starts, the client opens a TCP connection. If HTTP/2 is enabled, the client may send an HTTP/2 connection preface. This is a binary sequence that announces the client wants to speak HTTP/2. A server that only understands HTTP/1.1 sees this binary data as garbage. It might close the connection, or it might ignore the preface and wait for an HTTP/1.1 request line.
If the server ignores the preface and waits, the client eventually times out or sends a request. If the client sends an HTTP/2 HEADERS frame, the server sees binary data and likely closes the connection. If the client falls back to sending HTTP/1.1 text but the transport is still in HTTP/2 mode, the server replies with HTTP/1.1 200 OK. The transport reads this text. It expects a frame header with length, type, and flags. It finds H instead. The parser returns the error.
The error is not a compiler error. It is a runtime error returned by the http package. The compiler will not catch this. You only see it when the code runs against a server that does not match the client's protocol expectations.
Realistic fix: configure the transport in code
Setting GODEBUG=http2client=0 disables HTTP/2 for the entire process. This works, but it is a sledgehammer. It affects every HTTP request in your program. It is also a debug flag. The Go team reserves the right to change GODEBUG behavior in future releases. You should not rely on it in production code.
The robust fix is to configure the http.Client to use a transport that does not integrate HTTP/2. This keeps HTTP/2 available for other parts of your program if needed, and it makes the behavior explicit in your code.
package main
import (
"fmt"
"net/http"
)
// FetchLegacyData retrieves data from an HTTP/1.1-only server.
// It uses a custom transport to disable HTTP/2 negotiation.
func FetchLegacyData(url string) ([]byte, error) {
// Create a transport without http2 integration.
// This forces the client to use HTTP/1.1 for all requests.
transport := &http.Transport{}
client := &http.Client{
Transport: transport,
}
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Read the body.
buf := make([]byte, 4096)
n, err := resp.Body.Read(buf)
if err != nil && err.Error() != "EOF" {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf[:n], nil
}
func main() {
data, err := FetchLegacyData("http://localhost:8080/data")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Got %d bytes\n", len(data))
}
This code creates a fresh http.Transport. Since it does not call http2.ConfigureTransport, the transport only speaks HTTP/1.1. The client uses this transport. Requests go out as HTTP/1.1 text. The server replies with HTTP/1.1 text. The client parses the text successfully. No error.
You can also use http.DefaultTransport if you do not need custom settings, but DefaultTransport may have HTTP/2 integration enabled depending on your Go version and imports. Creating a new &http.Transport{} is the safest way to ensure HTTP/1.1 only.
Pitfalls and common mistakes
Using GODEBUG in production is a bad idea. The flag is designed for debugging. It can hide real issues. If you disable HTTP/2 globally, you lose the performance benefits of multiplexing and header compression for all requests. You might also break dependencies that expect HTTP/2 to work.
Importing http2 can have side effects. The golang.org/x/net/http2 package registers hooks that enable HTTP/2 support in net/http. If you import it, even just for testing, your client may try HTTP/2 on connections where it is not supported. Always check your imports. If you do not need HTTP/2, do not import http2.
The compiler rejects code that references undefined packages. If you try to use http2 without importing it, you get undefined: http2. If you import a package and do not use it, you get imported and not used. Go's strict import rules help you avoid accidental side effects. Keep your imports clean.
Another pitfall is reusing clients incorrectly. Creating a new http.Client for every request is slow. The client manages connection pooling. Reuse the client across requests. Configure the transport once, then use the client for all requests to the same type of server.
Goroutine leaks can happen if you start a goroutine to make a request and forget to read the response body. The connection stays open. The pool fills up. Subsequent requests block. Always call resp.Body.Close(). Use defer if the scope is simple.
Decision: when to use which approach
Use GODEBUG=http2client=0 when you are debugging locally or in CI and need to quickly isolate whether HTTP/2 is causing a failure. Use a custom http.Transport without HTTP/2 integration when you need a stable, code-controlled client that talks to HTTP/1.1-only servers in production. Use http.DefaultClient when you want automatic protocol negotiation and the server supports modern standards. Use http2.ConfigureTransport when you need HTTP/2 over cleartext for local development or internal networks where TLS is not available.
GODEBUG is for debugging, not deployment. Configure the transport, don't fight the client.