docs(mcp): document httputil handlers + WithStatus in wire builtin (v1.3.3)

This commit is contained in:
2026-08-12 17:59:16 -06:00
parent 87c5eda1bc
commit 766adb2989
5 changed files with 109 additions and 37 deletions
+15
View File
@@ -6,6 +6,21 @@ This module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html
---
## [1.3.3] — 2026-08-09
Patch. Documents the httputil handler adapters and configurable success status.
### Added
- Wire builtin gains an "HTTP handlers (httputil)" section — `Handle`/`HandleNoBody`/
`HandleEmpty`, their default success statuses (200/200/204), and `httputil.WithStatus`
(web v1.5.0) for 201 Created / 202 Accepted, with the 2xx-only panic-at-wiring rule and
automatic error mapping.
### Fixed
- Wire builtin feature-hook example passes `logger` to the handler constructor (httputil needs it).
## [1.3.2] — 2026-08-08
Patch. Scaffold/wire middleware order now mirrors `web.New`.
+1 -1
View File
@@ -1,6 +1,6 @@
# einherjar/mcp
[![version](https://img.shields.io/badge/version-v1.3.2-5C4EE5?style=flat-square)](https://code.nochebuena.dev/einherjar/mcp)
[![version](https://img.shields.io/badge/version-v1.3.3-5C4EE5?style=flat-square)](https://code.nochebuena.dev/einherjar/mcp)
[![license](https://img.shields.io/badge/license-AGPL--3.0-22863A?style=flat-square)](LICENSE)
[![go](https://img.shields.io/badge/Go-1.26+-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev)
+1 -1
View File
@@ -23,7 +23,7 @@ import (
const (
serverName = "einherjar-mcp"
serverVersion = "v1.3.2"
serverVersion = "v1.3.3"
)
func main() {
+44 -34
View File
File diff suppressed because one or more lines are too long
+48 -1
View File
@@ -348,7 +348,7 @@ func withUsers(
repo := userrepo.New(db)
uow := postgres.NewUnitOfWork(logger, db)
svc := usersvc.New(repo, uow)
h := userhandler.New(svc, v)
h := userhandler.New(svc, v, logger) // handler carries v + logger for httputil
// Literal-segment routes register BEFORE parametrised siblings.
srv.Put("/api/v1/users/me/password", h.ChangeOwnPassword)
@@ -384,6 +384,53 @@ srv.Put("/api/v1/users/{user_id}", h.UpdateUser)
srv.Put("/api/v1/users/me/password", h.ChangeOwnPassword)
```
## HTTP handlers (httputil)
Handler methods (`h.CreateUser`, `h.ListUsers`, …) wrap `web/httputil`, which does the
decode → validate → call → encode. The method body is just the typed business call:
```go
import (
"net/http"
"code.nochebuena.dev/einherjar/web/httputil"
)
// POST that creates a resource → 201 Created via WithStatus.
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
httputil.Handle(h.v, h.logger, func(ctx context.Context, req CreateUserReq) (CreateUserRes, error) {
id, err := h.svc.Create(ctx, req)
if err != nil {
return CreateUserRes{}, err // xerror → HTTP status is automatic (see below)
}
return CreateUserRes{ID: id}, nil
}, httputil.WithStatus(http.StatusCreated))(w, r)
}
```
**Adapters and their default success status:**
| Adapter | Body | Default | Use for |
|---|---|---|---|
| `httputil.Handle(v, logger, fn, opts…)` | req + res | **200** | POST/PUT returning a body |
| `httputil.HandleNoBody(logger, fn, opts…)` | res only | **200** | GET/HEAD |
| `httputil.HandleEmpty(v, logger, fn, opts…)` | req only | **204** | DELETE / body-less PUT |
**Override the success status with `httputil.WithStatus(code)`**`WithStatus(http.StatusCreated)`
(201) on a create, `WithStatus(http.StatusAccepted)` (202) on an async `HandleEmpty`. The code **must
be 2xx**: these adapters own only the success path, so a non-2xx code is a routing mistake and
**panics at wiring** (the service fails to boot rather than emit a wrong status at runtime). Never
pass a 4xx/5xx to `WithStatus`.
**Error status is automatic — never set it by hand.** Return the right `*xerrors.Err`
(`xerrors.NotFound(…)`, `xerrors.InvalidInput(…)`, `xerrors.PermissionDenied(…)`, …) and
`httputil.Error` maps it to the HTTP status and logs at the derived level (5xx→Error, 4xx→Warn,
499→Info). Validation failures become 400 automatically.
**Escape hatch:** for a response the adapters don't cover, write a raw `http.HandlerFunc` and call
`httputil.JSON(w, status, v)` or `httputil.NoContent(w)` yourself — that is the only place you pass
a status literal.
## Authorization
Every protected route registers with `.With(authz(provider, resource, grant))`: