go
52 lines · 7 steps
Building a hardened Go reverse proxy
How to wrap httputil.ReverseProxy with sane timeouts, header rewriting, and error handling for an edge gateway.
Explained by
highlit
1package proxy
2
3import (
4 "log"
5 "net"
6 "net/http"
7 "net/http/httputil"
8 "net/url"
9 "time"
10)
11
12func NewGatewayProxy(target string) (*httputil.ReverseProxy, error) {
13 backend, err := url.Parse(target)
14 if err != nil {
15 return nil, err
16 }
17
18 proxy := httputil.NewSingleHostReverseProxy(backend)
19
20 proxy.Transport = &http.Transport{
21 DialContext: (&net.Dialer{
22 Timeout: 5 * time.Second,
23 KeepAlive: 30 * time.Second,
24 }).DialContext,
25 MaxIdleConns: 100,
26 IdleConnTimeout: 90 * time.Second,
27 TLSHandshakeTimeout: 5 * time.Second,
28 ResponseHeaderTimeout: 10 * time.Second,
29 }
30
31 baseDirector := proxy.Director
32 proxy.Director = func(req *http.Request) {
33 baseDirector(req)
34 req.Host = backend.Host
35 req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
36 req.Header.Set("X-Forwarded-Proto", "https")
37 req.Header.Del("Authorization")
38 }
39
40 proxy.ModifyResponse = func(resp *http.Response) error {
41 resp.Header.Set("Via", "1.1 edge-gateway")
42 resp.Header.Del("Server")
43 return nil
44 }
45
46 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
47 log.Printf("proxy error for %s %s: %v", r.Method, r.URL.Path, err)
48 http.Error(w, "upstream unavailable", http.StatusBadGateway)
49 }
50
51 return proxy, nil
52}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A production reverse proxy needs an explicit Transport with timeouts so a slow upstream can't exhaust connections.
- 2Wrapping the default Director lets you rewrite requests while keeping the library's built-in URL routing intact.
- 3ModifyResponse and ErrorHandler give you control over what clients see on both success and upstream failure.
Related explainers
go
package render import ( "net/http"
How Gin renders MsgPack responses
serialization
content-type
interface-implementation
Intermediate
5 steps
php
namespace App\Providers; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
Adding a whereActive macro in Laravel
macros
service-provider
query-builder
Intermediate
5 steps
rust
use std::time::Duration; use axum::{ body::Body,
Turning Axum timeouts into 504 JSON
middleware
timeouts
error-handling
Intermediate
7 steps
go
package middleware import ( "net/http"
A per-IP rate limiter middleware in Gin
rate-limiting
token-bucket
middleware
Intermediate
8 steps
go
package tsvio import ( "bufio"
Reading and writing TSV records in Go
parsing
io-streams
error-handling
Intermediate
10 steps
go
package config import ( "fmt"
Loading and saving YAML config in Go
yaml
struct-tags
defaults
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-hardened-go-reverse-proxy-explained-go-bdb1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.