A high-performance, RFC 7540 compliant HTTP/2 client implementation written in Go.
Ah shit, here we go again! The classic question everyone asks when they see yet another library!
The Real Story Behind This Lib
So here's the deal... I was working on a telecommunication system (spoiler: the kind where if your service goes down for 5 minutes, your boss calls asking "what the hell happened???"). In this system, I used HTTP/2 for communication between modules - sounds fancy, right? Until...
BOOM! 💥 "Connection reset by peer" appears out of nowhere!
BOOM! 💥 "Compression error" strikes again!
BOOM! 💥 A bunch of other weird errors that the golang.org/x/net/http2 docs just say "this shouldn't happen" 🤡
At that moment I was like: "WTF is happening here?!"
The Pain Point: You know that feeling when you're working on a production system and you hit a bug in a third-party lib? It's like driving on a highway and suddenly your steering wheel locks up! You can't fix it because it's not your code, and you can only sit there and... pray! 🙏
The "Aha!" Moment: After the nth time getting called at 3AM because of HTTP/2 connection issues, I decided: "Screw this, I'm writing my own damn lib!"
Why This Lib Exists:
- Full Control: When there's a bug, I can fix it immediately instead of waiting for maintainers to merge PRs
- Focused on Real-World Usage: No fancy features that nobody actually uses
- Performance-First: Optimized for high-throughput systems (like telecom)
- Debuggable: Extensive logging and debugging tools built-in
- Battle-Tested: Already running in harsh production environments
TL;DR: I'm not someone who likes to reinvent the wheel, but sometimes you need a wheel that you can actually trust and control. Especially when that wheel decides whether your system lives or dies!
Hope it can help you avoid those 3AM debugging sessions!
This library provides a full-featured HTTP/2 client that strictly follows RFC 7540 specifications. It offers multiple interfaces ranging from drop-in replacement for net/http to low-level HTTP/2 protocol access.
✅ RFC 7540 Compliant: Full HTTP/2 protocol implementation
✅ Multiple Client Interfaces: Standard http.Client compatibility + raw HTTP/2 access
✅ Stream Multiplexing: Concurrent request handling on single connection
✅ Header Compression: HPACK implementation (RFC 7541)
✅ Flow Control: Per-stream and connection-level flow control
✅ Connection Pooling: Efficient connection management
✅ High Performance: Optimized for throughput and low latency
go get github.com/chronnie/http2package main
import (
"fmt"
"io"
"github.com/chronnie/http2"
)
func main() {
// Create HTTP/2 client
client, err := http2.NewHTTP2Client("example.com:443")
if err != nil {
panic(err)
}
defer client.Close()
// Make GET request
resp, err := client.Get("https://example.com/api/data")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Response: %s\n", string(body))
}The library provides three different client interfaces to suit various use cases:
Drop-in replacement for net/http.Client with HTTP/2 optimizations:
// Create client with standard interface
client, err := http2.NewHTTP2Client("server:port")
// Works exactly like http.Client
resp, err := client.Get("https://example.com/api")
resp, err := client.Post("https://example.com/api", "application/json", body)RFC 7540 Reference: Section 8.1 - HTTP Request/Response Exchange
Direct HTTP/2 protocol access for maximum performance:
// Create raw client
rawClient, err := http2.NewClient("server:port")
// Use convenience methods
resp, err := rawClient.DoGet("https://example.com/api")
resp, err := rawClient.DoPost("https://example.com/api", jsonData)For maximum flexibility with existing http.Request objects:
// Create request
req, _ := http.NewRequest("GET", "https://example.com/api", nil)
// Execute with HTTP/2
resp, err := client.Do(req)// Create new client connection
client, err := http2.NewClient(address string) (*Client, error)
// Close client and cleanup resources
client.Close() errorRFC 7540 Reference: Section 3 - Starting HTTP/2
// Standard HTTP methods
GET(path, authority string) (*Response, error)
POST(path, authority string, body []byte) (*Response, error)
PUT(path, authority string, body []byte) (*Response, error)
DELETE(path, authority string) (*Response, error)
// With custom headers
GETWithHeaders(path, authority string, headers map[string]string) (*Response, error)// Create new stream
CreateStream(headers map[string]string, body []byte, endStream bool) (*StreamResponse, error)
// Stream state management follows RFC 7540 Section 5.1RFC 7540 Reference: Section 5 - Streams and Multiplexing
The implementation supports all HTTP/2 frame types as defined in RFC 7540:
| Frame Type | Code | Purpose | RFC Section |
|---|---|---|---|
| DATA | 0x0 | Application data | Section 6.1 |
| HEADERS | 0x1 | Header fields | Section 6.2 |
| PRIORITY | 0x2 | Stream priority | Section 6.3 |
| RST_STREAM | 0x3 | Stream termination | Section 6.4 |
| SETTINGS | 0x4 | Connection config | Section 6.5 |
| PUSH_PROMISE | 0x5 | Server push | Section 6.6 |
| PING | 0x6 | Connection test | Section 6.7 |
| GOAWAY | 0x7 | Connection shutdown | Section 6.8 |
| WINDOW_UPDATE | 0x8 | Flow control | Section 6.9 |
| CONTINUATION | 0x9 | Header continuation | Section 6.10 |
HTTP/2 uses pseudo-headers for request/response metadata (RFC 7540 Section 8.1.2.3):
const (
PseudoHeaderMethod = ":method" // HTTP method
PseudoHeaderScheme = ":scheme" // URI scheme
PseudoHeaderAuthority = ":authority" // Host info
PseudoHeaderPath = ":path" // Request path
PseudoHeaderStatus = ":status" // Response status
)Standard HTTP/2 error codes (RFC 7540 Section 7):
| Error | Code | Description |
|---|---|---|
| NO_ERROR | 0x0 | Graceful shutdown |
| PROTOCOL_ERROR | 0x1 | Protocol error detected |
| INTERNAL_ERROR | 0x2 | Implementation fault |
| FLOW_CONTROL_ERROR | 0x3 | Flow-control limits exceeded |
| SETTINGS_TIMEOUT | 0x4 | Settings not acknowledged |
| STREAM_CLOSED | 0x5 | Frame received for closed stream |
| FRAME_SIZE_ERROR | 0x6 | Frame size incorrect |
| REFUSED_STREAM | 0x7 | Stream not processed |
| CANCEL | 0x8 | Stream cancelled |
| COMPRESSION_ERROR | 0x9 | Compression state not updated |
| CONNECT_ERROR | 0xa | TCP connection error |
| ENHANCE_YOUR_CALM | 0xb | Processing capacity exceeded |
| INADEQUATE_SECURITY | 0xc | Negotiated TLS parameters inadequate |
| HTTP_1_1_REQUIRED | 0xd | Use HTTP/1.1 for the request |
Supports concurrent streams on single connection (RFC 7540 Section 5):
// Multiple concurrent requests
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, _ := client.GET("/api/data", "example.com")
// Process response...
}()
}
wg.Wait()Implements HTTP/2 flow control (RFC 7540 Section 5.2):
- Connection-level: Global flow control
- Stream-level: Per-stream flow control
- Window updates: Automatic window management
HPACK compression reduces header overhead (RFC 7541):
- Static table: Pre-defined common headers
- Dynamic table: Connection-specific header cache
- Huffman encoding: Additional compression for header values
client, err := http2.NewHTTP2Client("httpbin.org:443")
if err != nil {
log.Fatal(err)
}
defer client.Close()
resp, err := client.Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Body: %s\n", body)jsonData := []byte(`{"message": "hello world"}`)
resp, err := client.Post("https://httpbin.org/post",
"application/json", bytes.NewReader(jsonData))
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Process response...const numRequests = 10000
client, _ := http2.NewClient("server:port")
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := client.GET("/api/fast", "server:port")
if err != nil {
log.Printf("Request failed: %v", err)
return
}
if resp.StatusCode != 200 {
log.Printf("Unexpected status: %d", resp.StatusCode)
}
}()
}
wg.Wait()
duration := time.Since(start)
fmt.Printf("Completed %d requests in %v\n", numRequests, duration)
fmt.Printf("Rate: %.2f req/sec\n", float64(numRequests)/duration.Seconds())Enable debug logging by setting the DEBUG_HTTP2_LOG environment variable:
# Set log level (debug, info, warn, error, fatal, panic)
export DEBUG_HTTP2_LOG=debug
# Run your application
go run main.go// Debug levels:
// - debug: Detailed HTTP/2 frame processing logs
// - info: General connection and stream information
// - warn: Non-critical issues and warnings
// - error: Error conditions that don't stop execution
// - fatal: Critical errors that terminate the application
// - panic: Severe errors that cause panicExample debug output:
[DEBUG] Frame received: SETTINGS, StreamID=0, Length=18
[INFO] Connection established to server:443
[DEBUG] Stream 1 state: OPEN -> HALF_CLOSED_LOCAL
[WARN] Flow control window low: 1024 bytes remaining
HTTP/2 connection settings (RFC 7540 Section 6.5):
// Settings are automatically negotiated during connection setup
// Common settings include:
// - HEADER_TABLE_SIZE: HPACK dynamic table size
// - ENABLE_PUSH: Server push capability
// - MAX_CONCURRENT_STREAMS: Stream concurrency limit
// - INITIAL_WINDOW_SIZE: Flow control window
// - MAX_FRAME_SIZE: Maximum frame payload size
// - MAX_HEADER_LIST_SIZE: Header list size limitgo test ./...# Run performance benchmarks
go run cmd/benchmark/main.go
# Expected output:
# Started benchmark...
# Send 10000 requests in 2.5sTest against real HTTP/2 servers:
go run cmd/example/main.goEnable debug logging by setting the DEBUG_HTTP2_LOG environment variable:
# Set log level (debug, info, warn, error, fatal, panic)
export DEBUG_HTTP2_LOG=debug
# Run your application
go run main.go// Debug levels:
// - debug: Detailed HTTP/2 frame processing logs
// - info: General connection and stream information
// - warn: Non-critical issues and warnings
// - error: Error conditions that don't stop execution
// - fatal: Critical errors that terminate the application
// - panic: Severe errors that cause panicExample debug output:
[DEBUG] Frame received: SETTINGS, StreamID=0, Length=18
[INFO] Connection established to server:443
[DEBUG] Stream 1 state: OPEN -> HALF_CLOSED_LOCAL
[WARN] Flow control window low: 1024 bytes remaining
This implementation strictly follows RFC 7540 specifications:
- Section 3: Connection establishment and preface
- Section 4: HTTP frame format and processing
- Section 5: Stream states and multiplexing
- Section 6: Frame type definitions
- Section 7: Error handling
- Section 8: HTTP semantics mapping
- Section 9: Security considerations
Comprehensive benchmarks comparing this library with golang.org/x/net/http2 under identical test conditions.
- Benchmark Type: Sequential request latency test
- Server: Local HTTP/2 test server (127.0.0.1:1234)
- Hardware: AMD Ryzen 5 5500U with Radeon Graphics
- OS: Windows
- Architecture: amd64
- Benchmark Duration: 30 seconds per test
| Metric | golang.org/x/net/http2 | github.com/chronnie/http2 | Improvement |
|---|---|---|---|
| Latency (ns/op) | 306,503 ns | 27,214 ns | 11.3x faster ⚡ |
| Throughput (req/s) | ~3,262 req/s | ~36,745 req/s | 11.3x higher 🚀 |
| Memory/Request | 4,199 B | 4,670 B | +11% |
| Allocations/Request | 39 allocs | 59 allocs | +51% |
This Library (github.com/chronnie/http2):
$ go test -bench=BenchmarkRawHTTP2Client$ -benchmem -benchtime=30s
goos: windows
goarch: amd64
pkg: http2bench
cpu: AMD Ryzen 5 5500U with Radeon Graphics
BenchmarkRawHTTP2Client-12 1349041 27214 ns/op 4670 B/op 59 allocs/op
PASS
ok http2bench 65.246sStandard Library (golang.org/x/net/http2):
$ go test -bench=BenchmarkGolangNetHTTP2$ -benchmem -benchtime=30s
goos: windows
goarch: amd64
pkg: http2bench
cpu: AMD Ryzen 5 5500U with Radeon Graphics
BenchmarkGolangNetHTTP2-12 113485 306503 ns/op 4199 B/op 39 allocs/op
PASS
ok http2bench 39.397s✅ 11.3x Lower Latency: Average request latency reduced from 306µs to 27µs
✅ 11.3x Higher Throughput: Handles 36,745 requests/sec vs 3,262 requests/sec
The benchmark code is available in a separate repository to avoid dependency pollution:
package http2bench_test
import (
"context"
"crypto/tls"
"net"
"net/http"
"testing"
"time"
customhttp2 "github.com/chronnie/http2"
"golang.org/x/net/http2"
)
// BenchmarkRawHTTP2Client tests this library
func BenchmarkRawHTTP2Client(b *testing.B) {
client, err := customhttp2.NewClient("127.0.0.1:1234")
if err != nil {
b.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
client.SetTimeout(10 * time.Second)
// Warmup
resp, err := client.GET("/info", "127.0.0.1:1234")
if err != nil {
b.Fatalf("Warmup failed: %v", err)
}
if resp == nil || resp.StatusCode != 200 {
b.Fatal("Warmup failed")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, err := client.GET("/info", "127.0.0.1:1234")
if err != nil {
b.Fatalf("Request %d failed: %v", i, err)
}
if resp == nil || resp.StatusCode != 200 {
b.Fatalf("Request %d: invalid response", i)
}
}
}
// BenchmarkGolangNetHTTP2 tests golang.org/x/net/http2
func BenchmarkGolangNetHTTP2(b *testing.B) {
tr := &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
client := &http.Client{
Transport: tr,
Timeout: 10 * time.Second,
}
// Warmup
resp, err := client.Get("http://127.0.0.1:1234/info")
if err != nil {
b.Fatalf("Warmup failed: %v", err)
}
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, err := client.Get("http://127.0.0.1:1234/info")
if err != nil {
b.Fatalf("Request failed: %v", err)
}
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}
}To reproduce these benchmarks:
# Clone the benchmark repository
git clone https://github.com/chronnie/http2-benchmark
cd http2-benchmark
# Install dependencies
go mod download
# Run the benchmarks
go test -bench=BenchmarkRawHTTP2Client$ -benchmem -benchtime=30s
go test -bench=BenchmarkGolangNetHTTP2$ -benchmem -benchtime=30scurrently no license
- RFC 7540 - HTTP/2 Specification
- RFC 7541 - HPACK Header Compression
- RFC 7230 - HTTP/1.1 Message Syntax
Made with ❤️ and strict RFC 7540 compliance