CodeGym /Kursy /Go SELF /Middleware chaining i kolejność zastosowania

Middleware chaining i kolejność zastosowania

Go SELF
Poziom 62, Lekcja 0
Dostępny

1. Wprowadzenie

Jeśli piszesz serwer „na żywca”, to niemal od razu pojawiają się wymagania przekrojowe: dodać nagłówek do wszystkich odpowiedzi, logować każde żądanie, sprawdzać dostęp, ograniczać częstotliwość, łapać paniki. I nagle odkrywasz, że ten sam kod dubluje się w każdym handlerze. W pewnym momencie zmieniasz jedną regułę (na przykład format błędu) — i zaczynasz szukać „jeszcze 17 miejsc, gdzie to poprawić”.

Middleware rozwiązuje ten problem tak samo, jak dobry nawyk mycia rąk rozwiązuje problem „dlaczego znowu się przeziębiłem”: to nie magia, tylko systematyczność.

Kluczowa idea: handler zajmuje się logiką biznesową, a middleware zajmuje się polityką przekrojową wokół tej logiki.

Kontrakt net/http: http.Handler jako punkt opakowania

Zanim zaczniemy budować łańcuchy, warto jeszcze raz zobaczyć kontrakt, wokół którego będziemy się poruszać. W net/http obsługujący żądania to dowolny typ, który ma metodę ServeHTTP(w, r). Właśnie dlatego w Go tak łatwo pisać adaptery i opakowania: po prostu zwracasz inny http.Handler, który wewnątrz wywołuje następny. To ten sam „wzorzec adaptera”, tylko bez pompatycznych słów.

Przypomnijmy minimalny szkielet serwera (mocno uproszczony):

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

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

Prawie wszystko interesujące w dalszej architekturze serwera kręci się wokół tego, co dokładnie przekazujemy jako drugi argument do ListenAndServe.

Middleware jako funkcja-opakowanie

Kiedy po raz pierwszy słyszysz „middleware”, chcesz zapytać: „To biblioteka? To framework? To religia?” W rzeczywistości to po prostu funkcja, która bierze jeden handler i zwraca inny handler. Czyli dosłownie „opakowanie” wokół http.Handler.

Najpopularniejsza sygnatura wygląda tak:

func(next http.Handler) http.Handler

Aby kod czytał się łatwiej, zwykle wprowadza się nazwany typ:

package httpmw

import "net/http"

type Middleware func(http.Handler) http.Handler

Po tym każda middleware czyta się jak „przyjmij następny poziom (next) i zwróć nowy poziom”.

2. Mechanika middleware: „przed → next → po”

Prawie każda middleware wewnątrz wygląda jak „zrobić coś przed żądaniem”, potem przekazać sterowanie niżej i „zrobić coś po”. Wygodnie jest wyobrażać to sobie jak cebulę: zewnętrzna warstwa widzi żądanie jako pierwsza, a odpowiedź — jako ostatnia.

Zróbmy minimalną middleware, która dodaje nagłówek do wszystkich odpowiedzi. (Tak, to proste, ale idealne do zrozumienia mechaniki.)

package httpmw

import "net/http"

func WithHeader(name, value string) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set(name, value) // "przed"
			next.ServeHTTP(w, r)        // "przekazać niżej"
		})
	}
}

Zwróć uwagę: używamy http.HandlerFunc, ponieważ to adapter „funkcja jako handler”. Czyli nie musimy deklarować osobnego type X struct{} tylko po to, aby mieć ServeHTTP.

Short-circuit: middleware może zatrzymać żądanie

Początkujący często mają wewnętrzne przekonanie, że next.ServeHTTP(...) trzeba wywoływać zawsze, bo „jak to inaczej pójdzie dalej?”. Ale sens niektórych middleware polega właśnie na tym, żeby nie przepuszczać żądania dalej: sprawdzanie dostępu, limity, tryb maintenance.

Zróbmy przykład „serwera w trybie konserwacji”: jeśli flaga jest włączona — odpowiadamy od razu.

package httpmw

import "net/http"

func MaintenanceMode(enabled bool) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if enabled {
				w.WriteHeader(http.StatusServiceUnavailable)
				return // short-circuit: nie idziemy niżej
			}
			next.ServeHTTP(w, r)
		})
	}
}

Najważniejsze jest tu zrozumienie morału: middleware ma prawo zakończyć żądanie samodzielnie. To nie „hack”, tylko podstawowe narzędzie budowania polityk.

4. Łańcuch middleware i kolejność zastosowania

Gdy middleware jest więcej niż jedno, chcemy zastosować je wszystkie do jednego mux (albo do jednego handlera). I wtedy pojawia się ważne pytanie: w jakiej kolejności są stosowane?

Jeśli mamy:

  • A — middleware nr 1
  • B — middleware nr 2
  • C — middleware nr 3
  • H — finalny handler (na przykład mux)

to logiczna „cebula” zwykle wygląda tak:

A(B(C(H)))

Czyli A jest najbardziej zewnętrzną warstwą: widzi żądanie jako pierwsza i „odprowadza” odpowiedź jako ostatnia.

Do zrozumienia kolejności przydatny jest schemat:

flowchart TD
    R[HTTP request] --> A[Middleware A]
    A --> B[Middleware B]
    B --> C[Middleware C]
    C --> H["Final handler (mux)"]
    H --> C
    C --> B
    B --> A
    A --> W[HTTP response]

Tu ważny jest jeden szczegół: „tam” idziemy A → B → C → H, a „z powrotem” (gdy handler już zakończył pracę) — H → C → B → A. Dlatego kolejność middleware wpływa nawet na takie rzeczy, jak logowanie statusu czy przechwytywanie panic: zewnętrzna warstwa „widzi wszystko”, co wydarzyło się wewnątrz.

Funkcja Chain: składamy łańcuch w czytelnej kolejności

Teraz chcemy zrobić tak, aby kolejność była:

  • jasno widoczna w kodzie,
  • zawsze składana tak samo (a nie „tu tak, tam inaczej”),
  • nie wymagała ręcznego zagnieżdżania A(B(C(H))), bo to boli nawet przy samym patrzeniu.

Zróbmy helper Chain. Kluczowy trik: stosujemy middleware w odwrotnej kolejności, aby pierwsza na liście była najbardziej zewnętrzna.

package httpmw

import "net/http"

type Middleware func(http.Handler) http.Handler

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

I właśnie to miejsce jest „magią bez magii”: lista czyta się od lewej do prawej (jak jesteśmy przyzwyczajeni), ale jest stosowana od prawej do lewej, aby wynik był mws[0](mws[1](...h)).

Kto wykona się pierwszy: mini-tabela

Aby już całkiem się nie pogubić (a pogubicie się, to tradycja), weźcie prostą tabelę. Załóżmy:

h := Chain(mux, A, B, C)

Wtedy:

Element Kto widzi żądanie pierwszy Kto wywołuje next pierwszy Kto „zamyka” odpowiedź jako ostatni
A
tak tak tak
B
nie po
A
przed
A
C
nie po
B
przed
B
mux
nie najbardziej wewnętrzny najbardziej wewnętrzny

Jeśli czujesz lekki zawrót głowy — to normalne. Kolejność middleware najpierw przypomina „kable od słuchawek”: wydaje się, że nic nie robiłeś, a i tak wszystko się poplątało.

„Ręczna matrioszka” vs Chain

Żeby umieć czytać cudzy kod, warto zobaczyć oba warianty. Czasem trafisz na coś takiego:

h := A(B(C(mux)))

To jest uczciwe, proste i działa. Ale po tygodniu A(B(C(D(E(F(...))))) wygląda jak hasło do Wi‑Fi w kawiarni: niby znaki są, ale radości niewiele.

Z Chain to samo zamienia się w:

h := Chain(mux, A, B, C)

I to już czyta się jako „A na zewnątrz, potem B, potem C”. Właśnie tego chcemy.

5. Podłączamy chaining do aplikacji bez przepisywania serwera

Do tej pory przekazywaliśmy do ListenAndServe nasz mux. Teraz będziemy przekazywać opakowany mux. I to jest ważny architectural win: zmieniamy zachowanie całego serwera, nie ruszając samych endpointów.

Szkielet main.go (uproszczony, ale w istocie poprawny):

package main

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

	"example.com/taskapi/httpmw"
)

func main() {
	mux := http.NewServeMux()
	registerRoutes(mux)

	h := httpmw.Chain(mux,
		httpmw.WithHeader("Content-Type", "application/json; charset=utf-8"),
		httpmw.MaintenanceMode(false),
		httpmw.WithTimeoutHint(2*time.Second),
	)

	log.Fatal(http.ListenAndServe(":8080", h))
}

Tutaj registerRoutes(mux) to wasz zwyczajowy kod rejestracji endpointów (/api/v1/tasks, /api/v1/tasks/{id} itd.), którego nie chcemy ruszać. A WithTimeoutHint to kolejna prosta middleware, którą zaraz napiszemy, aby zobaczyć przykład „sparametryzowanego” opakowania.

Przykład middleware z parametrem: WithTimeoutHint

Chcemy pokazać middleware, która przyjmuje parametr i robi coś użytecznego, ale nie wymaga trudnych tematów. Nagłówki pasują idealnie.

Załóżmy, że chcemy dać klientowi „wskazówkę”: serwer oczekuje, że żądania będą szybkie (to nie jest prawdziwy timeout, tylko demonstracja „zachowania przekrojowego”).

package httpmw

import (
	"net/http"
	"time"
)

