Kebutuhan lintas handler muncul begitu aplikasi tumbuh: setiap request perlu dicatat, endpoint rahasia perlu diperiksa tokennya, dan panic handler tidak boleh menjatuhkan seluruh layanan. Menulis ulang logika tersebut di setiap handler jelas tidak masuk akal. Middleware menyelesaikannya dengan cara membungkus handler — satu fungsi menerima handler lama dan mengembalikan handler baru yang menambahkan perilaku. Pada chapter ini kita akan membangun middleware logging, recover, dan auth, lalu menyusunnya menjadi rantai berurutan yang diuji lewat httptest.

Middleware Adalah Fungsi yang Membungkus Handler #

Bentuk tipenya hanya satu baris:

go
type Middleware func(http.Handler) http.Handler

Sebuah middleware menerima handler next lalu mengembalikan handler baru — biasanya sebuah closure — yang menjalankan kode sebelum dan sesudah memanggil next.ServeHTTP(w, r):

go
func contohMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // kode sebelum handler inti
                next.ServeHTTP(w, r)
                // kode setelah handler inti
        })
}

Pemanggilan next.ServeHTTP(w, r) adalah titik penyerahan ke handler berikutnya. Kode sebelum pemanggilan tersebut berjalan dalam perjalanan masuk, sedangkan kode setelahnya berjalan dalam perjalanan keluar — persis seperti lapisan bawang.

Tiga Middleware Praktis #

Mencatat Log dengan statusRecorder #

Middleware logging ingin mencatat status code akhir, padahal http.ResponseWriter tidak menyediakan pengambil status. Solusinya: bungkus w dengan struct yang merekam setiap WriteHeader:

go
type statusRecorder struct {
        http.ResponseWriter
        status int
}

func (rec *statusRecorder) WriteHeader(code int) {
        rec.status = code
        rec.ResponseWriter.WriteHeader(code)
}

Struct statusRecorder menempelkan http.ResponseWriter secara anonymous embedding, lalu menimpa method WriteHeader agar status tersimpan sebelum diteruskan ke w yang asli. Middleware logging kemudian mencetak baris seperti log: GET /items -> 200 setelah handler selesai.

Menangkap Panic dengan recover #

Middleware recover membungkus handler dalam defer berisi recover():

go
defer func() {
        if err := recover(); err != nil {
                http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
                        http.StatusInternalServerError)
        }
}()

Panic dari handler ditangkap di sini, lalu klien menerima respons 500 yang terkontrol. Server net/http sebenarnya sudah menangkap panic bawaan per koneksi — request lain tidak ikut mati — tetapi tangkapan bawaan memutus koneksi tanpa respons; middleware recover memberi Anda kendali penuh atas bentuk respons gagal.

Memeriksa Token di authMiddleware #

Middleware auth memeriksa header Authorization sebelum meneruskan request. Token yang tidak cocok dijawab 401 tanpa perlu handler inti ikut terpanggil:

go
if r.Header.Get("Authorization") != "Bearer token-demo" {
        http.Error(w, "token tidak valid", http.StatusUnauthorized)
        return
}

Urutan Rantai Itu Penting #

Penyusunan rantai dilakukan dari dalam ke luar: handler inti dibungkus middleware terakhir, lalu hasilnya dibungkus middleware sebelumnya, demikian seterusnya:

go
func chain(h http.Handler, mws ...Middleware) http.Handler {
        for i := len(mws) - 1; i >= 0; i-- {
                h = mws[i](h)
        }
        return h
}

Hasil pemanggilan chain(final, loggingMiddleware, recoverMiddleware, authMiddleware) berlapis seperti diagram berikut:

Logging berada paling luar agar baris log mencatat status akhir — termasuk 401 dari auth dan 500 dari recover. Recover berada di dalam logging, sehingga panic yang ditangkap tetap menghasilkan respons 500 yang tercatat, bukan koneksi putus tanpa jejak. Menukar urutan kedua lapisan tersebut membuat panic melewati logging sebelum tertangkap, dan baris log pun hilang. Pada aplikasi nyata, lapisan serupa juga menyetor request ID unik ke header atau context agar satu request bisa ditelusuri lintas layanan.

Program Utuh: Rantai Teruji #

Berkas middleware-demo.go pada bagian bawah halaman membangun ketiga middleware di atas, menyusunnya dengan chain, lalu menguji tiga skenario: request sah, request tanpa token, dan request yang memicu panic.

go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

// Middleware membungkus satu handler menjadi handler baru.
type Middleware func(http.Handler) http.Handler

// statusRecorder membungkus ResponseWriter untuk merekam status code.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (rec *statusRecorder) WriteHeader(code int) {
	rec.status = code
	rec.ResponseWriter.WriteHeader(code)
}

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)
		fmt.Printf("log: %s %s -> %d\n", r.Method, r.URL.Path, rec.status)
	})
}

func recoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
					http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") != "Bearer token-demo" {
			http.Error(w, "token tidak valid", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// chain menyusun middleware: m0 membungkus m1, m1 membungkus m2, dst.
func chain(h http.Handler, mws ...Middleware) http.Handler {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}

func main() {
	final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/ledak" {
			panic("boom dari handler")
		}
		fmt.Fprintln(w, "selamat datang di area terproteksi")
	})

	// Urutan bungkus: logging -> recover -> auth -> final.
	app := chain(final, loggingMiddleware, recoverMiddleware, authMiddleware)

	uji(app, "GET", "/items", "dengan token")
	uji(app, "GET", "/items", "tanpa token")
	uji(app, "GET", "/ledak", "dengan token")
}

func uji(h http.Handler, method, path, keterangan string) {
	fmt.Printf("== %s %s (%s)\n", method, path, keterangan)
	req := httptest.NewRequest(method, path, nil)
	if keterangan == "dengan token" {
		req.Header.Set("Authorization", "Bearer token-demo")
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	fmt.Printf("   status: %d | body: %s\n", rec.Code, strings.TrimSpace(rec.Body.String()))
}

Jalankan berkas tersebut dengan perintah berikut dari direktori chapter:

bash
go run middleware-demo.go

Output yang Dihasilkan:

text
== GET /items (dengan token)
log: GET /items -> 200
   status: 200 | body: selamat datang di area terproteksi
== GET /items (tanpa token)
log: GET /items -> 401
   status: 401 | body: token tidak valid
== GET /ledak (dengan token)
log: GET /ledak -> 500
   status: 500 | body: kesalahan internal: boom dari handler

Mari kita telaah baris demi baris program di atas:

  1. Baris 11 (type Middleware ...): tipe fungsi yang menjadi kontrak seluruh middleware — menerima handler, mengembalikan handler.
  2. Baris 14-22 (type statusRecorder ... dst.): pembungkus ResponseWriter yang menyimpan status lewat penimpaan method WriteHeader.
  3. Baris 24-30 (func loggingMiddleware(...)): memanggil handler inti dengan statusRecorder, lalu mencetak method, path, dan status akhir.
  4. Baris 32-42 (func recoverMiddleware(...)): menangkap panic lewat defer recover dan mengubahnya menjadi respons 500.
  5. Baris 44-52 (func authMiddleware(...)): menolak request tanpa token sah menggunakan respons 401 sebelum handler inti terpanggil.
  6. Baris 55-60 (func chain(...)): menyusun rantai dari dalam ke luar; iterasi mundur memastikan middleware pertama menjadi lapisan terluar.
  7. Baris 63-68 (final := ...): handler inti yang menyapa, sekaligus sengaja panic pada path /ledak untuk menguji recover.
  8. Baris 71 (app := chain(...)): memaketkan urutan logging -> recover -> auth -> final menjadi satu handler aplikasi.
  9. Baris 73-75 (uji(app, ...)): tiga skenario uji; perhatikan bahwa baris log: tercetak sebelum baris status: pada setiap skenario karena logging berjalan paling luar.

Middleware yang Anda bangun di sini adalah versi mini dari pola produksi. Proyek Web Server menyusun middleware serupa untuk logging terstruktur, autentikasi, dan penanganan error secara lebih lengkap, dan pengaturan timeout klien serta server menyambung pembahasan pada chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

middleware-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

// Middleware membungkus satu handler menjadi handler baru.
type Middleware func(http.Handler) http.Handler

// statusRecorder membungkus ResponseWriter untuk merekam status code.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (rec *statusRecorder) WriteHeader(code int) {
	rec.status = code
	rec.ResponseWriter.WriteHeader(code)
}

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)
		fmt.Printf("log: %s %s -> %d\n", r.Method, r.URL.Path, rec.status)
	})
}

func recoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				http.Error(w, fmt.Sprintf("kesalahan internal: %v", err),
					http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") != "Bearer token-demo" {
			http.Error(w, "token tidak valid", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// chain menyusun middleware: m0 membungkus m1, m1 membungkus m2, dst.
func chain(h http.Handler, mws ...Middleware) http.Handler {
	for i := len(mws) - 1; i >= 0; i-- {
		h = mws[i](h)
	}
	return h
}

func main() {
	final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/ledak" {
			panic("boom dari handler")
		}
		fmt.Fprintln(w, "selamat datang di area terproteksi")
	})

	// Urutan bungkus: logging -> recover -> auth -> final.
	app := chain(final, loggingMiddleware, recoverMiddleware, authMiddleware)

	uji(app, "GET", "/items", "dengan token")
	uji(app, "GET", "/items", "tanpa token")
	uji(app, "GET", "/ledak", "dengan token")
}

func uji(h http.Handler, method, path, keterangan string) {
	fmt.Printf("== %s %s (%s)\n", method, path, keterangan)
	req := httptest.NewRequest(method, path, nil)
	if keterangan == "dengan token" {
		req.Header.Set("Authorization", "Bearer token-demo")
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	fmt.Printf("   status: %d | body: %s\n", rec.Code, strings.TrimSpace(rec.Body.String()))
}