Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions handlers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,64 @@ type RequestPersister[Req, Resp any] interface {
}
```

There is no built-in persister in this package. Consumers implement `RequestPersister` when they need audit storage, for example delegating to `libRequest`:
### Handler outcome fields

After the response is sent (or after a panic is captured in `Recovery`), `Update` receives a `HandlerRequest` with populated outcome metadata:

```go
type HandlerOutcome struct {
Error error // nil on success; set on handler/init/parse/insert errors and panics
HTTPStatus int // HTTP status sent or intended (500 on panic); 0 if no response was sent
}

type HandlerRequest[Req, Resp any] struct {
...
Outcome HandlerOutcome
Duration time.Duration // elapsed handler time, set in Recovery
RespSent bool
}
```

`HTTPStatus` is recorded by the response layer (`response.LastHTTPStatusLocal`) after `Responder().OK` / `Responder().Error`, so it reflects the status actually emitted — not a duplicate mapping in handlers. On panic, `Recovery` sets `HTTPStatus` to `500` before `Update` runs.

Use `trx.Outcome.Error` with `libError.Unwrap` to build persistence outcome without reading parser locals such as `errorArray`.

### Record ID handoff

Use a shared local key instead of per-domain names (`shahkar_request_id`, `card_issue_id`, etc.):

```go
handlers.SetPersistedRecordID(req.W, recordID) // any type: int64, string UUID, etc.
id, ok := handlers.GetPersistedRecordID(req.W)
```

### FuncPersister

For tests or thin adapters, implement persistence with function fields:

```go
p := handlers.FuncPersister[MyReq, MyResp]{
InsertFn: func(path string, req *handlers.HandlerRequest[MyReq, MyResp]) error { ... },
UpdateFn: func(path string, req *handlers.HandlerRequest[MyReq, MyResp]) error { ... },
}
```

Nil function fields are no-ops. Use `Persistence: nil` when no persistence is needed.

### Example service persister

Consumers implement `RequestPersister` when they need audit storage, for example delegating to `libRequest`:

```go
type ServiceRequestPersister[Req, Resp any] struct{}

func (ServiceRequestPersister[Req, Resp]) Insert(path string, req *handlers.HandlerRequest[Req, Resp]) error {
return req.Core.RequestTools().InitRequest(req.W, req.Title, path)
err := req.Core.RequestTools().InitRequest(req.W, req.Title, path)
if err != nil {
return err
}
handlers.SetPersistedRecordID(req.W, req.W.Parser.GetLocal("reqLog").(libRequest.RequestPtr).GetId())
return nil
}

func (ServiceRequestPersister[Req, Resp]) Update(path string, req *handlers.HandlerRequest[Req, Resp]) error {
Expand All @@ -348,7 +399,11 @@ func (ServiceRequestPersister[Req, Resp]) Update(path string, req *handlers.Hand
return nil
}
reqLog.Outgoing = req.Response
// map errors from req.W.Parser.GetLocal("errorArray") as needed
if req.Outcome.Error != nil {
if ok, errData := libError.Unwrap(req.Outcome.Error); ok {
_ = errData // map code/status into your storage model
}
}
return req.Core.RequestTools().UpdateRequestWithContext(req.W.Ctx, reqLog)
}
```
Expand All @@ -363,6 +418,7 @@ The handlers package includes test coverage through files such as:

```text
baseHanlder_test.go
persistence_test.go
callApi_test.go
consumeHandler_test.go
dmlHandler_test.go
Expand Down
61 changes: 47 additions & 14 deletions handlers/baseHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ type HandlerInterface[Req any, Resp any] interface {
Simulation(req HandlerRequest[Req, Resp]) (Resp, error)
}

type HandlerOutcome struct {
Error error
HTTPStatus int
}

