Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Videos & KonferenzenTwo Minute Papers: Claude Opus 5.5 AI: A Massive Leap Forward(24.09.2026 um 10:40 Uhr)
Sicherheitslücken (CVE)USN-8805-1: Moodle vulnerability(23.09.2026 um 16:43 Uhr)
Sichere ProgrammierungI thought clipboard sync would be simple. Android had other plans.(24.09.2026 um 11:01 Uhr)
Sichere ProgrammierungAI-assisted genealogy, a follow-up(24.09.2026 um 11:02 Uhr)
Sicherheitslücken (CVE)Smart Contract Vulnerability Surface Analysis: HashKey Exchange(24.09.2026 um 11:02 Uhr)
Sichere ProgrammierungAI Agents Calling Your Existing Backend Without MCP Development(24.09.2026 um 11:06 Uhr)
Videos & KonferenzenTwo Minute Papers: Claude Opus 5.5 AI: A Massive Leap Forward(24.09.2026 um 10:40 Uhr)
Sicherheitslücken (CVE)USN-8805-1: Moodle vulnerability(23.09.2026 um 16:43 Uhr)
Sichere ProgrammierungI thought clipboard sync would be simple. Android had other plans.(24.09.2026 um 11:01 Uhr)
Sichere ProgrammierungAI-assisted genealogy, a follow-up(24.09.2026 um 11:02 Uhr)
Sicherheitslücken (CVE)Smart Contract Vulnerability Surface Analysis: HashKey Exchange(24.09.2026 um 11:02 Uhr)
Sichere ProgrammierungAI Agents Calling Your Existing Backend Without MCP Development(24.09.2026 um 11:06 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building a net/http Middleware Chain From Scratch in Go

Every Go web framework ships with middleware. Chi has it. Gin has it. Echo has it. And every tutorial shows you how to use it. But almost none of them show you how it actually works. This article builds a production-grade middleware…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Every Go web framework ships with middleware. Chi has it. Gin has it. Echo has it. And every tutorial shows you how to use it.



But almost none of them show you how it actually works.



This article builds a production-grade middleware chain using nothing but the Go standard library. No framework, no magic. By the end you will have written request logging, authentication, panic recovery, and timeout middleware from scratch, and you will understand exactly why the pattern works the way it does.









The Foundation: What Is http.Handler?



Everything in Go's HTTP world revolves around one interface:




type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}






That's it. One method. Any type that implements ServeHTTP is an HTTP handler. This simplicity is intentional, and it's what makes middleware composable.



There's also http.HandlerFunc, a function type that implements Handler:




type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}






This adapter lets you write a plain function and treat it as a Handler. You'll use this constantly.









The Middleware Signature



A middleware is a function that takes a Handler and returns a Handler:




type Middleware func(http.Handler) http.Handler






That's the whole pattern. Middleware wraps a handler, intercepts the request, does something before or after calling the next handler, and passes control along.



Here's the skeleton every middleware follows:




func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do something before
next.ServeHTTP(w, r)
// do something after
})
}






The next handler is the thing being wrapped. You call it explicitly. This is the key insight: Go middleware is not magic. It's just nested function calls, composed at setup time.









Building the Chain



Managing middleware manually gets messy fast:




// This is readable but doesn't scale
handler := LoggingMiddleware(AuthMiddleware(RecoveryMiddleware(myHandler)))






Let's write a proper Chain that takes a slice of middleware and applies them in order:




package middleware

import "net/http"

type Middleware func(http.Handler) http.Handler

func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}






The reverse iteration matters: if you want request logging to run first (outermost), it needs to wrap everything else. Applying in reverse order ensures the first middleware in your list is the first one that sees the request.



Usage:




chain := middleware.Chain(
RecoveryMiddleware,
LoggingMiddleware,
AuthMiddleware,
)

http.Handle("/api/", chain(myAPIHandler))






Clean, ordered, easy to reason about.









Middleware 1: Request Logging



The most common middleware. We want to log the method, path, status code, and duration of every request.



The tricky part: http.ResponseWriter doesn't expose the status code after it's been written. We need to wrap it:




package middleware

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

type responseWriter struct {
http.ResponseWriter
status int
written bool
}

func (rw *responseWriter) WriteHeader(code int) {
if !rw.written {
rw.status = code
rw.written = true
rw.ResponseWriter.WriteHeader(code)
}
}

func (rw *responseWriter) statusCode() int {
if !rw.written {
return http.StatusOK // default if WriteHeader was never called
}
return rw.status
}

func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

wrapped := &responseWriter{ResponseWriter: w}
next.ServeHTTP(wrapped, r)

slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.statusCode(),
"duration", time.Since(start),
"remote_addr", r.RemoteAddr,
)
})
}






A few things worth noting here:




  • We embed http.ResponseWriter in our wrapper struct so all other methods (Header(), Write(), etc.) delegate to the original automatically.

  • We guard against double calls to WriteHeader, which handlers can do accidentally.

  • We use log/slog (Go 1.21+) for structured logging. If you're on an older version, swap in your preferred logger.









