HTTP Runtime
Generated HTTP code talks to httpx, not to Gin, Fiber, Echo, or Hertz directly. server/httpz adds the JSON envelopes and handler wrappers that official templates use.
Layers
| Layer | Package | Role |
|---|---|---|
| Router contract | github.com/go-sphere/httpx | Engine, Router, Handler, Middleware, Context, Streamer |
| Adapters | httpx/stdx, httpx/ginx, httpx/fiberx, httpx/echox, httpx/hertzx | stdx is plain net/http; the rest wrap a concrete framework |
| Responses | github.com/go-sphere/sphere/server/httpz | WithJson, WithSSE, DataResponse, ErrorResponse, SSEStream |
| Middleware | sphere/server/middleware/* | Auth, CORS, online, rate limiter, selector |
protoc-gen-sphere defaults are httpx.Router / httpx.Context /
httpx.Handler, with httpz.WithJson for unary methods and httpz.WithSSE
for server-streaming methods. See protoc-gen-sphere.
Success Envelope
{
"success": true,
"data": { }
}httpz.WithJson writes HTTP 200 unless the handler set a status through httpx.ResponseInfo (for example 201).
Error Envelope
{
"success": false,
"code": 1001,
"message": "User not found"
}Rules enforced by httpz.AbortWithJsonError:
- HTTP status comes from
httpx.StatusError(or the parser). Invalid statuses become 500. codeis an application code. It is0unless the error implementshttpx.CodeError.messageis user-facing. It is the generic status text unless the error implementshttpx.MessageErrorwith a non-empty message.error(err.Error()) is included only whenhttpz.SetDebugMode(true).
Typed proto errors generated by protoc-gen-sphere-errors implement those interfaces, so .Join() / returning the enum produces a stable client payload. Plain fmt.Errorf and driver errors do not leak internal text in production.
Binding
Generated handlers bind through httpx.Context:
if err := ctx.BindJSON(&in); err != nil {
return nil, err
}
if err := ctx.BindQuery(&in); err != nil {
return nil, err
}
if err := ctx.BindURI(&in); err != nil {
return nil, err
}GET/HEAD/DELETE/OPTIONS never call BindJSON, even if the proto declared body. Enable fail_on_warn on protoc-gen-sphere if you want that to fail generation instead of warning.
Streaming
httpx.Streamer is the portable incremental-response capability implemented by
every adapter. httpx.ServerSentEvents builds SSE framing on top
of it and provides SSEWriter.Send, SendData, SendJSON, and Comment.
Every event is flushed as one unit.
Generated server-streaming methods use a two-phase httpz.WithSSE handler:
return httpz.WithSSE(func(ctx httpx.Context) (httpz.SSEStream[*WatchResponse], error) {
var in WatchRequest
if err := ctx.BindQuery(&in); err != nil {
return nil, err
}
stdCtx := ctx.Context()
return func(send func(*WatchResponse) error) error {
return srv.Watch(stdCtx, &in, send)
}, nil
})The prepare phase owns request binding and may return a normal JSON error before
anything is committed. The producer phase must only use captured ordinary Go
values—not httpx.Context—and must stop when send fails or the standard
context is canceled.
By default, the first reply commits a 200 text/event-stream response. Reply
messages are unnamed JSON data events, successful completion emits done,
and a later failure emits error with the normal ErrorResponse. A producer
failure before its first reply is still rendered as a regular JSON error status.
After commit, the wrapper emits 15-second keep-alive comments and disables nginx
buffering. Push endpoints that may wait indefinitely before their first reply
can opt into eager commit through a custom stream wrapper; see the streaming
guide for the trade-off.
stdx, Gin, Echo, and Hertz additionally implement httpx.Flusher for manually
flushing a response inside a handler. Fiber does not, but its Streamer
implementation works through Fiber’s deferred stream writer. In-process
httpx.TestRequester calls buffer the final stream body, so use a real network
connection to test incremental delivery and disconnect behavior.
See Server Streaming for the proto contract, service implementation pattern, client behavior, and deployment considerations.
Template Wiring
Official templates construct a net/http server and hand it to the stdx
adapter:
s := &http.Server{Addr: addr, ReadHeaderTimeout: readHeaderTimeout}
engine := stdx.New(
stdx.WithServer(s),
stdx.WithErrorHandler(httpz.AbortWithJsonError),
)
return engine // httpx.Enginestdx.WithAddr(addr) does the same when a bare listen address is enough and
you do not need to configure the server. Replace stdx with ginx, fiberx,
echox, or hertzx without changing generated service interfaces. Keep:
- generated
Register*HTTPServer(route httpx.Router, srv ...) - binding tags from
sphere.binding httpzenvelopes, unless you override the generator flags
Middleware
There is one middleware form: a layer receives the rest of the chain and returns what runs in its place. Use registers it on an engine, a group, or a router.
type Middleware func(next httpx.Handler) httpx.Handler
func RequestID(next httpx.Handler) httpx.Handler {
return func(ctx httpx.Context) error {
ctx.SetContext(withRequestID(ctx.Context()))
return next(ctx)
}
}Returning without calling next stops the chain (there is no Abort bookkeeping), and an error returned by next is rendered at the route where the chain was composed — a layer that logs or measures the outcome should read that error, not only ctx.StatusCode().
Where a layer runs:
- A route’s chain is resolved when the route is registered. A late
engine.Usereaches the routes an existing group registers afterwards; routes already registered keep the chain they were registered with. Uselayers always run inside anything mounted with the adapter’sUseNative(ginx,echox,fiberx,hertzx), whatever the registration order.stdxhas noUseNative, because net/http has no native middleware type beyond the oneAdaptStdMiddlewarealready takes.- An engine-scope chain also covers paths no route matched (404/405), so access logging, panic recovery, and CORS see them; a group’s chain never does, because a 404 belongs to no group.
ctx.SetContextvalues set below stay visible to the layers above afternextreturns.
Official templates register the shared logger layers at engine scope (internal/pkg/httpsrv/httpsrv.go):
engine.Use(logger.Log(lg), logger.RecoveryLog(lg, true))sphere/server/middleware ships the same shape for auth, CORS, online tracking, rate limiting, selector, and logger. See Logging for the logger layer’s composition rules and Upgrading to v0.0.5 / v0.0.6 if you are migrating middleware written against the old func(httpx.Context) error form.
Custom Error Parser
Templates install a parser that maps protovalidate and Ent errors before falling back to httpx.ParseError:
httpz.SetDefaultErrorParser(func(err error) (int32, int32, string) {
var ve *protovalidate.ValidationError
if errors.As(err, &ve) {
return 0, 400, /* joined violation messages */
}
return httpx.ParseError(err)
})The parser return is (code, status, message).
codeis still zeroed unless the error implementshttpx.CodeError.messagefromhttpx.MessageErroralways wins when non-empty.- Otherwise a non-empty parser message is used, falling back to the generic status text only when it is empty. Joined validation text comes back this way;
httpx.ParseErrorreturns an empty message for unclassified errors, so rawerr.Error()strings never reach the client. - Adapter default error handlers use
httpx.RenderError(status +{success, code, message}, noerrorfield). Official templates installhttpz.AbortWithJsonErrorthroughstdx.WithErrorHandlerso middleware failures use the same envelope asWithJson.
NewXxxError(msg) / NewWithStatus(status, msg) put msg in GetMessage(). XxxError(err) without extra arguments still has an empty user message and becomes the generic status text. Bind failures are wrapped as httpx.BadRequestError in every adapter.