type HandlerRequest[Req any, Resp any] struct {
Title string
Core requestCore.RequestCoreInterface
Expand All @@ -60,11 +65,18 @@ type HandlerRequest[Req any, Resp any] struct {
Args []any
RespSent bool
Builder func(status int, rawResp []byte, headers map[string]string) (*Resp, error)
Outcome HandlerOutcome
Duration time.Duration
// Tracing fields
Span trace.Span
SpanCtx context.Context
}

func (trx *HandlerRequest[Req, Resp]) SetOutcome(err error, httpStatus int) {
trx.Outcome.Error = err
trx.Outcome.HTTPStatus = httpStatus
}

// Tracing methods for HandlerRequest
func (hr *HandlerRequest[Req, Resp]) AddSpanAttribute(key, value string) {
if hr.Span != nil && hr.Span.IsRecording() {
Expand Down Expand Up @@ -114,6 +126,29 @@ func (hr HandlerRequest[Req, Resp]) GetParser() webFramework.RequestParser {
return hr.W.Parser
}

func respondError[Req, Resp any](core requestCore.RequestCoreInterface, trx *HandlerRequest[Req, Resp], err error) {
core.Responder().Error(trx.W, err)
trx.SetOutcome(err, response.LastHTTPStatus(trx.W))
}

func respondOK[Req, Resp any](core requestCore.RequestCoreInterface, trx *HandlerRequest[Req, Resp], resp Resp) {
core.Responder().OK(trx.W, resp)
trx.SetOutcome(nil, response.LastHTTPStatus(trx.W))
trx.RespSent = true
}

func respondOKWithReceipt[Req, Resp any](core requestCore.RequestCoreInterface, trx *HandlerRequest[Req, Resp], resp Resp, receipt *response.Receipt) {
core.Responder().OKWithReceipt(trx.W, resp, receipt)
trx.SetOutcome(nil, response.LastHTTPStatus(trx.W))
trx.RespSent = true
}

func respondOKWithAttachment[Req, Resp any](core requestCore.RequestCoreInterface, trx *HandlerRequest[Req, Resp], attachment *response.FileResponse) {
core.Responder().OKWithAttachment(trx.W, attachment)
trx.SetOutcome(nil, response.LastHTTPStatus(trx.W))
trx.RespSent = true
}

func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
core requestCore.RequestCoreInterface,
handler Handler,
Expand Down Expand Up @@ -169,7 +204,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
}

defer func() {
Recovery(start, w, handler, params, &trx, core, requestInserted)
Recovery(start, w, handler, params, &trx, core, requestInserted, recover())
}()

// Ensure span is ended
Expand All @@ -186,7 +221,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
params.ValidateHeader,
)
if errParse != nil {
core.Responder().Error(trx.W, errParse)
respondError(core, &trx, errParse)
return
}

Expand All @@ -196,11 +231,12 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
var err error
trx.Response, err = libTracing.TraceFunc(handler.Simulation, trx)
if err != nil {
core.Responder().Error(trx.W, err)
respondError(core, &trx, err)
return
}

core.Responder().OK(trx.W, trx.Response)
respondOK(core, &trx, trx.Response)
return
}

var errParse error
Expand All @@ -209,15 +245,15 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
params.Body,
params.ValidateHeader)
if errParse != nil {
core.Responder().Error(trx.W, errParse)
respondError(core, &trx, errParse)
return
}
w.Parser.SetLocal(libLogger.SlogRequestBody, trx.Request)
webFramework.AddLog(w, webFramework.HandlerLogTag, slog.Any("request", trx.Request))

if params.Persistence != nil {
if errInsert := params.Persistence.Insert(params.Path, &trx); errInsert != nil {
core.Responder().Error(trx.W, errInsert)
respondError(core, &trx, errInsert)
return
}
requestInserted = true
Expand All @@ -227,15 +263,15 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
errInit = libTracing.TraceError(handler.Initializer, trx)
webFramework.AddLog(w, webFramework.HandlerLogTag, slog.Any("initialize", errInit))
if errInit != nil {
core.Responder().Error(trx.W, errInit)
respondError(core, &trx, errInit)
return
}

var err error
trx.Response, err = libTracing.TraceFunc(handler.Handler, trx)
webFramework.AddLog(w, webFramework.HandlerLogTag, slog.Any("main-handler", err))
if err != nil {
core.Responder().Error(trx.W, err)
respondError(core, &trx, err)
return
}
webFramework.AddLog(w, webFramework.HandlerLogTag, slog.Any("response", trx.Response))
Expand All @@ -244,8 +280,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
if receipt != nil {
rc, ok := receipt.(*response.Receipt)
if ok {
core.Responder().OKWithReceipt(trx.W, trx.Response, rc)
trx.RespSent = true
respondOKWithReceipt(core, &trx, trx.Response, rc)
} else {
slog.Error("registered as handler with receipt, but receipt local was", slog.Any("receipt", fmt.Sprintf("%t", receipt)))
}
Expand All @@ -259,8 +294,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
if attachment != nil {
rc, ok := attachment.(*response.FileResponse)
if ok {
core.Responder().OKWithAttachment(trx.W, rc)
trx.RespSent = true
respondOKWithAttachment(core, &trx, rc)
} else {
slog.Error("registered as handler with attachment, but attachment local was", slog.Any("receipt", fmt.Sprintf("%t", attachment)))
}
Expand All @@ -270,8 +304,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
}

if !trx.RespSent {
core.Responder().OK(trx.W, trx.Response)
trx.RespSent = true
respondOK(core, &trx, trx.Response)
}
}
}
Loading
Loading