Documentation
¶
Overview ¶
Package luimaerr @notice luima's error contract: the one place that decides what a resolver error is allowed to tell a client.
@dev It is named luimaerr rather than errors because a package called errors shadows the standard library in every file that imports both — and PresentError itself calls errors.As twice.
It imports nothing else in luima, so a package that must not pull in Fiber or gqlgen's handler can still return a *CustomError.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func PresentError ¶
PresentError @notice The server's error contract, and Config.ErrorPresenter's default.
@dev gqlgen's default presenter forwards err.Error() verbatim. That would hand an unauthenticated client raw driver strings ("... SQLSTATE 23505") and with them the table's column and constraint names. luima ships no auth, so this redaction is the only thing between a caller and the schema.
Resolvers have to opt in to being heard: return a bare errors.New("user already exists") and the client sees "internal server error". That is the design, and it is most of why the CRUD helpers in the crud package exist — they do the classification so a resolver cannot forget it.
It is not, however, the only path to the wire. A transport-level failure — a malformed JSON body, an unsupported content type — is written by gqlgen's transport before an executor exists, so it never reaches this function and is not redacted. See the note on Config.Fiber in the server package. Errors gqlgen generates and hands here keep their own extensions.code (GRAPHQL_PARSE_FAILED, GRAPHQL_VALIDATION_FAILED, COMPLEXITY_LIMIT_EXCEEDED) through the pass-through branch below.
@param ctx the resolver context, read only for graphql.GetPath @param err the error a resolver returned @return *gqlerror.Error the message the client receives, with the field path attached
func SQLState ¶
SQLState @notice Returns the Postgres SQLSTATE of err, or "" if err is not a driver error.
if luima.SQLState(err) == "23505" { // unique_violation
@dev Codes worth classifying: 23505 unique_violation, 23503 foreign_key_violation, 23502 not_null_violation, 23514 check_violation. pg.Error also has IntegrityViolation() bool if one branch for the whole 23xxx class is enough.
pg.Error is an *interface* (error + Field(byte) + IntegrityViolation()), not a struct pointer and not pgx's *pgconn.PgError — so the errors.As target is `var pgErr pg.Error`. Getting this wrong is the most common bug when porting error handling between the two drivers: it fails to compile in one direction and silently never matches in the other.
@param err any error, including nil and wrapped chains @return string the five-character SQLSTATE, or "" when no pg.Error is in the chain
Types ¶
type CustomError ¶
type CustomError struct {
// UserMessage @notice The text the client receives verbatim. Assume it is public, and
// assume it is untrusted.
//
// @dev Public, so never build it from another error: &CustomError{UserMessage: err.Error()}
// undoes the redaction in one line that reads like careful error handling, because
// PresentError returns this field as-is. Untrusted, because the usual way to build it is
// from client input — crud.Create's label is documented as "user "+id. The response is
// JSON, so there is no injection at luima's layer; a client that renders error messages
// into the DOM inherits the sink.
UserMessage string
// InternalError @notice The cause, kept for the log and for errors.Is/As.
//
// @dev gqlgen's own reference server stores a string here; keeping the error means the
// underlying pg.Error stays reachable, which is what makes SQLState work on a wrapped
// error.
InternalError error
// Code @notice A machine-readable code for the client, e.g. "CONFLICT". Optional.
//
// @dev Clients should branch on this, never on UserMessage — the message is built from
// caller-supplied text (see crud.Create's label) and is not a stable contract. Empty means
// no extensions object is emitted, so a zero CustomError is unchanged on the wire.
//
// Nothing here is auth-shaped on purpose. CONFLICT and NOT_FOUND describe rows; a library
// that ships no auth has no business defining UNAUTHENTICATED.
Code string
}
CustomError @notice Carries a message the client is allowed to see.
@dev Any resolver error that is not a *CustomError is infrastructure detail as far as PresentError is concerned, and is redacted.
func (*CustomError) Error ¶
func (e *CustomError) Error() string
Error @notice Renders the message and, when there is one, the cause.
@dev The cause is included because this string goes to the log, never to the client — PresentError reads UserMessage directly.
@return string "user X already exists: <cause>", or just the message when there is no cause
func (*CustomError) Unwrap ¶
func (e *CustomError) Unwrap() error
Unwrap @notice Exposes the cause to errors.Is and errors.As.
@return error InternalError, which may be nil