func WithTimeoutHint(d time.Duration) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("X-Timeout-Hint", d.String())
			next.ServeHTTP(w, r)
		})
	}
}

Zaletą tego przykładu jest to, że pokazuje „middleware jako fabrykę”: raz ustawiasz parametr podczas składania serwera, a potem dla każdego żądania działa ta sama reguła.

Dlaczego łańcuch lepiej składać w jednym miejscu

Gdy łańcuch middleware składa się w jednym miejscu (zwykle w main), dostajesz niemal darmowe korzyści.

Po pierwsze, kolejność można przeczytać wzrokiem jak umowę serwera: od góry do dołu wymienione są polityki, które obowiązują dla wszystkich żądań. Po drugie, jeśli coś zepsuło się w logice (na przykład nagle zniknęły nagłówki albo serwer przestał odpowiadać na część żądań), sprawdzasz jeden plik, a nie biegasz po dziesiątkach handlerów.

I wreszcie, to dyscyplinuje: jedna middleware — jedna odpowiedzialność. Gdy zaczynasz mieszać w jednym opakowaniu i nagłówki, i auth, i logowanie, i obsługę panic, wracasz do tej samej „kluski”, tylko w bardziej modnym sosie.

6. Niuans dotyczący Context: jak middleware przenosi dane w dół

Na razie nie będziemy zagłębiać się w przechowywanie danych w kontekście (to osobny duży temat), ale ważne jest, aby zapamiętać: middleware często nie tylko ustawia nagłówki, ale też dodaje coś do r.Context() i przekazuje dalej przez r.WithContext(...).

Dlaczego to w ogóle możliwe? Bo http.Request już zawiera Context, a standardowa biblioteka oczekuje, że będziesz respektować życie żądania przez kontekst (anulowanie, deadliny, metadane).

Na razie zapamiętajmy tylko myśl: jeśli middleware „utworzyła nowy kontekst”, trzeba go włożyć z powrotem do r, inaczej niższe warstwy nie zobaczą zmian.

7. Typowe błędy przy chainingu i kolejności middleware

Błąd nr 1: błędne oczekiwanie co do kolejności („dlaczego logowanie nie widzi tego, co chciałem?”).
Bardzo częste pomieszanie: wstawiłeś middleware jako pierwszą na liście, ale oczekujesz, że „zadziała po wszystkich”. W rzeczywistości pierwsza na liście zwykle jest najbardziej zewnętrzna: widzi żądanie pierwsza i kończy odpowiedź jako ostatnia. Jeśli chcesz „wykonać się bliżej handlera”, warstwa musi być głębiej (bardziej na prawo na liście).

Błąd nr 2: zapomniałeś wywołać next.ServeHTTP(w, r) tam, gdzie trzeba przepuścić żądanie dalej.
To klasyka: napisałeś middleware, sprawdziłeś jeden przypadek, wszystko wygląda dobrze. Potem okazuje się, że serwer odpowiada tylko 401/403/coś jeszcze — a „normalne” żądania po prostu nie przechodzą. Leczy się to dyscypliną: trzymać w głowie szkielet „przed → next → po” i jawnie sprawdzać wszystkie gałęzie return.

Błąd nr 3: przypadkowe podwójne wywołanie next.ServeHTTP.
Czasem początkujący próbują zrobić „ponowne wywołanie” albo błędnie wywołują next i w if, i po if. Efekt: handler zapisuje do ResponseWriter dwa razy, pojawiają się dziwne odpowiedzi, a czasem — paniki. Prawie zawsze middleware powinna wywołać next dokładnie raz (albo zero razy, jeśli short-circuit).

Błąd nr 4: zbyt późne zapisywanie nagłówków.
Nagłówki trzeba ustawić zanim ktoś niżej zacznie pisać ciało odpowiedzi albo wywoła WriteHeader. Jeśli wewnętrzny handler już zapisał body, to „zmienić nagłówek później” jest jak próba przepisania napisów końcowych po zakończeniu filmu w kinie: teoretycznie chcesz, praktycznie jest już za późno.

Błąd nr 5: składanie łańcucha „w różnych miejscach” i dryf kolejności.
Najpierw opakowujesz mux w jednym pliku, potem jakiś handler opakowuje inny handler „lokalnie”, potem w testach pojawia się trzeci schemat — i tracisz zrozumienie, co jest polityką globalną, a co lokalnym wyjątkiem. Dobra zasada: ogólna kolejność middleware jest ustalana raz na granicy aplikacji, a lokalne opakowania stosuje się tylko wtedy, gdy naprawdę dobrze wiesz, po co.

1
Zadanie
Go SELF, poziom 62, lekcja 0
Niedostępne
Nagłówek w odpowiedzi
Nagłówek w odpowiedzi
1
Zadanie
Go SELF, poziom 62, lekcja 0
Niedostępne
Łańcuch cebulowy
Łańcuch cebulowy
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION