In this example we are going to be using context from signal.NotifyContext as HTTP server's base context in Golang. The reason why we do is because, we want to propagate interruption signals to all live HTTP requests so that they are either terminated or prevented from being run. Every request context will listen on this context. The context will immediately be cancelled without waiting for its original deadline so this takes precedence over all timeouts/deadlines on a context.


As a bonus feature, we will gracefully shutdown server using same context. Shutdown expects context from signal.NotifyContext and listens on it to start graceful server shutdown. As soon as the incoming context is closed, graceful server shutdown starts but allows live requests given timeout duration to complete.


Example


Here is a basic example.


main.go


package main

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

"random/api"
"random/service"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

// HTTP router -------------------------------------------------------------
rtr := http.NewServeMux()
rtr.HandleFunc("/fast", api.Fast)
rtr.HandleFunc("/slow", api.Slow)

// HTTP server -------------------------------------------------------------
srv := &http.Server{
// BaseContext accepts context from `signal.NotifyContext` and propagates
// interruption signals to all live HTTP requests so that they are either
// terminated or prevented from being run. Every request context listens
// on this context. The context will immediatelly be cancelled without
// waiting for its original deadline so this takes precedence over all
// timeouts/deadlines on a context.
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: rtr,
Addr: ":1234",
}

// Service -----------------------------------------------------------------
svc := service.Service{
Server: srv,
Timeout: time.Second * 10,
}

go func() {
if err := svc.Start(); err != nil {
slog.Error("app start", "error", err)

return
}
}()

if err := svc.Shutdown(ctx); err != nil {
slog.Warn("dirty service shutdown with possible interruptions", "error", err)
} else {
slog.Info("clean service shutdown without any interruption")
}
}

service.go


package service

import (
"context"
"net/http"
"time"
)

type Service struct {
Server *http.Server
Timeout time.Duration
}

func (s Service) Start() error {
if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}

return nil
}

// Shutdown expects context from `signal.NotifyContext` and listens on it to
// start graceful server shutdown. As soon as the incoming context is closed,
// graceful server shutdown starts but allows live requests given timeout duration
// to complete.
func (s Service) Shutdown(ctx context.Context) error {
<-ctx.Done()

ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()

if err := s.Server.Shutdown(ctx); err != nil {
return err
}

return nil
}

api.go


package api

import (
"log/slog"
"net/http"
"time"
)

func Fast(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(`fast`))
}

func Slow(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

for {
time.Sleep(time.Millisecond * 100)

go func() {
select {
case <-ctx.Done():
slog.Info(ctx.Err().Error(), slog.Int("routine", 1))
default:
slog.Info("all good", slog.Int("routine", 1))
}
}()
}
}

Test


Run service in a terminal tab and send a HTTP request to /slow endpoint, hit Ctrl+C key then observe logs.