Go Module 18 – HTTP servers and clients with net/http
Production servers: timeouts, shutdown, TLS, CSRF
Run a Go 1.27 HTTP server in production: timeouts and header limits, a SIGTERM shutdown that drops no request, and TLS and CSRF checks for browsers.
What you will learn
- Configure http.Server timeouts and header limits, and predict which one ends a slow connection
- Implement a graceful shutdown on SIGTERM with signal.NotifyContext and Server.Shutdown
- Configure TLS and HTTP/2, and protect browser forms with http.CrossOriginProtection
- Set session cookies and read client addresses behind a proxy so that neither can be forged
Before you start
What you’ll learn · Before you start
What you will learn
- Configure http.Server timeouts and header limits, and predict which one ends a slow connection
- Implement a graceful shutdown on SIGTERM with signal.NotifyContext and Server.Shutdown
- Configure TLS and HTTP/2, and protect browser forms with http.CrossOriginProtection
- Set session cookies and read client addresses behind a proxy so that neither can be forged
Before you start
On this page
A Go server started with http.ListenAndServe(":8080", mux) has no timeouts at all: in timeouts.go below, a client
that sent its headers one byte every 50 ms still held its connection after a second, and nothing in the server would
end it in an hour either. Stopped by a deploy, the same server drops every request in flight.
You will set a server’s timeouts and limits, stop it on SIGTERM without dropping a request, and turn on what browsers need, TLS and cross-origin checks. Every program here ran on Go 1.27.1, over in-memory pipes instead of a network.
A zero http.Server waits for ever
The zero http.Server is a valid configuration with no time limits. Four fields bound the phases of a connection:
ReadHeaderTimeoutbounds reading the request line and headers.ReadTimeoutbounds reading the whole request, body included.WriteTimeoutbounds writing the response, counted from the end of the request headers.IdleTimeoutbounds the wait for the next request on a kept-alive connection.
Two more bound the size of the headers: MaxHeaderBytes, 1 MB by default, and MaxHeaderValueCount, new in Go
1.27, 500 values by default. timeouts.go sends headers one byte every 50 ms to a zero server and to one with a
ReadHeaderTimeout of 200 ms:
// timeouts.go: a client that sends its request headers one byte every 50 ms, against a zero http.Server and against
// one with a ReadHeaderTimeout of 200 ms. The network is in-memory pipes, so the program needs no port.
package main
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
)
// slowHeaders sends a request line, then one header byte every 50 ms, while it reads whatever the server sends.
// It reports whether the server closed the connection within the budget, and after how long.
func slowHeaders(ln *memListener, budget time.Duration) (closed bool, after time.Duration, sent int) {
c, _ := ln.Dial(context.Background(), "", "")
defer c.Close()
start := time.Now()
done := make(chan int, 1)
go func() {
n, _ := io.Copy(io.Discard, c) // returns when the server closes the connection
done <- int(n)
}()
go func() {
c.Write([]byte("GET /books HTTP/1.1\r\nHost: library.test\r\nX-Slow: "))
for {
time.Sleep(50 * time.Millisecond)
if _, err := c.Write([]byte("x")); err != nil {
return
}
}
}()
select {
case n := <-done:
return true, time.Since(start), n
case <-time.After(budget):
return false, budget, 0
}
}
func main() {
handled := make(chan struct{}, 1)
mux := http.NewServeMux()
mux.HandleFunc("GET /books", func(w http.ResponseWriter, r *http.Request) { handled <- struct{}{} })
servers := []struct {
name string
srv *http.Server
}{
{"http.Server{}", &http.Server{Handler: mux}},
{"ReadHeaderTimeout 200ms", &http.Server{Handler: mux, ReadHeaderTimeout: 200 * time.Millisecond}},
}
for _, s := range servers {
fmt.Printf("%s: ReadHeaderTimeout %v, ReadTimeout %v, WriteTimeout %v, IdleTimeout %v\n", s.name,
s.srv.ReadHeaderTimeout, s.srv.ReadTimeout, s.srv.WriteTimeout, s.srv.IdleTimeout)
ln := newMemListener()
go s.srv.Serve(ln)
closed, after, sent := slowHeaders(ln, time.Second)
if closed {
fmt.Printf(" the server closed the connection after about 200 ms: %t, and sent %d bytes back\n",
after >= 200*time.Millisecond && after < 400*time.Millisecond, sent)
} else {
fmt.Println(" after 1s the client is still sending headers and the connection is still open")
}
select {
case <-handled:
fmt.Println(" the handler ran")
default:
fmt.Println(" the handler never ran")
}
s.srv.Close()
}
}
// memListener is a net.Listener made of in-memory pipes: Dial hands one end of a net.Pipe to Accept.
type memListener struct {
conns chan net.Conn
closed chan struct{}
once sync.Once
}
func newMemListener() *memListener {
return &memListener{conns: make(chan net.Conn), closed: make(chan struct{})}
}
func (l *memListener) Accept() (net.Conn, error) {
select {
case c := <-l.conns:
return c, nil
case <-l.closed:
return nil, net.ErrClosed
}
}
func (l *memListener) Close() error {
l.once.Do(func() { close(l.closed) })
return nil
}
func (l *memListener) Addr() net.Addr { return memAddr{} }
func (l *memListener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
server, client := net.Pipe()
select {
case l.conns <- server:
return client, nil
case <-l.closed:
return nil, errors.New("connection refused")
case <-ctx.Done():
return nil, ctx.Err()
}
}
type memAddr struct{}
func (memAddr) Network() string { return "mem" }
func (memAddr) String() string { return "library.test:80" } Output
http.Server{}: ReadHeaderTimeout 0s, ReadTimeout 0s, WriteTimeout 0s, IdleTimeout 0s
after 1s the client is still sending headers and the connection is still open
the handler never ran
ReadHeaderTimeout 200ms: ReadHeaderTimeout 200ms, ReadTimeout 0s, WriteTimeout 0s, IdleTimeout 0s
the server closed the connection after about 200 ms: true, and sent 0 bytes back
the handler never ran
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run timeouts.go
The zero server kept the connection, and a goroutine with it, for as long as the client wished. The second closed it at 200 ms without a reply, and the handler never ran. A server that faces the internet sets ReadHeaderTimeout, WriteTimeout and IdleTimeout, and ReadTimeout too when every route’s request bodies are small. Walk through the phases with the setting that bounds each:
- Request headers
Reading the request line and the headers ReadHeaderTimeout
ReadHeaderTimeout bounds this phase from the moment the server starts to read a request. MaxHeaderBytes limits how many bytes the headers may take, and MaxHeaderValueCount, new in Go 1.27, how many values they may hold. In timeouts.go, a client that sent a byte every 50 ms was cut off at 200 ms.
- Request body
Reading the body ReadTimeout
ReadTimeout bounds reading the whole request, the headers and the body together. The documentation notes that it cannot give one upload more time than another, which is why most servers set ReadHeaderTimeout and leave body limits to each handler.
- The handler
Running the handler the request context
No field of http.Server limits how long a handler runs. The request's context, with a deadline the handler sets, or http.TimeoutHandler around a route that never streams, are the tools for that.
- The response
Writing the response WriteTimeout
WriteTimeout bounds the writes of the response. It is reset when a new request's headers have been read, so it has to be longer than the slowest handler plus the time its reply takes to send.
- Keep-alive wait
Waiting for the next request IdleTimeout
After a response, an HTTP/1 connection may be kept open for the next request. IdleTimeout bounds that wait; when it is zero, ReadTimeout is used, and when both are zero, an idle connection stays open for as long as the client keeps it.
The timeline has 5 events, from Request headers to Keep-alive wait.
Checkpoint
Check: Server timeouts and header limits
Graceful shutdown finishes the requests in flight
A supervisor stops a process with SIGTERM, and Ctrl+C sends an interrupt. signal.NotifyContext turns either into a
context that ends, and srv.Shutdown(ctx) stops the server graceful shutdown: Stopping a server without dropping work: it stops accepting new connections, lets the requests in flight finish within a time budget, and only then exits. In Go, http.Server.Shutdown does this after a signal such as SIGTERM.: it closes
the listener, closes idle connections, and waits for active ones to finish. If the context passed to Shutdown runs out
first, Shutdown returns its error. shutdown.go sends itself SIGTERM 100 ms into a request that takes 300 ms:
// shutdown.go: a server that stops on SIGTERM the way a supervisor asks it to. signal.NotifyContext ends a context,
// Shutdown closes the listener and waits for the request in flight, and a request that comes after that is refused.
// The program sends SIGTERM to itself, 100 ms into a request that takes 300 ms.
package main
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
mux := http.NewServeMux()
mux.HandleFunc("GET /report", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(300 * time.Millisecond) // a request that is still running when the signal arrives
fmt.Fprintln(w, "report ready")
})
ln := newMemListener()
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second}
served := make(chan error, 1)
go func() { served <- srv.Serve(ln) }()
inFlight := make(chan string, 1)
go func() { inFlight <- get(ln, "http://library.test/report") }()
time.Sleep(100 * time.Millisecond)
syscall.Kill(os.Getpid(), syscall.SIGTERM) // what a supervisor sends to stop the process
<-ctx.Done()
fmt.Println("1. the signal context ended:", context.Cause(ctx))
shutdownErr := make(chan error, 1)
go func() {
sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
shutdownErr <- srv.Shutdown(sctx)
}()
time.Sleep(50 * time.Millisecond) // Shutdown has closed the listener by now
fmt.Println("2. a new request during the shutdown:", get(ln, "http://library.test/report"))
fmt.Println("3. the request that was in flight:", <-inFlight)
fmt.Println("4. Shutdown returned:", <-shutdownErr)
err := <-served
fmt.Printf("5. Serve returned %q, which is http.ErrServerClosed: %t\n", err, errors.Is(err, http.ErrServerClosed))
}
// get sends one request on a client of its own, so that it opens a connection of its own.
func get(ln *memListener, url string) string {
client := &http.Client{Transport: &http.Transport{DialContext: ln.Dial}}
resp, err := client.Get(url)
if err != nil {
return "error: " + err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return fmt.Sprintf("%s %q", resp.Status, body)
}
// memListener is a net.Listener made of in-memory pipes: Dial hands one end of a net.Pipe to Accept.
type memListener struct {
conns chan net.Conn
closed chan struct{}
once sync.Once
}
func newMemListener() *memListener {
return &memListener{conns: make(chan net.Conn), closed: make(chan struct{})}
}
func (l *memListener) Accept() (net.Conn, error) {
select {
case c := <-l.conns:
return c, nil
case <-l.closed:
return nil, net.ErrClosed
}
}
func (l *memListener) Close() error {
l.once.Do(func() { close(l.closed) })
return nil
}
func (l *memListener) Addr() net.Addr { return memAddr{} }
// Dial fails once the listener is closed, as a TCP connect to a port nobody listens on is refused.
func (l *memListener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
server, client := net.Pipe()
select {
case l.conns <- server:
return client, nil
case <-l.closed:
return nil, errors.New("connection refused")
case <-ctx.Done():
return nil, ctx.Err()
}
}
type memAddr struct{}
func (memAddr) Network() string { return "mem" }
func (memAddr) String() string { return "library.test:80" } Output
1. the signal context ended: terminated signal received 2. a new request during the shutdown: error: Get "http://library.test/report": connection refused 3. the request that was in flight: 200 OK "report ready\n" 4. Shutdown returned: <nil> 5. Serve returned "http: Server closed", which is http.ErrServerClosed: true
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run shutdown.go
The signal’s context ended with the cause terminated signal received. A new request was refused, the one in flight
finished with 200, Shutdown returned nil, and Serve returned http.ErrServerClosed. Step through the run:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A request in flight. A client asks for /report, whose handler takes 300 ms. The server is serving it on the connection the client opened.
- The signal arrives. 100 ms later the supervisor sends SIGTERM, as a deploy does before it replaces the process. signal.NotifyContext ends its context, and context.Cause says terminated signal received.
- The listener closes. Shutdown closes the listener first, so no new connection is accepted, and closes the connections that are idle. Serve returns http.ErrServerClosed at once, while Shutdown keeps waiting.
- A new request is refused. A second client tries to connect during the shutdown. Nothing accepts connections any more, so the attempt is refused at once instead of being left to wait.
- The first request finishes. The handler that was running when the signal came finishes its 300 ms and its reply goes out, 200 report ready. Its connection is now idle, and Shutdown closes it.
- Shutdown returns. With no connection left, Shutdown returns nil, well inside its 10 s budget, and main can exit. Nothing that was accepted was dropped.
Returning from main when Serve returns
Serve and ListenAndServe return ErrServerClosed as soon as Shutdown begins, not when it ends. A main that exits then kills every request still running. Wait for Shutdown to return, as shutdown.go does, then exit.
Checkpoint
Check: Graceful shutdown on SIGTERM
TLS and cross-origin checks protect browser users
TLS encrypts the connection and proves the server’s name. Serve it with srv.ListenAndServeTLS(certFile, keyFile) or
a tls.Config, and Go 1.27 already chooses well: the minimum version is TLS 1.2 for clients and servers whatever the
GODEBUG settings, hybrid post-quantum key exchange is on by default, and HTTP/2 is offered over TLS automatically.
A cross-site request forgery, CSRF, is a page on another site that makes a signed-in user’s browser send a request
that changes something. http.CrossOriginProtection, added in Go 1.25, refuses such requests without tokens: it reads
the fetch metadata headers: Request headers that browsers add and page scripts cannot set, saying where a request came from. Sec-Fetch-Site is one of them: same-origin, same-site, cross-site, or none for a request the user started directly. header that browsers send, or compares Origin with Host when that
header is missing, and always lets GET, HEAD and OPTIONS through. csrf.go sends it six requests:
// csrf.go: http.CrossOriginProtection (Go 1.25) in front of a form that cancels an order, and the six kinds of
// request it meets: from the shop's own pages, from another site, from an older browser, from a script, a safe GET,
// and from a second site of the shop, a sibling subdomain, which is trusted on purpose.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /orders/{id}/cancel", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "order %s cancelled\n", r.PathValue("id"))
})
mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "order %s\n", r.PathValue("id"))
})
cop := http.NewCrossOriginProtection()
if err := cop.AddTrustedOrigin("https://shop.library.test"); err != nil {
fmt.Println("AddTrustedOrigin:", err)
}
h := cop.Handler(mux)
requests := []struct {
what, method, path string
headers map[string]string
}{
{"the shop's own page", "POST", "/orders/7/cancel", map[string]string{"Sec-Fetch-Site": "same-origin"}},
{"a page on another site", "POST", "/orders/7/cancel", map[string]string{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.test"}},
{"an older browser, Origin only", "POST", "/orders/7/cancel", map[string]string{"Origin": "https://evil.test"}},
{"a script, no browser headers", "POST", "/orders/7/cancel", nil},
{"another site, but a GET", "GET", "/orders/7", map[string]string{"Sec-Fetch-Site": "cross-site"}},
{"the trusted second site", "POST", "/orders/7/cancel", map[string]string{"Sec-Fetch-Site": "same-site", "Origin": "https://shop.library.test"}},
}
for i, rq := range requests {
req := httptest.NewRequest(rq.method, "https://library.test"+rq.path, nil)
for k, v := range rq.headers {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Printf("%d. %s: %d %s\n", i+1, rq.what, rec.Code, strings.TrimSpace(rec.Body.String()))
}
} Output
1. the shop's own page: 200 order 7 cancelled 2. a page on another site: 403 cross-origin request detected from Sec-Fetch-Site header 3. an older browser, Origin only: 403 cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host 4. a script, no browser headers: 200 order 7 cancelled 5. another site, but a GET: 200 order 7 6. the trusted second site: 200 order 7 cancelled
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run csrf.go
The other site’s POST got 403, and so did an older browser’s, caught by its Origin. A script with no browser headers got through: CSRF needs a victim’s browser, so other callers must be stopped by authentication instead.
Key idea
Go 1.27’s TLS defaults protect the connection with no extra settings, and CrossOriginProtection refuses the writes that another site’s page makes a browser send; callers that are not browsers still need authentication.
Checkpoint
Check: TLS defaults and cross-origin protection
Three parts go further. Each timeout is measured on its own connection. The drain around Shutdown gets a readiness check, a hook for streams and a budget. TLS, cookies and proxy headers are checked in real runs.
Every timeout and header limit, measured, and sized with numbers Pro: opens with a Pro, Premium or Ultimate pass
A setting is only as good as what it ends and when. This part measures each timeout and header limit on a connection of its own, shows the two results that surprise people, and turns the numbers into a choice of values.
In this part:
- Every timeout and header limit, measured on its own connection
- Sizing the timeouts, with worked numbers
1 runnable example (Go) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Shutdown under load: readiness, hooks for streams and a drain budget Pro: opens with a Pro, Premium or Ultimate pass
Shutdown waits for requests, but not every request ends by itself, and traffic does not stop the moment the signal arrives. This part adds the three pieces a deploy needs around Shutdown, measures what happens without one of them, and fits the whole drain into a supervisor’s grace period.
In this part:
- A readiness check, a shutdown hook and a deadline for the drain
- A drain budget that fits the grace period, with worked numbers
1 runnable example (Go) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
TLS, cookies and proxy headers for browser traffic, measured Pro: opens with a Pro, Premium or Ultimate pass
Browser traffic adds three things a server must get exactly right: the TLS it negotiates, the cookies it sets and the client address it believes. This part measures a real handshake, follows CrossOriginProtection’s checks in their order, and tests a cookie and a proxy header against forged requests.
In this part:
- TLS 1.3, post-quantum key exchange and HTTP/2, measured on a handshake
- Cross-origin protection, check by check
- Session cookies and client addresses that cannot be forged
- Practise: a session cookie the browser holds to its rules
5 runnable examples (Go, Go (cookie.go), Go (cookie_test.go), Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: harden a server
practice/server.go has NewServer, which returns a zero http.Server for an app whose routes take small form bodies. practice/server_test.go checks its timeouts and its header limit, and that a cross-site POST is refused while a same-origin one gets through:
Go (server.go) · practice/server.go
// Package practice holds the settings of a production server to finish: timeouts and header limits, and cross-origin
// protection for browser forms.
package practice
import "net/http"
// NewServer returns the server that main starts, for an app whose routes take small form bodies only. It must not
// wait for ever on a slow client, and every request must pass http.CrossOriginProtection before it reaches h.
func NewServer(addr string, h http.Handler) *http.Server {
return &http.Server{Addr: addr, Handler: h} // a zero Server: no timeouts, no protection
} Go (server_test.go) · practice/server_test.go
package practice
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestTimeoutsAndLimits(t *testing.T) {
srv := NewServer(":8080", http.NotFoundHandler())
checks := []struct {
name string
got, max time.Duration
}{
{"ReadHeaderTimeout", srv.ReadHeaderTimeout, 10 * time.Second},
{"ReadTimeout", srv.ReadTimeout, time.Minute},
{"WriteTimeout", srv.WriteTimeout, time.Minute},
{"IdleTimeout", srv.IdleTimeout, 5 * time.Minute},
}
for _, c := range checks {
if c.got <= 0 || c.got > c.max {
t.Errorf("%s = %v, want more than 0 and at most %v", c.name, c.got, c.max)
}
}
if srv.MaxHeaderBytes <= 0 || srv.MaxHeaderBytes > 64<<10 {
t.Errorf("MaxHeaderBytes = %d, want a limit from 1 byte to 64 KB", srv.MaxHeaderBytes)
}
}
func TestCrossOriginFormsRefused(t *testing.T) {
ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })
h := NewServer(":8080", ok).Handler
for _, site := range []string{"cross-site", "same-origin"} {
req := httptest.NewRequest("POST", "https://library.test/orders/7/cancel", nil)
req.Header.Set("Sec-Fetch-Site", site)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
want := map[string]int{"cross-site": http.StatusForbidden, "same-origin": http.StatusNoContent}[site]
if rec.Code != want {
t.Errorf("a POST with Sec-Fetch-Site %s got %d, want %d", site, rec.Code, want)
}
}
} from session import run
# The starter server against its tests: a zero http.Server with no cross-origin protection.
run(
"""
cd practice && go test .
""",
expect_failure=("cd practice && go test .",),
) Output
$ cd practice && go test .
--- FAIL: TestTimeoutsAndLimits (0.00s)
server_test.go:23: ReadHeaderTimeout = 0s, want more than 0 and at most 10s
server_test.go:23: ReadTimeout = 0s, want more than 0 and at most 1m0s
server_test.go:23: WriteTimeout = 0s, want more than 0 and at most 1m0s
server_test.go:23: IdleTimeout = 0s, want more than 0 and at most 5m0s
server_test.go:27: MaxHeaderBytes = 0, want a limit from 1 byte to 64 KB
--- FAIL: TestCrossOriginFormsRefused (0.00s)
server_test.go:41: a POST with Sec-Fetch-Site cross-site got 204, want 403
FAIL
FAIL example.com/practice 1.552s
FAIL
[exit status 1]
This output changes from run to run: go test prints how long each test and the whole run took, which differs from one run to the next.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
Make go test . pass in the practice folder. A first hint: wrap the handler with
http.NewCrossOriginProtection().Handler(h). Then the questions on timeouts, shutdown and browser protection:
Practice that adapts to you
Practise this lesson
6 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): Why does a Go server need ReadHeaderTimeout when it already has a handler timeout?
A handler timeout only starts once the handler runs, and the handler runs only after the request’s headers have arrived. A client that sends its headers slowly never reaches the handler, so only ReadHeaderTimeout ends it. Without one, each such client holds a connection and a goroutine for as long as it likes.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
A Go API's goroutine count climbs to 50,000 overnight while its requests per second stay flat, and the process runs out of memory. It was started with http.ListenAndServe(":8080", mux). What is the likely cause, how do you confirm it, and what do you configure?
-
Senior to staff
Every deploy causes a burst of 502 errors at the load balancer for about 5 seconds, although the service calls srv.Shutdown on SIGTERM. Explain the likely causes and design a shutdown sequence that drops nothing.
-
Senior to staff
A web app in Go uses cookie sessions and HTML forms to move money between a user's accounts. Design its protection against forged requests and stolen session cookies: which Go features you use, which cookie attributes you set, and what is still needed for older browsers and non-browser clients.
Key takeaways
- The zero http.Server waits for ever: set ReadHeaderTimeout, WriteTimeout and IdleTimeout, ReadTimeout when every body is small, and limits for the headers.
- Stop on SIGTERM with signal.NotifyContext and Shutdown, and let main exit only once Shutdown has returned.
- Go 1.27 serves TLS 1.2 and up with post-quantum key exchange and HTTP/2 by default, and CrossOriginProtection refuses cross-site browser writes without tokens.
Terms in this lesson
- fetch metadata headers
- Request headers that browsers add and page scripts cannot set, saying where a request came from. Sec-Fetch-Site is one of them: same-origin, same-site, cross-site, or none for a request the user started directly.
- graceful shutdown
- Stopping a server without dropping work: it stops accepting new connections, lets the requests in flight finish within a time budget, and only then exits. In Go, http.Server.Shutdown does this after a signal such as SIGTERM.
- Little's law
- A rule of queueing theory: the average number of items in a system equals the rate at which they arrive times the average time each one stays. 200 arrivals a second that stay 5 s each make about 1,000 present at once.
References
- Package net/http: Server (The Go Authors)
- Package net/http: Server.Shutdown and RegisterOnShutdown (The Go Authors)
- Package net/http: CrossOriginProtection (The Go Authors)
- Package os/signal: NotifyContext (The Go Authors)
- Package crypto/tls: Config (The Go Authors)
- Go 1.27 Release Notes: net/http and crypto/tls (The Go Authors)
- Go 1.26 Release Notes: crypto/tls (The Go Authors)
- Go 1.25 Release Notes: net/http (The Go Authors)
- Go 1.24 Release Notes: net/http and crypto/tls (The Go Authors)
- Package golang.org/x/crypto/acme/autocert (The Go Authors)
- Session Management Cheat Sheet (OWASP Foundation)
- X-Forwarded-For (MDN Web Docs) (Mozilla)
- Pod Lifecycle: Termination of Pods (Kubernetes documentation) (The Kubernetes Authors)
- RFC 5737: IPv4 Address Blocks Reserved for Documentation (IETF)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress