Szerver
A server a framework HTTP-chassis-a: egy chi router a standard net/http fölött, a framework hibakezelésével, validációjával és telemetriájával bekötve, a közös életciklus alatt futtatva. A pozicionálása stdlib-first: a chi nem framework, hanem routing a net/http fölött. Minden middleware sima func(http.Handler) http.Handler, minden handler http.HandlerFunc-kompatibilis, így a Go-ökoszisztéma bármely net/http-eszköze (middleware-libek, httptest, profilerek) módosítás nélkül működik.
import "github.com/gp-system/framework/server"
Ez a chassis-a minden generált projektnek; az alkalmazáskód sosem beszél közvetlenül a net/http-vel, egyszer, a main-ben hívja meg a server.Run-t.
Config
A generált projektben a server.Config prefix nélkül ágyazódik az alkalmazás konfigjába, és az envconf.MustLoad tölti env-változókból:
type Config struct {
Server server.Config
DB dbx.Config `envPrefix:"DB_"`
// ...
}
A mezők és env-változóik:
| Mező | Env | Default | Mit szabályoz |
|---|---|---|---|
ListenAddr | LISTEN_ADDR | :3000 | a cím, amin a szerver hallgat |
ShutdownTimeout | SHUTDOWN_TIMEOUT | 10s | mennyi ideje van az in-flight kéréseknek SIGINT/SIGTERM után |
ReadHeaderTimeout | READ_HEADER_TIMEOUT | 5s | meddig küldheti a kliens a kérés fejléceit (Slowloris-védelem) |
ReadTimeout | READ_TIMEOUT | 30s | a teljes kérés (fejléc + body) beolvasásának kerete |
WriteTimeout | WRITE_TIMEOUT | 0s (kikapcsolva) | a válasz kiírásának kerete |
IdleTimeout | IDLE_TIMEOUT | 120s | meddig marad nyitva egy tétlen keep-alive kapcsolat |
CORSOrigins | CORS_ORIGINS | * | engedélyezett originok, vesszővel elválasztva |
CORSMethods | CORS_METHODS | GET,POST,PUT,PATCH,DELETE,OPTIONS | a CORS-preflightok által engedett metódusok |
CORSHeaders | CORS_HEADERS | Content-Type,Authorization | a CORS-preflightok által engedett kérés-fejlécek |
CORSAllowCredentials | CORS_ALLOW_CREDENTIALS | false | cookie-k/credentialök cross-origin kéréseken (wildcard originokkal együtt érvénytelen) |
TrustProxy | HTTP_TRUST_PROXY | false | a realip middleware: a RemoteAddr az X-Real-Ip/X-Forwarded-For-ból íródik át; csak olyan proxy mögött kapcsold be, ami felülírja ezeket a fejléceket |
AccessLog | HTTP_ACCESS_LOG | false | a strukturált access log (kérésenként egy rekord); telemetria dev módban ettől függetlenül mindig logol |
RequestTimeout | HTTP_REQUEST_TIMEOUT | 0s (kikapcsolva) | a timeout middleware: kérésenkénti context-határidő; alapból ki, mert a streamelő válaszokat is elvágná |
BodyLimit | BODY_LIMIT | 4194304 (4 MiB) | max kérés-body méret bájtban |
ExposeInternalErrors | HTTP_EXPOSE_INTERNAL_ERRORS | false | belső hibarészletek az 5xx válaszokban (csak dev!) |
Telemetry | (beágyazott) | nincs | telemetry.Config (OTel, logger, Sentry) |
A teljes env-referencia (a DB-, worker- és telemetria-változókkal együtt): Konfiguráció-referencia.
Timeoutok
A defaultok éles forgalomra vannak hangolva, nem demókra:
ReadHeaderTimeouta Slowloris-védelem első vonala: egy kliens, aki bájtonként csöpögteti a fejléceket, 5 másodperc után lekapcsolódik.ReadTimeouta teljes kérés beolvasását fedi.WriteTimeoutszándékosan0(kikapcsolva): a lassú vagy streamelő válaszokat (nagy letöltés, SSE) is elvágná. Kapcsold be, ha minden endpointod rövid életű.
Body limit: sosem kapcsolható ki véletlenül
const DefaultBodyLimit = 4 << 20 // 4 MiB
func (c Config) EffectiveBodyLimit() int
A server sosem a nyers BodyLimit mezőt olvassa, hanem az EffectiveBodyLimit()-et: egy 0 vagy negatív érték nem „korlátlant" jelent, hanem visszaesik a 4 MiB defaultra. Egy elgépelt vagy üresen hagyott BODY_LIMIT így nem tudja észrevétlenül levenni a sapkát. A limit túllépése 413-as problem-válasz.
HTTP_EXPOSE_INTERNAL_ERRORS
true értékkel az 5xx válaszok detail-je a mögöttes hibaüzenetet hordozza, és, ha a láncban errs hiba van, a válasz stack és chain extension membereket is kap. Szándékosan külön kapcsoló az OTEL_DEV_MODE-tól: a dev-logolás bekapcsolása így nem kezdhet el véletlenül belső részleteket szivárogtatni a klienseknek. Productionben hagyd false-on: a részletek ott a logba és a Sentry-be mennek, nem a válaszba.
A beágyazott telemetria
A Telemetry mező egy telemetry.Config: az OTel bootstrap, az alapértelmezett slog logger és az opcionális Sentry-integráció beállításai (OTEL_SERVICE_NAME, OTEL_DEV_MODE, LOG_LEVEL, SENTRY_DSN, ..., mind standard név, prefix nélkül). A server.Run ezt, a cfg.Log-gal együtt, átadja az app.Telemetry-nak, mielőtt bármi más elindulna. Részletek: Observability.
A közös életciklus
A server nem maga implementálja a graceful shutdownt: az engine-agnosztikus app csomagra delegál, ugyanarra, amire a worker és a realtime is épül. A folyamat minden gpsystem-processzben ugyanaz:
app.Telemetry: OTel, logger, Sentry.- Route-regisztráció (a te
registerfüggvényed). - Listen: a processz innentől blokkol.
SIGINT/SIGTERM-re: a szerver nem fogad új kérést, és aShutdownTimeout-on belül kivárja az in-flight kéréseket. Egy második signal azonnal öli a processzt.- A
WithCloser-rel regisztrált cleanupok LIFO sorrendben lefutnak (saját türelmi idővel, akkor is, ha a drain elfogyasztotta a keretet). - Utolsóként a telemetria flush-ol: a closerekben keletkező spanok így még exportálódnak.
A pontos sorrendet és a signal-kezelés részleteit az Alkalmazás-életciklus oldal írja le; ez az oldal azt mutatja, mit köt be a server.Run erre a közös vázra.
server.Config a teljes webszerver-konfigurációt hordozza: minden mező env-változóból töltődik be, típusosan, és a graceful shutdown nem egy külső process manager dolga, hanem a binárisé.Mit köt be a server.Run
func Run(ctx context.Context, cfg Config, register RegisterFunc, opts ...Option) error
func New(cfg Config, opts ...Option) *chi.Mux // tesztekhez, speciális esetekre
type RegisterFunc func(r chi.Router) error
A Run blokkol a kilépésig: telemetria bootstrap (app.Telemetry) → router felépítése → register(r) → http.Server + ListenAndServe → graceful shutdown SIGINT/SIGTERM-re. Tiszta leállásnál nil-t ad vissza. A New a teljesen bekötött routert adja vissza futtatás és telemetria-bootstrap nélkül: a httptest-tel párosítod (lásd Tesztelés).
A middleware-lánc egy nevesített, rendezett stack (server.Stack), a framework defaultjaival előre feltöltve. Minden bejegyzésnek neve van; a neveket a server.MW* konstansok írják le, így egy elgépelt stack-mutáció fordítási hiba. A default sorrend, legkülsőtől befelé:
| Név | Mikor aktív | Mit csinál |
|---|---|---|
requestid (MWRequestID) | mindig | minden kérésnek azonosítót ad: egy jól formált bejövő X-Request-Id megmarad, egyébként UUID generálódik; a válasz-fejlécen visszaadja, a httpmw.RequestIDFromContext-tel olvasható |
realip (MWRealIP) | csak HTTP_TRUST_PROXY=true mellett | a RemoteAddr-t az X-Real-Ip/X-Forwarded-For-ból (első hop) írja át, így a logok és a Sentry a kliens címét látják a proxyé helyett |
otel (MWOtel) | kivéve WithoutTelemetry mellett | kérésenkénti szerver-span + HTTP-metrikák (OpenTelemetry); a span a recovery-t és a logolást is körbeöleli, így egy elkapott 500 a spanre kerül, az access-log rekordok pedig trace-kontextust hordoznak |
sentryhub (MWSentryHub) | mindig | kérésenkénti izolált Sentry hub, hogy a breadcrumbök ne keveredjenek kérések között; no-op, ha a Sentry ki van kapcsolva |
recover (MWRecover) | mindig | egy panicból errs hiba lesz, aminek a stackje a panic helyére mutat, és 500-as problem-válaszként renderelődik (az http.ErrAbortHandler-t, a net/http flow-control szignálját, továbbengedi) |
validator (MWValidator) | csak WithValidator mellett | a WithValidator-ral adott validátort a request contextbe teszi, ahol a StrictValidator(nil) megtalálja (lásd lent) |
accesslog (MWAccessLog) | HTTP_ACCESS_LOG=true vagy OTEL_DEV_MODE=true mellett | kérésenként egy strukturált slog rekord: method, path, route (a chi route-pattern), status, bytes, duration, remote, request_id; 5xx error szinten; a recover-en belül ül, így egy panicoló kérés is logol (panic=true-val) |
cors (MWCORS) | mindig | a CORS_ORIGINS, CORS_METHODS, CORS_HEADERS és CORS_ALLOW_CREDENTIALS alapján |
bodylimit (MWBodyLimit) | mindig | a body-t http.MaxBytesReader-be csomagolja cfg.EffectiveBodyLimit()-tel: túllépésnél 413-as problem |
timeout (MWTimeout) | csak HTTP_REQUEST_TIMEOUT > 0 mellett | a beállított idő után megszakítja a request contextet, a handlerhez legközelebb |
Erre jönnek rá a WithMiddleware és WithStack extrák. Az auth név (MWAuth) az identity middleware-nek van fenntartva arra az esetre, ha egy projekt a teljes stackre mountolja; a framework alapból nem telepít bejegyzést alá (az add auth az api routeren köti be az identityt). A stack alatt a router NotFound/MethodNotAllowed kezelői a chi gyári plain-text hibái helyett 404/405 problem+json választ adnak; a mountolt subrouterek öröklik őket.
A beépített bejegyzések maguk a gpsystem/httpmw csomag exportált építőkockái (RequestID, RealIP, Otel, RequestHub, Recoverer, AccessLog, CORSAPI, ...), közösek a realtime gateway-jel, így egy saját stack újrahasznosíthatja vagy újracsomagolhatja őket.
Két viselkedési megjegyzés. Minden válasz X-Request-Id fejlécet hordoz, így egy support-ticket, egy log-rekord és egy trace ugyanarra a kérésre tud hivatkozni. A dev konzol kérés-logja pedig már nem a chi gyári Logger-e, hanem a fenti strukturált access log (telemetria dev módban mindig aktív): az OTel slog bridge-dzsel a rekordjai trace-korreláltak.
A server.Config timeoutjai a http.Server-be fordulnak (ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout).
Hibarenderelés: a net/http-ban nincs központi error handler
A beépített error hookkal rendelkező frameworkökkel szemben a net/http nem ad módot arra, hogy egy handler „visszaadjon" egy hibát a frameworknek: a handler maga írja meg a választ. A framework ezt három ponton hidalja át, hogy a drót-oldali viselkedés mindenhol egységes legyen:
- a generált szerverkód hibahookjai a
httperrnet/http íróira vannak kötve (WriteError,WriteBadRequest): a handleredből visszaadott hiba így a teljes mappingen megy át; - a router-szintű esetek (panic, 404, 405, body limit) a fenti táblázat szerint problemként renderelődnek;
- a kérés-validáció nem bind-time fut (a strict szerver
json.Decoder-rel dekódol), hanem aserver.StrictValidatorstrict middleware-ben (lásd lent).
A shop belépési pontja
A mintaalkalmazás HTTP-binárisa, amelynek minden sorát a generátor írta:
package main
import (
"context"
"log"
"github.com/go-chi/chi/v5"
"github.com/gp-system/dbx/pg"
"github.com/gp-system/envconf"
"github.com/gp-system/events/outbox"
"github.com/gp-system/framework/server"
"github.com/acme/shop/internal/modules/shop"
"github.com/acme/shop/internal/platform/config"
)
func main() {
cfg := envconf.MustLoad[config.Config]()
ctx := context.Background()
pool := pg.MustNewPool(ctx, cfg.DB)
err := server.Run(ctx, cfg.Server, func(r chi.Router) error {
api := chi.NewRouter()
api.Use(server.IdentityMiddleware(cfg.JWT))
// gpsystem:api-middleware
deps := shop.Dependencies{
DB: pg.NewDB(pool),
Transactor: pg.NewTransactor(pool),
Dispatcher: outbox.NewDispatcher(outbox.NewStore(pg.NewDB(pool))),
}
shop.RegisterApi(api, deps)
shop.RegisterAdmin(api, deps)
r.Mount("/api/v1", api)
return nil
},
// gpsystem:middleware
server.WithCloser("pgxpool", func(context.Context) error {
pool.Close()
return nil
}),
)
if err != nil {
log.Fatal(err)
}
}
A prefixelés Mount-tal történik: minden modul surface-ei egy friss subrouterre regisztrálnak, ami a /api/v1 alá mountolódik. A két anchor-komment middleware-varrat: a fölöttük álló api.Use(server.IdentityMiddleware(cfg.JWT)) sort az add auth szúrta be a gpsystem:api-middleware-nél, a gpsystem:middleware pedig azt jelöli, hova kerülnek a WithStack/WithMiddleware opciók az opciólistában.
Opciók
server.WithCloser("pgxpool", func(ctx context.Context) error { pool.Close(); return nil })
server.WithMiddleware(myRateLimiter) // ...func(http.Handler) http.Handler
server.WithStack(func(s *server.Stack) { // sebészet a nevesített láncon
s.InsertBefore(server.MWCORS, "tenant", tenantMW)
s.Remove(server.MWSentryHub)
})
server.WithHTTPServer(func(s *http.Server) { s.MaxHeaderBytes = 1 << 16 })
server.WithValidator(validate.New(validate.WithRegister(registerCustomTags)))
server.WithoutTelemetry() // tesztek / külső OTel setup
WithCloser(name, fn): cleanup graceful shutdownkor, miután a szerver már nem fogad kérést. A closerek LIFO sorrendben futnak; a telemetria utolsóként flush-ol, így egy closerben emittált span még exportálódik.WithMiddleware(...func(http.Handler) http.Handler): router-szintű middleware, a framework defaultjai után fűzve. A standard net/http middleware-forma: bármely ökoszisztéma-middleware ide passzol. Ami csak egy surface-re kell, azt a surfaceRoute-jára tedd (lásd lent).WithStack(func(*server.Stack)): minden, amit aWithMiddlewarenem tud: beszúrás egy nevesített default elé vagy mögé (s.InsertBefore(server.MWCORS, "tenant", tenantMW)), implementáció-csere (Replace), vagy egy default eldobása (s.Remove(server.MWSentryHub); egy nem létező név eltávolítása no-op, így a feltételes defaultok feltétel nélkül eltávolíthatók). A mutációk ismeretlen célra vagy duplikált névre panicelnek: egy bekötési hiba bootkor bukik el, nem kérés-időben. ANames()a lánc sorrendjét adja vissza, tesztekhez.WithHTTPServer(func(*http.Server)): escape hatch ahttp.Serverazon mezőihez, amiket a framework nem exponál (TLS,MaxHeaderBytes, ...). A framework által beállított mezőket is felülírhatod: a te mutációd fut utoljára.WithValidator(*validate.Validator): az alkalmazás-szintű request-validátor, tipikusan saját validációs tagekkel; aStrictValidator(nil)middleware-ek request-időben ezt oldják fel.WithoutTelemetry(): kihagyja azapp.Telemetry-t és az OTel middleware-t; tesztekhez, vagy ha a processz maga konfigurálja a telemetriát.
RegisterFunc és StrictValidator
Az add surface surface-enként egy Register<Surface> függvényt illeszt a modul register.go-jába. A shop api surface-ére szó szerint ez generálódik:
func RegisterApi(router chi.Router, deps Dependencies) {
apiSvc := apiservice.New()
apiHandler := apihttp.New(apiSvc)
apiPolicies := apipolicy.New()
router.Route("/shop", func(r chi.Router) {
// gpsystem:surface-middleware
shopapigen.HandlerWithOptions(shopapigen.NewStrictHandlerWithOptions(
apiHandler,
[]shopapigen.StrictMiddlewareFunc{
server.StrictValidator[shopapigen.StrictHandlerFunc](nil),
// Az utolsó a legkülső: az authorizáció a validáció előtt fut.
kitpolicy.Enforcer[shopapigen.StrictHandlerFunc](apiPolicies, shopapigen.PermissionByOperation, shopapigen.PolicyByOperation),
},
shopapigen.StrictHTTPServerOptions{
RequestErrorHandlerFunc: httperr.WriteBadRequest,
ResponseErrorHandlerFunc: server.WriteError,
},
), shopapigen.ChiServerOptions{
BaseRouter: r,
ErrorHandlerFunc: httperr.WriteBadRequest,
})
})
}
Sorról sorra:
- Service → handler → policy registry: explicit felépítés, container nélkül.
router.Route("/shop", ...): azapisurface a modul gyökerére kerül (/api/v1/shop/...); minden más surface a saját prefixe alá (/admin/shop).NewStrictHandlerWithOptions: a generált strict szerver, plusz a hibahookok: a request-dekódolási hibák (RequestErrorHandlerFunc,ErrorHandlerFunc)httperr.WriteBadRequest-en át 400-ként, a handlered hibái (ResponseErrorHandlerFunc)server.WriteError-on át renderelődnek, ami azauth/rbac/policysentineleket Problemre képezi, mindent mást pedig ahttperr.WriteError-ra delegál, a teljes mapping szerint.server.StrictValidator+kitpolicy.Enforcer: validáció és policy-érvényesítés strict middleware-ként; a lista utolsó eleme fut legkívül, ezért az enforcer megelőzi a validátort.
StrictValidator: validáció a handler előtt
func StrictValidator[H ~func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error)](
v *validate.Validator,
) func(f H, operationID string) H
A strict szerver json.Decoder-rel dekódol, validáció nélkül. A StrictValidator ezt a rést zárja: a teljesen bekötött request objektumot (paraméterek + body) validálja a framework validátorával, mielőtt a handlered futna; bukásnál 422 problem a ResponseErrorHandlerFunc-on át. A típusparaméter azért kell, mert az oapi-codegen a StrictHandlerFunc-ot generált csomagonként definiálja.
A validátor feloldása request-időben, ebben a sorrendben: az explicit v argumentum → a server.WithValidator-ral beállított (a contextből) → validate.New(). A generált kód ezért nil-t ad át: egyetlen WithValidator opció a main.go-ban az egész alkalmazásra érvényes, a generált fájlokhoz nem kell nyúlni.
Surface-szintű middleware
Router-szintű middleware-t a WithMiddleware/WithStack ad; ami csak egy surface-re vonatkozik (mint a shop admin surface-ének szerepvédelme), azt a surface Route-blokkjában kötöd be, a generátor által az első sorában hagyott // gpsystem:surface-middleware anchornál:
func RegisterAdmin(router chi.Router, deps Dependencies) {
adminSvc := adminservice.New()
adminHandler := adminhttp.New(adminSvc)
adminPolicies := adminpolicy.New()
router.Route("/admin/shop", func(r chi.Router) {
// gpsystem:surface-middleware
r.Use(server.RequireRole("admin"))
shopadmingen.HandlerWithOptions(shopadmingen.NewStrictHandlerWithOptions(
adminHandler,
[]shopadmingen.StrictMiddlewareFunc{
server.StrictValidator[shopadmingen.StrictHandlerFunc](nil),
kitpolicy.Enforcer[shopadmingen.StrictHandlerFunc](adminPolicies, shopadmingen.PermissionByOperation, shopadmingen.PolicyByOperation),
},
shopadmingen.StrictHTTPServerOptions{
RequestErrorHandlerFunc: httperr.WriteBadRequest,
ResponseErrorHandlerFunc: server.WriteError,
},
), shopadmingen.ChiServerOptions{
BaseRouter: r,
ErrorHandlerFunc: httperr.WriteBadRequest,
})
})
}
Az rbac.Identity ilyenkor már a contextben van: az add auth bekötötte a server.IdentityMiddleware-t az api routerre, így a server.RequireRole("admin") önmagában véd minden /api/v1/admin/shop/... route-ot (identity nélkül 401, szerep nélkül 403). Az api surface publikus route-jait nem érinti. add auth nélküli projektben elé a kötelező-token server.AuthMiddleware kerül, az auth.Config-ot a modul Dependencies-én átadva.
server.WithMiddleware/server.WithStack; surface-szintű (auth, szerep) → r.Use a surface Route-blokkjának gpsystem:surface-middleware anchoránál; művelet-szintű (permission, tulajdonos-ellenőrzés) → @permission/@policy a specben, az enforcer érvényesíti.Tesztelés
A New a teljesen bekötött routert adja vissza, amit sima net/http/httptest-tel tesztelhetsz:
r := server.New(server.Config{}, server.WithoutTelemetry())
r.Post("/things", createThing)
srv := httptest.NewServer(r)
defer srv.Close()
resp, err := http.Post(srv.URL+"/things", "application/json", body)
A panic-recoverer, a 404/405 problem-kezelők és a body limit ilyenkor is aktívak, tehát a teszt pontosan azt a problem+json választ látja, amit a kliens fog.
Használt patternek
- Context-injektált validator (
injectValidator/validatorFromContext, unexported context-kulccsal): Design patternek. - Generikus middleware-adapter (
StrictValidator[H ~func(...)], illeszkedik minden generáltStrictHandlerFunc-hoz): Design patternek.
Kapcsolódó oldalak
- Alkalmazás-életciklus: a közös
app.Runváz, amire aserver.Runépül. - Hibaválaszok: a teljes hiba-mapping tábla.
- Validáció: saját validációs tagek.
- Policy-k: a fent használt enforcer.