Help us shape the future of Go! Take the 2026 Go Developer Survey

apioutcome

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 15 Imported by: 0

README

apioutcome

One request outcome across HTTP response, completion log, and the active trace.

apioutcome is for Go backend APIs that already have a router, logger, and optional OpenTelemetry instrumentation. Add one middleware to observe existing routes. Where an endpoint needs a stable public error code, return or write a Problem. The library emits one safe slog record per request and annotates the span already in the request context. It does not start a tracer or require a new handler signature.

// Existing net/http router and handlers still work.
handler := apioutcome.Middleware(apioutcome.Options{Logger: logger})(mux)
http.ListenAndServe(":8080", handler)

This is a v0 library: review the contract before production use.

Install and compatibility

Integration Package Tested framework Go minimum
net/http and compatible routers github.com/truongbo17/apioutcome Go standard library 1.22
Gin github.com/truongbo17/apioutcome/ginoutcome Gin 1.10.1 1.22
Fiber v2 github.com/truongbo17/apioutcome/fiberoutcome Fiber 2.52.15 1.22
Fiber v3 github.com/truongbo17/apioutcome/fiberv3outcome Fiber 3.5.0 1.25, separate module
go get github.com/truongbo17/apioutcome@v0.2.0
# Only Fiber v3 users need this separate module:
go get github.com/truongbo17/apioutcome/fiberv3outcome@v0.1.0

The root module is tested on Go 1.22, 1.24, and 1.26. The Fiber v3 module is tested on Go 1.25 and 1.26. Its separate go.mod keeps newer Fiber dependencies out of the root module. Framework versions above are the versions pinned for testing, not a promise that every earlier or later framework release is compatible.

Adopt in an existing service

net/http, chi, and other http.Handler routers
opts := apioutcome.Options{Logger: slog.New(slog.NewJSONHandler(os.Stdout, nil))}
mux := http.NewServeMux()
mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
    var order struct { ProductID string `json:"product_id"` }
    if err := apioutcome.DecodeJSON(r, &order, 64*1024); err != nil {
        apioutcome.WriteError(w, r, err)
        return
    }
    if order.ProductID == "" {
        apioutcome.WriteError(w, r, apioutcome.Problem(422, "invalid_order", "product_id is required", nil))
        return
    }
    w.WriteHeader(http.StatusCreated)
})
http.ListenAndServe(":8080", apioutcome.Middleware(opts)(mux))

The middleware observes all routes, including the router's 404 and handlers you have not changed. WriteError is only needed where you want a consistent problem response and code. For a handler that naturally returns an error, apioutcome.Wrap(handler, opts) remains available. WriteError must run before response bytes or headers are sent to replace the response; install Middleware around existing handlers so commit state is tracked.

Gin
router := gin.New()
router.Use(ginoutcome.Middleware(apioutcome.Options{Logger: logger}))
router.POST("/orders", func(c *gin.Context) {
    if c.Query("product_id") == "" {
        ginoutcome.WriteError(c, apioutcome.Problem(422, "invalid_order", "product_id is required", nil))
        return
    }
    c.Status(http.StatusCreated)
})

Existing gin.HandlerFunc routes keep their signatures. ginoutcome.Wrap is available for returned-error handlers. Put the middleware before routes. Gin's own logger or recovery middleware may produce additional logs or handle panics first depending on order; choose one completion logger and place recovery deliberately.

Fiber v2
app := fiber.New()
app.Use(fiberoutcome.Middleware(apioutcome.Options{Logger: logger}))
app.Post("/orders", func(c *fiber.Ctx) error {
    if c.Query("product_id") == "" {
        return apioutcome.Problem(422, "invalid_order", "product_id is required", nil)
    }
    return c.SendStatus(http.StatusCreated)
})
Fiber v3
app := fiber.New()
app.Use(fiberv3outcome.Middleware(apioutcome.Options{Logger: logger}))
app.Post("/orders", func(c fiber.Ctx) error {
    if c.Query("product_id") == "" {
        return apioutcome.Problem(422, "invalid_order", "product_id is required", nil)
    }
    return c.SendStatus(http.StatusCreated)
})

The Fiber adapters handle returned errors before Fiber's configured ErrorHandler runs. If you have a custom ErrorHandler, decide which behavior should own error responses and test middleware order. The adapters preserve a body or nondefault status already set before a later error. They do not include or log private error text.

What the library guarantees

  • A Problem(status, code, detail, cause) before response commit writes application/problem+json with type: "about:blank", the HTTP status phrase as title, and a code extension. The code is an application constant: lowercase letters, digits, and underscores. Safe detail is included only for 4xx; 5xx detail and cause stay private.
  • Unknown returned errors become HTTP 500 internal_error. A handler that directly writes a 4xx or 5xx without calling WriteError is logged as unclassified_http_error; the library does not reinterpret its body.
  • Completion logs include method, status, code, duration, error_after_commit, and a trace ID if available. They exclude paths, query strings, request bodies, error causes, and panic values. The active OTel span receives the status and code; the library creates no span or exporter.
  • DecodeJSON bounds the request body and rejects unknown fields and trailing values. It handles JSON syntax and size, not application validation rules.
  • A response already committed stays intact. The completion event flags a later error. Streaming and WebSocket responses can be observed, but cannot be replaced with JSON after their first write.

about:blank follows RFC 9457; clients should use type as the primary problem identifier under that standard. The code extension is useful for application-specific handling. Keep public codes and details in trusted source code.

Where it fits

Project Primary job Use with apioutcome
Gin, Fiber HTTP routing and framework middleware Yes; use the matching adapter
Huma API schema, validation, OpenAPI, and HTTP framework Usually choose Huma's own error flow; apioutcome targets existing routers
otelchi and OTel HTTP instrumentation Create spans and collect HTTP telemetry Yes; apioutcome annotates the active span
Existing slog setup Own log format and destination Yes; pass your logger

The narrow purpose is to keep the public error, completion record, and active trace in agreement without replacing the rest of the service. See BENCHMARKS.md for measured cost and reproducible commands.

Development

go test ./...
go test -race ./...
go vet ./...
(cd fiberv3outcome && go test ./... && go vet ./...)

See SPEC.md, CONTRIBUTING.md, and SECURITY.md. Licensed under MIT.

Documentation

Overview

Package apioutcome connects returned HTTP handler errors to safe client responses, structured completion logs, and the active OpenTelemetry span.

Index

Constants

View Source
const DefaultMaxBodyBytes int64 = 1 << 20

DefaultMaxBodyBytes is the size limit used when DecodeJSON receives a nonpositive maxBytes value.

Variables

This section is empty.

Functions

func DecodeJSON

func DecodeJSON(r *http.Request, dst any, maxBytes int64) error

DecodeJSON reads one JSON value with a hard byte limit and rejects unknown fields and trailing values. It returns a public Error on malformed input.

func Middleware added in v0.2.0

func Middleware(opts Options) func(http.Handler) http.Handler

Middleware observes an existing http.Handler without changing its signature. Install it once around the router so unmatched routes are observed too.

func NotFound

func NotFound(opts Options) http.Handler

NotFound returns an HTTP fallback handler for unmatched routes. Register it on "/" after more specific routes when using http.ServeMux.

func Report

func Report(ctx context.Context, logger *slog.Logger, event Event)

Report writes one safe slog record and annotates the active OTel span. It never logs request paths, queries, bodies, or private error causes.

func Wrap

func Wrap(handler Handler, opts Options) http.Handler

Wrap adapts a returned-error handler. Existing handlers can use Middleware and WriteError instead, without changing their signature.

func WriteError added in v0.2.0

func WriteError(w http.ResponseWriter, r *http.Request, err error)

WriteError records a public code and writes a problem response. When called under Middleware after the response is committed, it only records the code.

Types

type Error

type Error struct {
	Status int
	Code   string
	Detail string
	Cause  error
}

Error describes a public API failure. Cause remains private: apioutcome never puts it in an HTTP response, completion log, or trace attribute.

func AsProblem

func AsProblem(err error) *Error

AsProblem returns the public classification of err. Unknown errors become an internal_error with no private details in its public fields.

func Problem

func Problem(status int, code, detail string, cause error) *Error

Problem constructs a public error. Code and detail must be constants chosen by the application, never strings copied from client input or error causes.

func Render

func Render(w http.ResponseWriter, r *http.Request, err error) *Error

Render writes an RFC 9457-shaped problem response and returns its public description. The caller must ensure the response has not been committed.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap makes Cause available to application code using errors.Is/As.

type Event

type Event struct {
	Method           string
	Status           int
	Code             string
	ErrorAfterCommit bool
	Duration         time.Duration
}

Event is the safe completion data shared by the HTTP and Gin adapters.

type Handler

type Handler func(http.ResponseWriter, *http.Request) error

Handler is an HTTP handler that can return an error before writing a response. A returned error after writing cannot change the HTTP response.

type Options

type Options struct{ Logger *slog.Logger }

Options configures the completion logger. A nil Logger uses slog.Default.

Directories

Path Synopsis
examples
orders command
Package fiberoutcome adapts apioutcome to Fiber v2.
Package fiberoutcome adapts apioutcome to Fiber v2.
Package ginoutcome adapts apioutcome to Gin middleware and handlers.
Package ginoutcome adapts apioutcome to Gin middleware and handlers.

Jump to

Keyboard shortcuts

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