server

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package server @notice Mounts a gqlgen handler on Fiber v3, correctly.

@dev "Correctly" is doing real work in that sentence. Every line of Mount exists because leaving it out fails silently: r.All rather than r.Post (or browser clients 405 on the CORS preflight, with nothing in the log), SetQueryCache (or every request re-parses and re-validates), extension.Introspection (or the playground's docs pane is blind).

This package cannot replace gqlgen codegen, and no package could. gqlgen generates code into your module: generated.NewExecutableSchema is a symbol only your own `go tool gqlgen generate` run produces, from your own schema.graphqls. So you still own gqlgen.yml, schema.graphqls, graph/resolver.go and the codegen step — see docs/gqlgen-contract.md, because getting it wrong produces runtime panics that `go build` does not catch.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(r fiber.Router, cfg Config)

Mount @notice Registers the same routes on a router you already have — an existing app, or a group.

@dev cfg.Fiber is ignored here: the app already exists, so its configuration is not this function's to set.

@param r a *fiber.App or the result of app.Group(prefix) @param cfg the server configuration; only Schema is required

func New

func New(cfg Config) *fiber.App

New @notice Builds a *fiber.App with the GraphQL endpoint and playground mounted.

@param cfg the server configuration; only Schema is required @return *fiber.App an app ready for Listen, with cfg.Fiber applied

Types

type Config

type Config struct {
	// Schema @notice Your generated executable schema, e.g.
	//   generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{DB: db}})
	Schema graphql.ExecutableSchema

	// Endpoint @notice The GraphQL path. Default "/graphql".
	Endpoint string

	// Playground @notice The GraphiQL path. Default "/".
	//
	// @dev Fiber matches it exactly, where net/http's Handle("/", ...) was a prefix match — an
	// unknown path is a plain 404 here.
	Playground string

	// DisablePlayground @notice Turns GraphiQL off entirely. Do this in production.
	//
	// @dev A separate bool rather than Playground == "": the zero Config must be the good
	// configuration, so "" has to mean "unset", which leaves no spelling for "off".
	DisablePlayground bool

	// PlaygroundTitle @notice The browser tab title. Default "graphql".
	PlaygroundTitle string

	// DisableIntrospection @notice Turns schema introspection off. Do this in production.
	//
	// @dev Zero value is "on", as with the playground — introspection is what makes the docs
	// pane work, so the zero Config has to keep it.
	//
	// This is defence in depth and a smaller attack surface; it is not confidentiality, and it
	// does not hide your field names. gqlparser's validator appends "Did you mean ...?" to
	// validation errors (gqlparser/validator/core/helpers.go), and PresentError passes
	// validation errors through by design, so a caller who guesses "nam" still learns there is
	// a "name". Do not treat it as a substitute for authorization.
	DisableIntrospection bool

	// RequestTimeout @notice Deadline for the whole request, propagated into the resolver
	// context. Default 15s. Negative disables it.
	//
	// @dev Zero means "unset", as with QueryCache. This is the only bound a query gets:
	// go-pg sets no ReadTimeout or WriteTimeout default, pg.ParseURL rejects statement_timeout
	// in the DSN, and nothing else in the stack imposes one. What makes a context deadline
	// sufficient is that go-pg honours it three ways — the pool wait selects on ctx.Done()
	// (internal/pool/pool.go waitTurn), the socket deadline is ctx.Deadline() verbatim when no
	// ReadTimeout is set (internal/pool/conn.go deadline), and ctx.Done() triggers a real
	// Postgres CancelRequest against the running backend (base.go withConn).
	//
	// 15s is deliberately under go-pg's own 30s PoolTimeout default (options.go init): below
	// it, a saturated pool sheds deterministic 408s; at or above it, the failure mode is a coin
	// flip between ErrPoolTimeout and the timeout. That CancelRequest is best-effort — it dials
	// a second connection and only logs a failure — so a server-authoritative bound still wants
	// statement_timeout via pg.Options.OnConnect. See SECURITY.md.
	RequestTimeout time.Duration

	// QueryCache @notice The parsed-query LRU size. Default 1000. Negative disables it.
	//
	// @dev Zero means "unset", not "off", because a zero-valued Config must not be the
	// pathological one: handler.New starts at graphql.NoCache, and with no cache every
	// request re-parses and re-validates the whole query document. There is no good reason to
	// pass a negative here.
	QueryCache int

	// ComplexityLimit @notice Caps query complexity. Default 1000. Negative disables it.
	//
	// @dev Zero means "unset", as with QueryCache.
	ComplexityLimit int

	// MaxDepth @notice Caps operation nesting depth. Default 15. Negative disables it.
	//
	// @dev Zero means "unset", as with QueryCache and ComplexityLimit. ComplexityLimit does not
	// cover this: a 40-level query costs about 40 against a limit of 1000, so a cyclic schema —
	// User.friends: [User!]! — passes it and multiplies into a resolver call per node per level.
	// gqlgen ships no depth limiter of its own.
	//
	// The walk resolves fragment spreads out of doc.Fragments. It has to: a spread node carries
	// no SelectionSet, so a walker that visits only Doc.Operations reads every fragment as a
	// leaf and a 40-deep document passes at depth 1 — measured, and a two-line change to the
	// attacking query. An inline fragment is a type condition and does not count as a level.
	//
	// 15 is chosen against the deepest document a default install serves, which is the
	// playground's own introspection query: it measures 13, so the default clears it by two.
	// TestMaxDepthAdmitsIntrospection pins that. If your schema legitimately nests deeper than
	// this, raise it — the number is a starting point, not a finding about your schema.
	MaxDepth int

	// ErrorPresenter @notice The error contract. Default [luimaerr.PresentError].
	ErrorPresenter graphql.ErrorPresenterFunc

	// HTTPMiddleware @notice net/http middleware wrapped around the gqlgen handler, outermost
	// first.
	//
	// @dev The one layer that has all three of: the real *http.Request with its cookies
	// parsed, an http.ResponseWriter whose headers survive back through the adaptor —
	// Set-Cookie included, because fasthttp's Header.Add routes it into the dedicated cookie
	// list rather than the generic map — and r.WithContext, which every resolver sees as its
	// own ctx, typed. Fiber middleware on the mounted group has none of the net/http shapes,
	// so anything written as func(http.Handler) http.Handler — request logging, tracing,
	// tenancy, rate limiting, a session layer — mounts here unchanged and stays portable to
	// chi, echo or plain net/http.
	//
	// The chain runs inside withFiberContext, so each middleware already sees the request
	// context that the resolvers will see: the RequestTimeout deadline is set, c.SetContext
	// values are attached, and whatever the middleware adds rides the same context down.
	// TestHTTPMiddleware pins all three properties.
	HTTPMiddleware []func(http.Handler) http.Handler

	// Configure @notice Runs against the built gqlgen server immediately before it is mounted.
	//
	// @dev One escape hatch rather than a field per knob: srv.Use, AroundOperations,
	// SetRecoverFunc, SetParserTokenLimit and SetDisableSuggestion are all reachable through
	// it, and a new gqlgen extension point needs no change here. Without it, srv is a local
	// that never escapes Mount, and a consumer wanting any of those has to abandon Mount —
	// which is the whole library.
	//
	// It runs after every default setter — query cache, introspection, complexity, depth,
	// presenter — so it can override them rather than be silently overridden by them.
	//
	// AddTransport is the exception, and it is the one that was broken: luima's own transports
	// are registered after this runs, so a transport registered here outranks them. Before
	// 0.3.0 they were registered first, and since gqlgen selects the first transport whose
	// Supports matches, a transport added here could never be selected — see the comment on
	// the AddTransport calls in Mount.
	Configure func(*handler.Server)

	// Fiber @notice Passed through to fiber.New. Ignored by Mount.
	//
	// @dev Fiber's BodyLimit defaults to 4 MB where net/http had none — irrelevant to
	// queries, and the thing to raise here the day you add the multipart transport. Read the
	// CSRF cost before you add that transport: multipart/form-data is a "simple" request, so no
	// preflight protects it and a cross-site HTML form can execute a mutation with the caller's
	// cookies attached (measured — gotcha #37).
	//
	// Setting Fiber.ErrorHandler does nothing useful for resolver errors: the adaptor always
	// returns nil, and so does the timeout middleware on both its normal and its timed-out
	// path, so no resolver error ever becomes a Fiber error.
	//
	// ErrorPresenter is the only contract for errors a *resolver* returns. Transport-level
	// failures — a malformed JSON body, an unsupported content type — are written by gqlgen's
	// transport before any executor exists, so they never reach the presenter and are not
	// redacted; a malformed body is echoed back in the message. Nothing sensitive of the
	// server's is in that path, but the claim that everything goes through the presenter is
	// not one to build on.
	Fiber fiber.Config
}

Config @notice Assembles the server.

@dev Only Schema is required; every zero value has a working default, so Config{Schema: ...} is the good configuration rather than the pathological one.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL