HTTP & Routing

The server core

The engine-neutral server.Config (timeouts, CORS, body limit) and the shared lifecycle both engines build on.

The server package is the engine-neutral core of the HTTP layer: a single Config struct consumed identically by both engines, Fiber (server/fiberx) and chi (server/chix). It has no HTTP-framework dependency; application code never uses it directly: it picks one of the engines and embeds server.Config into its own config struct.

import "github.com/gp-system/gpsystem/server"

This split is what makes the engine choice a free one: config, environment variables, error rendering and lifecycle are identical across the two engines. What differs is the engine's native API (*fiber.App vs chi.Router). Operationally, a project written on Fiber behaves exactly like one on chi.

Config

In a generated project, server.Config is embedded without a prefix in the application config and loaded from environment variables by envconf.MustLoad:

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

The fields and their environment variables:

FieldEnvDefaultWhat it controls
ListenAddrLISTEN_ADDR:3000the address the engine listens on
ShutdownTimeoutSHUTDOWN_TIMEOUT10show long in-flight requests get after SIGINT/SIGTERM
ReadHeaderTimeoutREAD_HEADER_TIMEOUT5show long a client may take to send request headers (net/http only, see below)
ReadTimeoutREAD_TIMEOUT30sbudget for reading the whole request (headers + body)
WriteTimeoutWRITE_TIMEOUT0s (disabled)budget for writing the response
IdleTimeoutIDLE_TIMEOUT120show long an idle keep-alive connection stays open
CORSOriginsCORS_ORIGINS*allowed origins, comma-separated
BodyLimitBODY_LIMIT4194304 (4 MiB)max request body size in bytes
ExposeInternalErrorsHTTP_EXPOSE_INTERNAL_ERRORSfalseinternal error details in 5xx responses (dev only!)
Telemetry(embedded)nonetelemetry.Config (OTel, logger, Sentry)

The full env reference (including the DB, worker and telemetry variables): Configuration reference.

Timeouts

The defaults are tuned for production traffic, not demos:

  • ReadHeaderTimeout is the first line of Slowloris defense: a client that drips headers byte by byte gets disconnected after 5 seconds. Only the net/http chassis (chi) uses it as a separate field; fasthttp underneath Fiber folds it into ReadTimeout.
  • ReadTimeout covers reading the whole request. On Fiber it additionally closes idle keep-alive connections on graceful shutdown: with a zero value, shutdown would block for the full ShutdownTimeout.
  • WriteTimeout is deliberately 0 (disabled): it would also cut off slow or streaming responses (large downloads, SSE). Enable it when all your endpoints are short-lived.

Body limit: never accidentally disabled

const DefaultBodyLimit = 4 << 20 // 4 MiB

func (c Config) EffectiveBodyLimit() int

The engines never read the raw BodyLimit field; they call EffectiveBodyLimit(): a 0 or negative value does not mean "unlimited"; it falls back to the 4 MiB default. A typo'd or empty BODY_LIMIT therefore cannot silently remove the cap. Exceeding the limit produces a 413 problem response on both engines.

HTTP_EXPOSE_INTERNAL_ERRORS

With true, 5xx responses carry the underlying error message in detail, and, when the chain holds an errs error, the response also gains stack and chain extension members. It is deliberately a separate switch from OTEL_DEV_MODE: turning on dev logging can never accidentally start leaking internals to clients. Keep it false in production: details go to the log and Sentry there, not into the response.

The embedded telemetry config

The Telemetry field is a telemetry.Config: settings for the OTel bootstrap, the default slog logger and the optional Sentry integration (OTEL_SERVICE_NAME, OTEL_DEV_MODE, LOG_LEVEL, SENTRY_DSN, ..., all standard names, no prefix). The engine's Run calls telemetry.Setup from it before anything else starts. Details: Observability.

The shared lifecycle

The engines do not implement graceful shutdown themselves: both delegate to the engine-agnostic app package. The flow is the same in every gpsystem process (HTTP server and worker):

  1. telemetry.Setup: OTel, logger, Sentry.
  2. Route registration (your register function).
  3. Listen: the process blocks from here.
  4. On SIGINT/SIGTERM: the engine stops accepting requests and drains in-flight ones within ShutdownTimeout. A second signal kills the process immediately.
  5. Cleanups registered with WithCloser run in LIFO order (with their own grace period, even when the drain consumed the budget).
  6. Telemetry flushes last: spans emitted by closers still get exported.

The exact ordering, signal-handling details and the worker-side counterpart are described on Application lifecycle.

server.Config carries the full web-server configuration: every field is loaded from an environment variable, typed, and graceful shutdown is the binary's job, not an external process manager's.

Choose your engine

The two engines are equal peers: you choose at project creation (--engine fiber is the default, --engine chi the alternative), and the generators follow that choice throughout:

  • Fiber (server/fiberx): Fiber v3 on top of fasthttp, with a central error handler, bind-time validation, and an expressive router API.
  • chi (server/chix): thin routing over the standard net/http, where every middleware is a plain func(http.Handler) http.Handler and every handler is stdlib-compatible. Pick this if you want every net/http tool in the Go ecosystem to work unmodified.

Your business code is untouched by the choice: the generated handler interface is identical on both engines, and the service and repository layers never see HTTP at all.

Projects generated against the old single server package keep working unchanged. To adopt the new layout, in cmd/<module>/main.go switch the import from server to server/fiberx and the server.Run/server.WithCloser calls to fiberx.Run/fiberx.WithCloser. The embedded server.Config stays as is.
Copyright © 2026