HTTP Server

The http.Server struct provides a configurable to create an HTTP-server compared to the simple http.ListenAndServe function we have used.

Basic HTTP Server Configuration

http.Server allows you to explicitly define server parameters:

package main

import (
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)

	server := &http.Server{
		Addr:         "127.0.0.1:8080",
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	err := server.ListenAndServe()
	if err != nil {
		panic(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world"))
}

Key Configuration Options

Timeouts

Proper timeouts are critical for production REST APIs

  • ReadTimeout: Maximum duration for reading the entire request, including body
  • WriteTimeout: Maximum duration before timing out writes of the response
  • IdleTimeout: Maximum amount of time to wait for the next request when keep-alives are enabled

Without proper timeouts, your server may be vulnerable to slow client attacks or resource exhaustion.

TLS Configuration

For secure HTTPS connections, you can configure TLS directly:

server := &http.Server{
	Addr:    "127.0.0.1:8443",
	Handler: mux,
	TLSConfig: &tls.Config{
		MinVersion: tls.VersionTLS12,
	},
}

err := server.ListenAndServeTLS("server.crt", "server.key")
if err != nil {
	panic(err)
}

Graceful Shutdown

One of the most important features of using http.Server directly is the ability to implement graceful shutdown, which is essential for production REST APIs:

package main

import (
	"context"
	"errors"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	err := run()

	if err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}

	log.Println("Server stopped gracefully")
}

func run() error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)

	server := &http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: mux,
	}

	errc := make(chan error, 1)

	// start the server in a goroutine
	go func() {
		defer close(errc)
		// when shutdown is called, the server will immediatelly return and stop accepting new connections
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			errc <- err

			return
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	<-quit
	log.Println("Shutting down server...")

	// we give the server 5 seconds to finish the in progress requests
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Disable keep-alives
	server.SetKeepAlivesEnabled(false)

	// we should wait for the server to finish
	err := server.Shutdown(ctx)
	if err != nil {
		return err
	}

	return <-errc
}

func handler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world"))
}

When to use http.Server and best practices

Always use the http.Server. An exception might be non production test apps

  • Always set timeouts
  • Implement graceful shutdown
  • Use TLS on production