Skip to content
HTTP Runtime

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

LayerPackageRole
Router contractgithub.com/go-sphere/httpxEngine, Router, Handler, Middleware, Context, Streamer
Adaptershttpx/stdx, httpx/ginx, httpx/fiberx, httpx/echox, httpx/hertzxstdx is plain net/http; the rest wrap a concrete framework
Responsesgithub.com/go-sphere/sphere/server/httpzWithJson, WithSSE, DataResponse, ErrorResponse, SSEStream
Middlewaresphere/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.
  • code is an application code. It is 0 unless the error implements httpx.CodeError.
  • message is user-facing. It is the generic status text unless the error implements httpx.MessageError with a non-empty message.
  • error (err.Error()) is included only when httpz.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.Engine

stdx.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
  • httpz envelopes, 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.Use reaches the routes an existing group registers afterwards; routes already registered keep the chain they were registered with.
  • Use layers always run inside anything mounted with the adapter’s UseNative (ginx, echox, fiberx, hertzx), whatever the registration order. stdx has no UseNative, because net/http has no native middleware type beyond the one AdaptStdMiddleware already 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.SetContext values set below stay visible to the layers above after next returns.

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).

  • code is still zeroed unless the error implements httpx.CodeError.
  • message from httpx.MessageError always 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.ParseError returns an empty message for unclassified errors, so raw err.Error() strings never reach the client.
  • Adapter default error handlers use httpx.RenderError (status + {success, code, message}, no error field). Official templates install httpz.AbortWithJsonError through stdx.WithErrorHandler so middleware failures use the same envelope as WithJson.

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.

Related

Last updated on