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 streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
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.