A kit

Szerver

A chi-alapú HTTP-chassis: Config (timeoutok, CORS, body limit), a közös életciklus, és hogy mit köt be a server.Run.

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:

internal/platform/config/config.go
type Config struct {
    Server server.Config
    DB     dbx.Config `envPrefix:"DB_"`
    // ...
}

A mezők és env-változóik:

MezőEnvDefaultMit szabályoz
ListenAddrLISTEN_ADDR:3000a cím, amin a szerver hallgat
ShutdownTimeoutSHUTDOWN_TIMEOUT10smennyi ideje van az in-flight kéréseknek SIGINT/SIGTERM után
ReadHeaderTimeoutREAD_HEADER_TIMEOUT5smeddig küldheti a kliens a kérés fejléceit (Slowloris-védelem)
ReadTimeoutREAD_TIMEOUT30sa teljes kérés (fejléc + body) beolvasásának kerete
WriteTimeoutWRITE_TIMEOUT0s (kikapcsolva)a válasz kiírásának kerete
IdleTimeoutIDLE_TIMEOUT120smeddig marad nyitva egy tétlen keep-alive kapcsolat
CORSOriginsCORS_ORIGINS*engedélyezett originok, vesszővel elválasztva
BodyLimitBODY_LIMIT4194304 (4 MiB)max kérés-body méret bájtban
ExposeInternalErrorsHTTP_EXPOSE_INTERNAL_ERRORSfalsebelső hibarészletek az 5xx válaszokban (csak dev!)
Telemetry(beágyazott)nincstelemetry.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:

  • ReadHeaderTimeout a Slowloris-védelem első vonala: egy kliens, aki bájtonként csöpögteti a fejléceket, 5 másodperc után lekapcsolódik.
  • ReadTimeout a teljes kérés beolvasását fedi.
  • WriteTimeout szándékosan 0 (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:

  1. telemetry.Setup: OTel, logger, Sentry.
  2. Route-regisztráció (a te register függvényed).
  3. Listen: a processz innentől blokkol.
  4. SIGINT/SIGTERM-re: a szerver nem fogad új kérést, és a ShutdownTimeout-on belül kivárja az in-flight kéréseket. Egy második signal azonnal öli a processzt.
  5. A WithCloser-rel regisztrált cleanupok LIFO sorrendben lefutnak (saját türelmi idővel, akkor is, ha a drain elfogyasztotta a keretet).
  6. 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.

A 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:

MiHonnanMit csinál
recovererkitegy 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-injektorkita WithValidator-ral adott validátort a request contextbe teszi, ahol a StrictValidator(nil) megtalálja (lásd lent)
OTel middlewareotelhttpkérésenkénti szerver-span + HTTP-metrikák: OpenTelemetry
Sentry request hubkitké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 loggerchi/middleware.Loggercsak OTEL_DEV_MODE=true mellett: kérés-log a konzolra
CORSgo-chi/corsCORS_ORIGINS-ból; GET/POST/PUT/PATCH/DELETE/OPTIONS, Content-Type + Authorization fejlécek
body limitchi/middleware.RequestSizea body-t http.MaxBytesReader-be csomagolja cfg.EffectiveBodyLimit()-tel: túllépésnél 413-as problem
NotFound / MethodNotAllowedkita 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 httperr net/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 a server.StrictValidator strict 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:

cmd/shop/main.go
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 surface Route-jára tedd (lásd lent).
  • WithHTTPServer(func(*http.Server)): escape hatch a http.Server azon 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; a StrictValidator(nil) middleware-ek request-időben ezt oldják fel.
  • WithoutTelemetry(): kihagyja a telemetry.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:

internal/modules/shop/register.go
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:

  1. Service → handler → policy registry: explicit felépítés, container nélkül.
  2. router.Route("/shop", ...): az api surface a modul gyökerére kerül (/api/v1/shop/...); minden más surface a saját prefixe alá (/admin/shop).
  3. 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 az auth/rbac/policy sentineleket Problemre képezi, mindent mást pedig a httperr.WriteError-ra delegál, a teljes mapping szerint.
  4. 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:

internal/modules/shop/register.go
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.

A middleware-szintek ökölszabálya: processz-szintű (rate limit, request ID) → 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ált StrictHandlerFunc-hoz): Design patternek.

Kapcsolódó oldalak

Copyright © 2026