Build a Go HTTP Proxy from Scratch: Full Guide with Code
Learn how to implement a functional HTTP/HTTPS proxy in Go, covering proxy concepts, forward and reverse types, protocol specifics, header transformations, and a complete, runnable code example that listens on TCP, parses requests, connects to target servers, and forwards traffic.
This article explains how to implement an HTTP proxy using Go, providing a practical guide for developers who need proxy functionality in real-world applications.
A proxy acts as an intermediary in a network, forwarding client requests to servers and returning responses, effectively serving as a transit station for network information.
Proxies are classified into forward (client‑side) and reverse (server‑side) proxies.
Forward proxy: the client configures the proxy; the proxy forwards requests to the server and is transparent to the server.
Reverse proxy: the server configures the proxy; the proxy forwards client requests to the server and is transparent to the client.
Different protocols are used for each type:
Forward proxy protocols: http, https, socks4, socks5, vpn.
Reverse proxy protocols: tcp, udp, http, https.
http proxy overview
An http proxy is a simple forward proxy that uses the http protocol for communication between the client and the proxy server.
It can carry http, https, ftp, and other protocols, with slight variations in the request format.
http protocol
Client request headers sent to the proxy:
// direct connection
GET / HTTP/1.1
Host: staight.github.io
Connection: keep-alive
// http proxy
GET http://staight.github.io/ HTTP/1.1
Host: staight.github.io
Proxy-Connection: keep-aliveKey differences compared to a direct connection:
URL becomes a full path (e.g., http://staight.github.io/).
The Connection header is replaced by Proxy-Connection.
All other headers remain unchanged.
Why use the full path?
To identify the target server; without a full URL and Host header the proxy cannot determine where to forward the request.
Why replace Connection with Proxy-Connection?
To maintain compatibility with older HTTP/1.0 proxies that do not understand the keep‑alive semantics of HTTP/1.1.
Interaction diagram for an http proxy:
https protocol
Client request headers for an https proxy (CONNECT method):
CONNECT staight.github.io:443 HTTP/1.1
Host: staight.github.io:443
Proxy-Connection: keep-aliveDifferences from the http case:
The request method changes from GET to CONNECT.
The URL does not include a protocol field.
After the CONNECT handshake, the proxy simply forwards encrypted TCP data without modifying the payload.
Interaction diagram for an https proxy:
Code implementation
First, create a TCP listener on port 8080 and handle each incoming connection:
// tcp connection, listen on 8080
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handle(client)
}Read the client data into a buffer:
var b [1024]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}Parse the request method and URL, then determine the target address based on the protocol (CONNECT indicates https, otherwise http with a default port of 80 if none is specified):
var method, URL, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '
')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
log.Println(err)
return
}
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else {
address = hostPortURL.Host
if strings.Index(hostPortURL.Host, ":") == -1 {
address = hostPortURL.Host + ":80"
}
}Establish a TCP connection to the target server and forward data. For CONNECT requests, send an HTTP 200 response to the client first:
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established
")
} else {
server.Write(b[:n])
}
go io.Copy(server, client)
io.Copy(client, server)The complete source code is provided below:
package main
import (
"bytes"
"fmt"
"io"
"log"
"net"
"net/url"
"strings"
)
func main() {
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handle(client)
}
}
func handle(client net.Conn) {
if client == nil {
return
}
defer client.Close()
log.Printf("remote addr: %v
", client.RemoteAddr())
var b [1024]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, URL, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '
')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
log.Println(err)
return
}
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else {
address = hostPortURL.Host
if strings.Index(hostPortURL.Host, ":") == -1 {
address = hostPortURL.Host + ":80"
}
}
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established
")
} else {
server.Write(b[:n])
}
go io.Copy(server, client)
io.Copy(client, server)
}After adding the proxy configuration, run the program and point your client to the proxy server.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
