Szerver
A server a kit HTTP-chassis-a: egy chi router a standard net/http fölött, a kit 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/gpsystem/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 |
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 ebből hívja a telemetry.Setup-ot, 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:
telemetry.Setup: 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 → 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 és a router-szintű kezelők, ebben a sorrendben:
| Mi | Honnan | Mit csinál |
|---|---|---|
| recoverer | kit | 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) |
| validátor-injektor | kit | a WithValidator-ral adott validátort a request contextbe teszi, ahol a StrictValidator(nil) megtalálja (lásd lent) |
| OTel middleware | otelhttp | kérésenkénti szerver-span + HTTP-metrikák: OpenTelemetry |
| Sentry request hub | kit | 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 |
| dev logger | chi/middleware.Logger | csak OTEL_DEV_MODE=true mellett: kérés-log a konzolra |
| CORS | go-chi/cors | CORS_ORIGINS-ból; GET/POST/PUT/PATCH/DELETE/OPTIONS, Content-Type + Authorization fejlécek |
| body limit | chi/middleware.RequestSize | a body-t http.MaxBytesReader-be csomagolja cfg.EffectiveBodyLimit()-tel: túllépésnél 413-as problem |
NotFound / MethodNotAllowed | kit | a chi gyári plain-text hibái helyett 404/405 problem+json; a mountolt subrouterek öröklik |
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 kit 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/gpsystem/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()
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
}, 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.
Opciók
server.WithCloser("pgxpool", func(ctx context.Context) error { pool.Close(); return nil })
server.WithMiddleware(myRateLimiter, myRequestID) // ...func(http.Handler) http.Handler
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 kit defaultjai után. 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).WithHTTPServer(func(*http.Server)): escape hatch ahttp.Serverazon mezőihez, amiket a kit nem exponál (TLS,MaxHeaderBytes, ...). A kit á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 atelemetry.Setup-ot é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) {
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 kit 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 ad; ami csak egy surface-re vonatkozik (mint a shop admin surface-ének JWT-védelme), azt a surface Route-blokkjában kötöd be a kit net/http-s auth middleware-párjával, a server.AuthMiddleware-rel és a server.RequireRole-lal:
import (
"github.com/gp-system/auth"
// ...
)
type Dependencies struct {
DB *pg.DB
Transactor dbx.Transactor
Auth auth.Config // JWT_SECRET, JWT_ISSUER, ..., a platform-configból
// gpsystem:dependencies
}
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) {
r.Use(server.AuthMiddleware(deps.Auth), 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,
})
})
}
A server.AuthMiddleware a Bearer tokent parseolja és rbac.Identity-t tesz a contextbe; a server.RequireRole("admin") e nélkül a szerep nélkül 403-as problemmel rövidre zár. Minden /api/v1/admin/shop/... route védett. Az api surface publikus route-jait nem érinti.
server.WithMiddleware; surface-szintű (auth, szerep) → r.Use a surface Route-blokkjában; 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.