Hibamodell
A gpsystem hibamodellje két rétegből áll, és egyetlen elvből: egy hibaforma a dróton, egy mapping a kódban. Minden hiba, amit egy gpsystem service visszaad (validációs bukás, 404, panic, adatbázis-timeout), RFC 9457 problem dokumentumként érkezik a klienshez, Content-Type: application/problem+json-nel:
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/api/v1/shop/orders",
"errors": [
{ "field": "quantity", "rule": "min", "message": "must be at least 1" }
]
}
A drót felé néző forma a httperr.Problem; alatta a service-ek és repository-k hagyományos, errs-szel épített Go hibákat adnak vissza: az errs önálló, csak standard librarytől függő modul (github.com/gp-system/errs). Ezek a hibák stack trace-t, gépi olvasású kódot és kliensbiztos publikus üzenetet hordoznak. Az errs a kittel és a kit nélkül is ugyanúgy működik, bármilyen Go programban; ez az oldal a teljes pipeline szintjén marad. A teljes errs API-hoz (New, Wrap, Define, attribútumok, accessorok, stack-szemantika) lásd az errs: Áttekintés és az errs: API oldalt.
A kettősség attribútumokkal fejeződik ki: a Public és a Status a render-oldal (mit lát a kliens), a Code, a wrap-lánc és a stack a report-oldal (mi megy a logba/Sentrybe). A renderelést nem az egyes hiba végzi, hanem egyetlen központi mapping, és a láncnak négy különböző modul felel meg, egy-egy láncszemért:
| Ki | Mit csinál | Oldal |
|---|---|---|
errs | épít és wrappel hibákat: stack, kód, publikus üzenet | errs: Áttekintés |
httperr | renderel egy HTTP problem+json válasszá | Hibaválaszok |
telemetry/logx | strukturált rekordként logolja | Logolás |
telemetry/sentryx | Sentrybe riportolja, kód szerint csoportosítva | Sentry |
Miért két üzenet
Egy hibának két közönsége van. A belső üzenet (Error()) neked szól: teljes részlet, becsomagolt okok, a hibázó query. A publikus üzenet az egyetlen szöveg, amit a kliens láthat. A kettő szétválasztása azt jelenti, hogy egy elszabadult err.Error() a válaszban nem szivárogtathat ki belső részletet: a httperr mapping a errs.PublicOf-ot olvassa, soha nem az Error()-t.
A stack trace és a wrap-lánc minden 5xx-nél a logba kerül. HTTP-válaszba csak dev módban (HTTP_EXPOSE_INTERNAL_ERRORS=true) jut el, a Problem stack és chain extension membereként; productionben kizárólag a logban.
A shop példa: elfogyott a készlet
A várt üzleti hiba (ErrOutOfStock) ott van deklarálva, ahol természetesen keletkezik: a modul-szintű repositoryban. A handler viszont soha nem importálja a repositoryt, ezért a sentinel a láncon fölfelé re-exportálódik: a core továbbadja (var ErrOutOfStock = repository.ErrOutOfStock), a surface service pedig szintén, így a handler és a service mindig csak a saját surface-e service csomagját ellenőrzi. A surface-specifikus wrap-hiba (ErrPlaceOrder) a surface service-ében él:
package repository
import (
"net/http"
"github.com/gp-system/errs"
)
var ErrOutOfStock = errs.Define("shop_out_of_stock",
errs.Public("A termék elfogyott."),
errs.Status(http.StatusConflict))
package service
import (
"github.com/gp-system/errs"
"github.com/acme/shop/internal/modules/shop/core"
)
// Re-export a core-ból (ami a repositoryból exportálja tovább): a handler
// csak a saját surface-e service csomagját importálja.
var ErrOutOfStock = core.ErrOutOfStock
var ErrPlaceOrder = errs.Define("shop_api_place_order_failed",
errs.Public("A rendelés feladása nem sikerült, próbáld újra később."))
A hiba a repositoryban keletkezik (itt capture-ölődik a stack), és minden réteg a saját kontextusával wrappeli fölfelé:
func (r *Orders) DecrementStock(ctx context.Context, productID string, qty int) error {
tag, err := r.db.Exec(ctx, decrementStockSQL, productID, qty)
if err != nil {
return errs.Wrap(err, "orders: decrement stock",
errs.With("product_id", productID))
}
if tag.RowsAffected() == 0 {
return ErrOutOfStock.New("orders: decrement stock",
errs.With("product_id", productID))
}
return nil
}
func (s *Service) PlaceOrder(ctx context.Context, in PlaceOrderInput) error {
err := s.core.PlaceOrder(ctx, in) // tranzakció + készletcsökkentés + outbox a core-ban
if errors.Is(err, ErrOutOfStock) {
return err // várt üzleti hiba: megy tovább változatlanul
}
return ErrPlaceOrder.Wrap(err, "shop: place order") // nil-safe: nil-re nil
}
A handler csak visszaadja, amit kap, a renderelés nem az ő dolga:
func (h *Handler) PlaceOrder(ctx context.Context, req gen.PlaceOrderRequest) (gen.PlaceOrderResponse, error) {
if err := h.svc.PlaceOrder(ctx, toPlaceOrder(req)); err != nil {
return nil, err
}
return gen.PlaceOrder201Response{}, nil
}
És ekkor a három közönség hármat lát:
A kliens (409, mert az ErrOutOfStock státusz-mappelt):
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "A termék elfogyott.",
"instance": "/api/v1/shop/orders",
"code": "shop_out_of_stock"
}
A log: semmit. Egy státusz-mappelt 4xx elvárt kliens-viselkedés, nem incidens. Nem generál request failed logsort. A Sentry: szintén semmit, ugyanezért.
Ha viszont a Postgres esik ki alóla, a lánc az ErrPlaceOrder-en át 500-ként ér ki: a kliens csak a "A rendelés feladása nem sikerült..." detailt és a shop_api_place_order_failed kódot kapja, a log a teljes láncot és stacket (lásd Logolás), a Sentry pedig egy shop_api_place_order_failed című, kód szerint csoportosított issue-t a trace-re linkelve (lásd Sentry).
A mapping
A központi mapping (a httperr renderelője, a generált szerver hibahookjaiba és a router panic/404/405 kezelőibe kötve) ebben a sorrendben dönt:
| Hiba | Válasz |
|---|---|
*httperr.Problem | változtatás nélkül renderelve |
| validációs hiba a bindből | 422, mezőnkénti errors[]-szel (lásd Validáció) |
| kérés-dekódolási hiba (hibás body/paraméter) | 400 |
errs hiba a láncban | errs.StatusOf (default 500), detail = a publikus üzenet, code member = a kód |
| minden más | 500, detail nélkül |
5xx-nél a mögöttes hiba a slog-ba megy a kérés trace-kontextusával. A kliens nem kap belső részletet. Dev módban (HTTP_EXPOSE_INTERNAL_ERRORS=true) a detail debug célból kitöltődik, és ha a láncban errs hiba van, a válasz stack és chain extension membereket is kap. A teljes drót-oldali kép, az engine-hookok pontos leírásával, a Hibaválaszok oldalon van.
A gyors 4xx-ekhez a httperr konstruktorai továbbra is rendelkezésre állnak (httperr.NotFound(...), httperr.Conflict(...), httperr.Validation(...)), de amint egy hibamódnak neve van, az errs.Define a jobb otthon: a felismerés (errors.Is) és a renderelés (státusz, kód, üzenet) egyetlen deklarációban él, nem egy handlerbeli if-ágban.
Log kimenet
Az *errs.Error implementálja az slog.LogValuer-t, így a slog.Any("error", err) strukturált csoportot bocsát ki egy lapos string helyett, minden 5xx-nél automatikusan:
"error": {
"msg": "shop: place order: orders: decrement stock: connection refused",
"code": "shop_api_place_order_failed",
"public": "A rendelés feladása nem sikerült, próbáld újra később.",
"chain": [
{ "msg": "shop: place order",
"file": "api/service/service.go", "line": 42, "function": "service.(*Service).PlaceOrder",
"code": "shop_api_place_order_failed" },
{ "msg": "orders: decrement stock",
"file": "repository/orders.go", "line": 31, "function": "repository.(*Orders).DecrementStock",
"meta": { "product_id": "prod_42" } },
{ "msg": "connection refused" }
],
"stack": [
{ "file": "repository/orders.go", "line": 31, "function": "repository.(*Orders).DecrementStock" },
{ "file": "api/service/service.go", "line": 42, "function": "service.(*Service).PlaceOrder" },
{ "file": "api/http/handler.go", "line": 55, "function": "http.(*Handler).PlaceOrder" }
]
}
A chain wrap-szintenként egy objektum (a szint saját üzenete, a wrap helye, és az ott beállított kód/metadata), a végén a root cause-zal. A stack a teljes hívási út a keletkezési pontról, a közbeeső, nem wrappelő hívásokkal együtt. A teljes mechanika (miért néz ki így a chain és a stack, és a LogValue/sima-wrap csapda) a Logolás oldalon van.
Sentry
Az *errs.Error kiteszi a StackTrace() []uintptr metódust (azt a formát, amit a sentry-go reflexióval felismer), így a telemetry/sentryx extra huzalozás nélkül kinyeri a stacket. Küldés előtt az integráció átcímkézi az exceptiont a legkülső hiba Code-jára (ha van), így a kód nemcsak a válasz code membere, hanem a Sentry issue címe és csoportosítási fingerprintje is: egy hibamód = egy issue, üzenetszöveg-változásoktól függetlenül.
A spec oldala
A generált spec/typespec/shared/errors.tsp a httperr.Problem tükre (a FieldError-ral együtt), így az OpenAPI-ból generált frontend-kliensek pontosan ezt a formát kapják. Ha az egyiket bővíted, bővítsd a másikat is. A dev-only stack és chain membereket az errors.tsp nem tükrözi: productionben soha nem jelennek meg, így kliens nem építhet rájuk.
Használt patternek
Ez az oldal a Design patternek katalógusban részletesebben is szereplő két minta pipeline-nézete: a sentinel-definíció testreszabott Is-sel (errs.Define, teljes API az errs: API oldalon) és a lusta, láncononkénti stack-capture. A kaszkádoló errors.As mapping (a httperr oldalán) és az RFC 9457 forma a Hibaválaszok oldalon él.
Merre tovább
- errs: Áttekintés: telepítés, önálló használat.
- errs: API: a teljes
New/Wrap/Define/attribútumok/accessorok végigvezetés. - Hibaválaszok: a drót felé néző
Problemforma és az engine-mapping. - Logolás és Sentry: mi történik a hibával, miután logolták vagy riportolták.