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 ¶
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
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.