Processing compressed HTTP responses in Go is essential for efficient web scraping and API consumption. Compression can reduce response sizes by 60-80%, significantly improving performance and reducing bandwidth costs.
Understanding HTTP Compression
HTTP compression uses algorithms like gzip, deflate, and br (Brotli) to compress response bodies. The client indicates support via the Accept-Encoding header, and the server responds with the Content-Encoding header specifying the compression used.
Automatic Compression Handling
Go's http package automatically handles gzip compression when using http.Get() or http.DefaultClient:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// Automatic gzip decompression
resp, err := http.Get("https://httpbin.org/gzip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("Response: %s\n", string(body))
fmt.Printf("Content-Encoding: %s\n", resp.Header.Get("Content-Encoding"))
}
Manual Compression Handling
For more control or to handle additional encodings, implement manual decompression:
package main
import (
"compress/flate"
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
)
func main() {
url := "https://httpbin.org/deflate"
// Create request with compression support
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
// Request multiple compression formats
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Get decompressed reader
reader, err := getDecompressedReader(resp)
if err != nil {
panic(err)
}
defer reader.Close()
body, err := io.ReadAll(reader)
if err != nil {
panic(err)
}
fmt.Printf("Response: %s\n", string(body))
fmt.Printf("Content-Encoding: %s\n", resp.Header.Get("Content-Encoding"))
}
func getDecompressedReader(resp *http.Response) (io.ReadCloser, error) {
encoding := resp.Header.Get("Content-Encoding")
switch {
case strings.Contains(encoding, "gzip"):
return gzip.NewReader(resp.Body)
case strings.Contains(encoding, "deflate"):
return flate.NewReader(resp.Body), nil
default:
return resp.Body, nil
}
}
Complete Example with Error Handling
Here's a production-ready example with comprehensive error handling:
package main
import (
"compress/flate"
"compress/gzip"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type CompressionClient struct {
client *http.Client
}
func NewCompressionClient() *CompressionClient {
return &CompressionClient{
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *CompressionClient) Get(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
// Accept multiple compression formats
req.Header.Set("Accept-Encoding", "gzip, deflate")
req.Header.Set("User-Agent", "Go-HTTP-Client/1.1")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("performing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
reader, err := c.getReader(resp)
if err != nil {
return nil, fmt.Errorf("creating decompressed reader: %w", err)
}
defer reader.Close()
body, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
return body, nil
}
func (c *CompressionClient) getReader(resp *http.Response) (io.ReadCloser, error) {
encoding := strings.ToLower(resp.Header.Get("Content-Encoding"))
switch {
case strings.Contains(encoding, "gzip"):
return gzip.NewReader(resp.Body)
case strings.Contains(encoding, "deflate"):
return flate.NewReader(resp.Body), nil
case encoding == "":
// No compression
return resp.Body, nil
default:
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
}
}
func main() {
client := NewCompressionClient()
urls := []string{
"https://httpbin.org/gzip",
"https://httpbin.org/deflate",
"https://httpbin.org/json",
}
for _, url := range urls {
fmt.Printf("Fetching: %s\n", url)
body, err := client.Get(url)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("Response size: %d bytes\n", len(body))
fmt.Printf("First 100 chars: %.100s...\n\n", string(body))
}
}
Key Best Practices
- Always defer close readers: Both response bodies and decompression readers must be closed
- Check Content-Encoding: Never assume compression is used; always check the header
- Handle multiple encodings: Support gzip, deflate, and optionally Brotli
- Use case-insensitive comparisons: Header values may vary in case
- Set timeouts: Prevent hanging connections when dealing with compressed streams
- Error handling: Wrap errors with context for better debugging
Performance Considerations
- Automatic vs Manual: Use automatic handling for simple cases, manual for custom requirements
- Memory usage: Compressed responses use less bandwidth but more CPU and memory during decompression
- Streaming: For large responses, consider streaming decompression instead of loading everything into memory
Common Gotchas
- Double decompression: Don't manually decompress when using
http.Get()- it's automatic for gzip - Empty responses: Some servers send compressed empty responses that can cause reader errors
- Partial content: Range requests with compression can be tricky
- Transfer-Encoding vs Content-Encoding: These are different - Transfer-Encoding is hop-by-hop
This approach ensures your Go applications can efficiently handle compressed HTTP responses while maintaining robust error handling and performance.