Middleware 2: Panic Recovery



A panic in a goroutine serving a request will crash the entire server if not recovered. This middleware catches panics and turns them into 500 responses:




package middleware

import (
"log/slog"
"net/http"
"runtime/debug"
)

func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered",
"error", err,
"stack", string(debug.Stack()),
)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}






debug.Stack() gives you the full goroutine stack trace at the point of the panic, which is invaluable for debugging production incidents.



Put RecoveryMiddleware first (outermost) in your chain. It needs to wrap everything so nothing escapes.









Middleware 3: Authentication



This one reads a Bearer token, validates it, and puts the claims into the request context so downstream handlers can read them without re-doing the work.




package middleware

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

type contextKey string

const UserIDKey contextKey = "userID"

func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
if !strings.HasPrefix(header, "Bearer ") {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

token := strings.TrimPrefix(header, "Bearer ")
userID, err := validateToken(token) // your validation logic
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

// Reading it in a handler
func myHandler(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(UserIDKey).(string)
if !ok {
http.Error(w, "missing user", http.StatusInternalServerError)
return
}
// use userID
}






A few design choices worth explaining:



Why a custom contextKey type? Context keys are compared by value and type. Using a plain string like "userID" means any package could accidentally read or overwrite your value. A private contextKey type scoped to your package makes collisions impossible.



Why return early on auth failure? The middleware should short-circuit: if auth fails, call http.Error and return without calling next.ServeHTTP. Forgetting the return after the error write is a classic bug that leads to handlers running even when auth failed.









Middleware 4: Request Timeout



Long-running requests can exhaust your server's connection pool. This middleware cancels the request context after a deadline:




package middleware

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

func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()

done := make(chan struct{})
go func() {
next.ServeHTTP(w, r.WithContext(ctx))
close(done)
}()

select {
case <-done:
// handler finished in time
case <-ctx.Done():
http.Error(w, "request timeout", http.StatusGatewayTimeout)
}
})
}
}






Note that TimeoutMiddleware takes a time.Duration and returns a Middleware. This is the factory pattern: when a middleware needs configuration, wrap it in a function that accepts that config and returns the actual middleware. Clean and composable.



Usage:




chain := middleware.Chain(
RecoveryMiddleware,
LoggingMiddleware,
middleware.TimeoutMiddleware(5*time.Second),
AuthMiddleware,
)












Putting It All Together



Here's a full, working example:




package main

import (
"fmt"
"net/http"
"time"

"yourmodule/middleware"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
fmt.Fprintf(w, "Hello, %s\n", userID)
}

func main() {
chain := middleware.Chain(
middleware.RecoveryMiddleware,
middleware.LoggingMiddleware,
middleware.TimeoutMiddleware(5*time.Second),
middleware.AuthMiddleware,
)

mux := http.NewServeMux()
mux.Handle("/hello", chain(http.HandlerFunc(helloHandler)))

http.ListenAndServe(":8080", mux)
}






The execution order for a request to /hello:




RecoveryMiddleware (enter)
LoggingMiddleware (enter, start timer)
TimeoutMiddleware (enter, start context)
AuthMiddleware (enter, validate token)
helloHandler (run)
AuthMiddleware (exit)
TimeoutMiddleware (exit)
LoggingMiddleware (exit, log result)
RecoveryMiddleware (exit)






Each middleware wraps the next. The chain is just deeply nested function calls, resolved at request time.









When to Reach for a Framework



This pattern scales well. But there are cases where the standard library genuinely falls short:





  • Path parameters: net/http's ServeMux gained basic wildcard support in Go 1.22 (/users/{id}), but complex routing (regex, optional segments) still requires a router like Chi or httprouter.


  • Middleware on route groups: applying middleware to /api/ but not /healthz requires careful mux setup or a router that supports route groups natively.



For most APIs, the standard library plus a lightweight router like Chi (which uses this exact middleware pattern under the hood) is the right call.









What You Built



From scratch, using only the standard library:




  • A reusable Chain function for composing middleware in order

  • Structured request logging with response code capture

  • Panic recovery with stack traces

  • Token authentication with context propagation

  • Configurable request timeouts



And more importantly: you now know what a framework's middleware stack actually is. It's this. Just wrapped in a nicer API.






If this was useful, the obvious next step is reading the source of Chi's middleware package. It follows exactly this pattern and is clean, readable Go. Link in the tags above.

SOC Incident Playbook: Remote Code Execution (RCE) Defense
title: Detect Exploitation - Building a net/http Middleware Chain From Scratch in Go
id: 77cc6c81-1b1a-4e43-b5e6-47b2b93bc8ec
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-24
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-24"
        description = "YARA Signature for "
    strings:
        $str = "Building a net/http Middleware" ascii wide
    condition:
        any of them
}
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich Building a net/http Middleware Chain Fro.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a net/http Middleware Chain From Scratch in Go

Thematisch verwandte Begriffe: Building, nethttp, Middleware, Chain · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-97056 | SigNoz versions from v0.98.0 up to (but not including) v0.143.0, when co…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick