docs(mcp): document httputil handlers + WithStatus in wire builtin (v1.3.3)
This commit is contained in:
@@ -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
|
## [1.3.2] — 2026-08-08
|
||||||
|
|
||||||
Patch. Scaffold/wire middleware order now mirrors `web.New`.
|
Patch. Scaffold/wire middleware order now mirrors `web.New`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# einherjar/mcp
|
# einherjar/mcp
|
||||||
|
|
||||||
[](https://code.nochebuena.dev/einherjar/mcp)
|
[](https://code.nochebuena.dev/einherjar/mcp)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://go.dev)
|
[](https://go.dev)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
serverName = "einherjar-mcp"
|
serverName = "einherjar-mcp"
|
||||||
serverVersion = "v1.3.2"
|
serverVersion = "v1.3.3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
+44
-34
File diff suppressed because one or more lines are too long
@@ -348,7 +348,7 @@ func withUsers(
|
|||||||
repo := userrepo.New(db)
|
repo := userrepo.New(db)
|
||||||
uow := postgres.NewUnitOfWork(logger, db)
|
uow := postgres.NewUnitOfWork(logger, db)
|
||||||
svc := usersvc.New(repo, uow)
|
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.
|
// Literal-segment routes register BEFORE parametrised siblings.
|
||||||
srv.Put("/api/v1/users/me/password", h.ChangeOwnPassword)
|
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)
|
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
|
## Authorization
|
||||||
|
|
||||||
Every protected route registers with `.With(authz(provider, resource, grant))`:
|
Every protected route registers with `.With(authz(provider, resource, grant))`:
|
||||||
|
|||||||
Reference in New Issue
Block a user