Documentation
¶
Overview ¶
Package gogo is the "gogo~" HTTP framework: a Go binding for the uWebSockets C++ HTTP server tuned for low cgo overhead.
Build ¶
The native binding is opt-in because it compiles the vendored uWebSockets / uSockets sources and requires cgo. Native builds need Go 1.24+, CGO_ENABLED=1, a C compiler, a C++20-capable compiler, and the host zlib library/headers. Build with:
CGO_ENABLED=1 go build -tags gogo ./... CGO_ENABLED=1 go run -tags gogo . CGO_ENABLED=1 go test -tags gogo ./...
Without the gogo build tag the package compiles a stub that returns an error from NewApp — useful for tools that import the package but won't actually run a server. See docs/install-build.md for OS package prerequisites and downstream smoke build validation.
Hello world ¶
import gogo "github.com/Snocko-main/gogo"
func main() {
app, err := gogo.NewApp()
if err != nil { panic(err) }
defer app.Close()
app.Get("/hello/:name", func(res *gogo.Response, req *gogo.Request) {
res.Send(200, "text/plain", "hi " + req.Parameter(0))
})
if !app.Listen(3000) { panic("listen") }
app.Run()
}
Routing ¶
Route patterns are path-only patterns that must start with "/". gogo uses uWS route syntax for literals, named params such as "/users/:id", and trailing wildcards such as "/files/*", then adds typed annotations such as "/users/:id<int>". Typed annotations are stripped before registration and checked before middleware or handlers run; invalid values receive 404.
For a concrete HTTP method, literal segments take precedence over named params, and named params take precedence over wildcard catch-alls. Method-specific routes run before Any routes. Custom NotFound and MethodNotAllowed handlers are installed as a catch-all at Listen time after user routes, so explicit routes retain precedence. MethodNotAllowed distinguishes wrong-method requests for literal, named-param, typed-param, and terminal wildcard routes; typed-param constraints must match before the route contributes to the Allow header.
Use Group or Mount to bind a prefix and middleware to a router identity:
api := app.Group("/api", authMW)
api.Get("/users/:id", showUser)
Group prefixes may include named or typed params, reject wildcards, strip trailing slashes, and concatenate with child patterns that also start with "/". Router middleware composes at route registration with no per-request prefix check; parent middleware wraps child middleware.
Handler styles ¶
gogo offers three handler styles, picking the lowest-overhead dispatch path that still satisfies the constraints of the route:
app.Get / app.Post / app.Any (Handler): synchronous handler invoked from the uWS loop thread. Must not block. One cgo callback per request. Use for fast in-memory replies and middleware that doesn't fan out to async work.
app.GetAsync (AsyncHandler): handler runs on a goroutine and is free to block. Without middleware, gogo dispatches through a shared-memory ring with ZERO cgo crossings per request — the C++ side snapshots the request into the AsyncCtx and pushes a pointer onto the ring; a pool of long-lived Go workers drains it. With middleware, the framework falls back to a sync callback that runs the chain with the live request, then captures a snapshot and spawns the user goroutine.
app.PostAsync / app.PutAsync / app.PatchAsync / app.DeleteAsync (BodyAsyncHandler): async handler that receives a fully-collected body up to maxBodyBytes. Oversize bodies return 413 automatically. Uses res.OnData internally and switches to async mode once the body is complete.
All async handlers receive a *Request snapshot (URL/method/query/params/ headers all captured before uWS freed the live request). Snapshot caps in the zero-cgo shared path: URL 256, query 512, params 8x64, headers 8 KB total; requests that exceed those caps are rejected with 431 rather than being silently truncated. The middleware/body-async paths copy headers exactly via cgo so they have no cap.
Responses ¶
Use res.Send for one-shot replies with status, content-type, and body. The framework auto-picks the fastest path:
- Sync handler: one cgo crossing into uWS.
- Async handler with body ≤ 8 KB: ZERO cgo — written into shared-memory inline buffers and pushed onto the App's response ring; the loop drains it.
- Async handler with body > 8 KB: cgo Loop::defer falls back.
res.JSON wraps Send with Config.JSONEncoder (encoding/json.Marshal by default) and Content-Type: application/json.
Middleware ¶
Sync middleware runs on the uWS loop thread and must NOT block. Use it for cheap cross-cutting work (auth header check, logging, CORS).
app.Use(loggerMW) // every route registered later
app.Use("/api/*", authMW, corsMW) // only routes under /api/
app.Get("/api/x", handleX)
authMW := func(next gogo.Handler) gogo.Handler {
return func(res *gogo.Response, req *gogo.Request) {
if req.Header("authorization") == "" {
res.Send(401, "text/plain", "no")
return
}
next(res, req)
}
}
The first argument to Use may optionally be a path prefix that scopes the middleware to request URLs under that prefix. "/api/*", "/api/**", and "/api" mean exact "/api" plus children under "/api/"; "/", "/*", and "/**" mean global. Without a pattern, middleware applies to every later-registered route.
Global middleware composes at route registration with no per-request string comparison. Scoped middleware matches the live request URL so dynamic routes cannot bypass a scoped guard. Static replies (Reply, string, []byte targets of app.Get) use the zero-cgo fast path only when no matching sync middleware or typed-param constraint needs to run.
Async middleware ¶
AsyncMiddleware wraps GetAsync and body-async handlers and runs on the same goroutine as the user handler, so it IS free to block — typical use: resolve a user from a session token via a DB lookup, then hand the loaded user to the handler.
app.Use("/api/*", func(next gogo.AsyncHandler) gogo.AsyncHandler {
return func(res *gogo.Response, req *gogo.Request) {
token := req.Header("authorization")
user, err := db.LoadUserByToken(token) // blocking — ok
if err != nil {
res.Send(401, "text/plain", "unauthorized\n")
return
}
req.SetLocal("user", user)
next(res, req)
}
})
app.GetAsync("/api/me", func(res *gogo.Response, req *gogo.Request) {
user := req.Local("user").(*User)
res.JSON(200, user)
})
Async middleware applies only to GetAsync and body-async routes. If only async middleware matches a GetAsync route (no sync mw), the framework still uses the zero-cgo shared-memory dispatch path; the async chain composes inside the worker goroutine alongside the user handler. Sync routes (Get, Post, Put, Patch, Delete, Any) never see async middleware.
req.SetLocal / req.Local pass values from middleware to the handler; req.Body() returns the collected body for body-async routes (nil otherwise), so async middleware can inspect the body before the handler.
Cookies, JSON ¶
req.Cookie("session")
res.SetCookie(gogo.Cookie{Name: "x", Value: "y", HttpOnly: true})
res.JSON(200, map[string]any{"ok": true})
Multi-core ¶
Single-loop mode (NewApp + Run) caps throughput at one OS thread — uWebSockets is event-loop driven, not goroutine-per-request. To run multiple event loops, use RunMultiCore:
handle, err := gogo.RunMultiCore(runtime.NumCPU(), 3000, func(app *gogo.App) {
app.Get("/plain", plainHandler)
// … same routes / middleware as a single-loop app …
})
if err != nil { log.Fatal(err) }
// signal-driven immediate shutdown:
<-sigCh
handle.Shutdown()
handle.Wait()
RunMultiCore spawns N independent App instances, each bound to the same port. The default mode lets the kernel distribute accepted sockets with SO_REUSEPORT, which avoids cross-loop socket handoff overhead. If you need predictable per-loop connection placement, use RunMultiCoreWithOptions with MultiCoreBalanced; that mode round-robins accepted sockets across App loops at extra accept-path cost. If SO_REUSEPORT distributes well in your production environment but your async routes need a larger default worker budget, keep MultiCoreReusePort and set RunMultiCoreOptions.WorkerHintLoops. setup runs once per instance on the OS thread that instance will own. setup has no error return; do fallible shared initialization before RunMultiCore. A setup panic is recovered, converted to an error, and any created Apps are closed. RunMultiCore currently creates each worker with the zero-value Config; there is no Config parameter for app-scoped settings such as BodyLimit, BodyReadTimeout, BindAddr, CapturePeerIP, TrustProxy, or custom JSON codecs. Use process-wide knobs before RunMultiCore and per-route/per-middleware options inside setup.
MultiCoreHandle.Shutdown calls Shutdown on every worker App, so multicore shutdown is immediate and active connections are closed. There is no multicore equivalent of App.ShutdownGracefully yet.
Tuning knobs that actually matter:
- GOMAXPROCS — pin to the same N you passed to RunMultiCore. The scheduler then has exactly one P per loop; oversubscribing wastes context-switch budget, undersubscribing starves loops.
- SetWorkerCount — controls the GetAsync worker-goroutine pool. Default = ceil(1.5 × worker-hint loops). A single App and default RunMultiCore reuseport mode use the one-loop default so async workers do not steal CPU when the kernel places many connections on one listener. MultiCoreBalanced uses the full loop count. RunMultiCoreOptions.WorkerHintLoops overrides only that loop-count hint; SetWorkerCount still wins when you need an exact worker count. Raise either for IO-bound handlers that keep many requests blocked at once.
- Shared resources (DB pools, caches) — create ONCE outside RunMultiCore and capture the pointers into the handler closures. setup runs once per loop; allocating fresh DB pools per loop wastes RAM and connection slots.
- Per-loop CPU pinning — gogo does not pin to specific cores. Linux's scheduler typically keeps each loop on its initial CPU for cache locality. If you need stricter pinning run the server under `taskset -c 0-(N-1)` or wrap LockOSThread with a sched_setaffinity call.
On a 4 vCPU host the gogo bench /plain route scales from ~112 k RPS at 1 core to ~230 k RPS at 2 cores (~2.05× linear). Past 2 cores the same-host wrk client starts competing with the server for CPU, so the apparent 4-core number drops back to ~200 k — a true 4-core measurement needs a separate load-generator host. Either way, gogo per core consistently outruns fiber per core on this hardware (+86 % at 1 core, +48 % at 4 cores, both routes saturated).
See examples/multicore for a full setup with signal handling + a /metrics endpoint formatted as Prometheus text exposition. See docs/production-examples.md for the v1 production example coverage map.
Graceful shutdown ¶
runDone := make(chan struct{})
go func() {
app.Run()
close(runDone)
}()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := app.ShutdownContext(ctx); err != nil {
log.Printf("forced shutdown: %v", err)
}
<-runDone
app.Close() // free native resources after Run returns
ShutdownContext returns nil only after Run exits. If the context expires first, it force-closes active connections and returns the context error; wait for Run to return before calling Close.
Shutdown, ShutdownGracefully, and ShutdownContext are safe from any goroutine. Native builds own the uWS loop on an internal locked goroutine, so normal single-app code does not need runtime.LockOSThread. Register routes, middleware, and WebSocket behavior before Listen / Run; shutdown and publish APIs are the supported cross-goroutine entry points once the loop is running.
Performance characteristics ¶
- Per-request alloc on the GET fast path: dominated by Go runtime work, not framework.
- statusLine for common HTTP codes (200/201/204/3xx/4xx/5xx) is precomputed and zero-alloc.
- PanicHandler (see SetPanicHandler) catches handler panics across HTTP, async, WebSocket, defer, and body callbacks, emits a best-effort 500 where an HTTP response is still available, and keeps the server alive.
Index ¶
- Constants
- Variables
- func GetDefaultMultipartPartLimit() int64
- func GetMaxHTTPAdapterBodyBytes() int64
- func GetMaxRenderBytes() int64
- func GetMaxSendFileBytes() int64
- func GetSendFileBackpressureBytes() uint64
- func GetSendFileChunkBytes() int
- func GetStreamBackpressureBytes() uint64
- func ParseBody(contentType string, body []byte, out any) error
- func ParseMultipart(contentType string, body []byte, fn func(*MultipartPart) error) error
- func ParseMultipartStream(contentType string, body []byte, opt MultipartOptions, ...) error
- func ParseMultipartWithOptions(contentType string, body []byte, opt MultipartOptions, ...) error
- func RegisterParamType(name string, check func(string) bool)
- func SetDefaultMultipartPartLimit(maxBytes int64)
- func SetMaxHTTPAdapterBodyBytes(maxBytes int64)
- func SetMaxRenderBytes(maxBytes int64)
- func SetMaxSendFileBytes(maxBytes int64)
- func SetPanicHandler(fn PanicHandler)
- func SetSendFileBackpressureBytes(backpressureBytes uint64)
- func SetSendFileChunkBytes(chunkBytes int)
- func SetStreamBackpressureBytes(backpressureBytes uint64)
- func SignCookieValue(value string, secrets ...string) string
- func VerifyCookieValue(signed string, secrets ...string) (string, bool)
- func WaitForSharedWorkers(time.Duration) bool
- type Aborted
- type App
- func (a *App) AllowedMethods(path string) []string
- func (a *App) Any(pattern string, handler Handler)
- func (a *App) Close()
- func (a *App) Delete(pattern string, handler Handler)
- func (a *App) DeleteAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (a *App) Get(pattern string, target any)
- func (a *App) GetAsync(pattern string, handler AsyncHandler)
- func (a *App) Group(prefix string, mws ...any) *Router
- func (a *App) Head(pattern string, handler Handler)
- func (a *App) Listen(port int) bool
- func (a *App) MethodNotAllowed(h Handler)
- func (a *App) Mount(prefix string, register func(r *Router)) *Router
- func (a *App) Name(name, pattern string)
- func (a *App) NotFound(h Handler)
- func (a *App) OnListen(fn func(port int))
- func (a *App) OnShutdown(fn func())
- func (a *App) Options(pattern string, handler Handler)
- func (a *App) Patch(pattern string, handler Handler)
- func (a *App) PatchAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (a *App) Post(pattern string, handler Handler)
- func (a *App) PostAsync(pattern string, maxBodyBytes int, handler PostAsyncHandler)
- func (a *App) Publish(topic string, message []byte, opcode OpCode)
- func (a *App) PublishBatch(msgs []PublishMessage)
- func (a *App) Put(pattern string, handler Handler)
- func (a *App) PutAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (a *App) Run()
- func (a *App) SetTemplateEngine(e TemplateEngine)
- func (a *App) Shutdown()
- func (a *App) ShutdownContext(ctx context.Context) error
- func (a *App) ShutdownGracefully(timeout time.Duration)
- func (a *App) URL(name string, params map[string]string) (string, error)
- func (a *App) Use(args ...any)
- func (a *App) UseAsync(args ...any)
- func (a *App) WebSocket(pattern string, behavior WebSocketBehavior)
- type AsyncHandler
- type AsyncMiddleware
- type BodyAsyncHandler
- type Config
- type Cookie
- type HTMLTemplateOptions
- type Handler
- type JSONDecoder
- type JSONEncoder
- type LimitedTemplateEngine
- type Loop
- type Middleware
- type MultiCoreHandle
- type MultiCoreMode
- type MultipartOptions
- type MultipartPart
- type MultipartStreamPart
- type OpCode
- type PanicHandler
- type PostAsyncHandler
- type PublishMessage
- type Reply
- type Request
- func (r *Request) Body() []byte
- func (r *Request) BodyParser(out any) error
- func (r *Request) Context() context.Context
- func (r *Request) Cookie(name string) string
- func (r *Request) CookieSigned(name string, secrets ...string) (string, bool)
- func (r *Request) Get(name string) string
- func (r *Request) Header(name string) string
- func (r *Request) Headers(fn func(name, value string) bool) int
- func (r *Request) Hostname() string
- func (r *Request) IP() string
- func (r *Request) IPs() []string
- func (r *Request) Local(key string) any
- func (r *Request) Method() string
- func (r *Request) Multipart(fn func(*MultipartPart) error) error
- func (r *Request) MultipartStream(opt MultipartOptions, fn func(*MultipartStreamPart) error) error
- func (r *Request) MultipartWithOptions(opt MultipartOptions, fn func(*MultipartPart) error) error
- func (r *Request) Param(name string) string
- func (r *Request) ParamInt(name string, def int) int
- func (r *Request) ParamInt64(name string, def int64) int64
- func (r *Request) Parameter(index int) string
- func (r *Request) ParameterInt(index int, def int) int
- func (r *Request) ParameterInt64(index int, def int64) int64
- func (r *Request) Protocol() string
- func (r *Request) Query() string
- func (r *Request) QueryBool(name string, def bool) bool
- func (r *Request) QueryInt(name string, def int) int
- func (r *Request) QueryInt64(name string, def int64) int64
- func (r *Request) QueryParam(name string) string
- func (r *Request) Secure() bool
- func (r *Request) SetLocal(key string, value any)
- func (r *Request) Truncated() bool
- func (r *Request) URL() string
- type Response
- func (r *Response) Append(key, value string) *Response
- func (r *Response) Async(fn func())
- func (r *Response) AwaitDrain(threshold uint64) error
- func (r *Response) Body(maxBytes int, done func(body []byte, err error))
- func (r *Response) BufferedAmount() uint64
- func (r *Response) Cork(fn func())
- func (r *Response) Download(req *Request, path, filename string) error
- func (r *Response) End(body string)
- func (r *Response) Header(key, value string) *Response
- func (r *Response) JSON(code int, v any)
- func (r *Response) JSONBytes(code int, b []byte)
- func (r *Response) JSONP(callback string, v any)
- func (r *Response) JSONStream(code int, fn func(*json.Encoder) error) error
- func (r *Response) Loop() *Loop
- func (r *Response) OnAborted() *Aborted
- func (r *Response) OnData(fn func(chunk []byte, isLast bool))
- func (r *Response) OnFinish(fn func())
- func (r *Response) Redirect(location string, code int)
- func (r *Response) Render(name string, data any)
- func (r *Response) SSE(fn func(*SSEStream) error) error
- func (r *Response) Send(code int, contentType, body string)
- func (r *Response) SendFile(req *Request, path string) error
- func (r *Response) SetBodyEncoder(...)
- func (r *Response) SetBodyEncoderLimit(maxBytes int, ...)
- func (r *Response) SetCookie(c Cookie)
- func (r *Response) SetCookieSigned(c Cookie, secrets ...string)
- func (r *Response) Status(code int) *Response
- func (r *Response) StatusCode() int
- func (r *Response) Stream(status int, contentType string, fn func(w io.Writer) error) error
- func (r *Response) Write(body string) *Response
- type Router
- func (r *Router) Any(pattern string, handler Handler)
- func (r *Router) Delete(pattern string, handler Handler)
- func (r *Router) DeleteAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (r *Router) Get(pattern string, target any)
- func (r *Router) GetAsync(pattern string, handler AsyncHandler)
- func (r *Router) Group(prefix string, mws ...any) *Router
- func (r *Router) Head(pattern string, handler Handler)
- func (r *Router) Name(name, pattern string)
- func (r *Router) Options(pattern string, handler Handler)
- func (r *Router) Patch(pattern string, handler Handler)
- func (r *Router) PatchAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (r *Router) Post(pattern string, handler Handler)
- func (r *Router) PostAsync(pattern string, maxBodyBytes int, handler PostAsyncHandler)
- func (r *Router) Put(pattern string, handler Handler)
- func (r *Router) PutAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
- func (r *Router) Use(args ...any)
- func (r *Router) UseAsync(mws ...AsyncMiddleware)
- func (r *Router) WebSocket(pattern string, behavior WebSocketBehavior)
- type RunMultiCoreOptions
- type SSEEvent
- type SSEStream
- type SameSite
- type TemplateEngine
- type TestServer
- func NewTestServer(setup func(*App)) (*TestServer, error)
- func NewTestServerT(tb testing.TB, setup func(*App)) *TestServer
- func NewTestServerTWithOptions(tb testing.TB, setup func(*App) error, opts TestServerOptions) *TestServer
- func NewTestServerWithOptions(setup func(*App) error, opts TestServerOptions) (*TestServer, error)
- func (ts *TestServer) App() *App
- func (ts *TestServer) Client() *http.Client
- func (ts *TestServer) Close()
- func (ts *TestServer) Do(req *http.Request) (*http.Response, error)
- func (ts *TestServer) Get(path string) (*http.Response, error)
- func (ts *TestServer) Port() int
- func (ts *TestServer) Post(path, contentType string, body io.Reader) (*http.Response, error)
- func (ts *TestServer) URL() string
- type TestServerOptions
- type UpgradeContext
- func (c *UpgradeContext) Accept(protocol string)
- func (c *UpgradeContext) Header(name string) string
- func (c *UpgradeContext) IP() string
- func (c *UpgradeContext) Method() string
- func (c *UpgradeContext) Protocols() []string
- func (c *UpgradeContext) Query() string
- func (c *UpgradeContext) QueryParam(name string) string
- func (c *UpgradeContext) Reject(status int, body string)
- func (c *UpgradeContext) SetUserData(v any)
- func (c *UpgradeContext) URL() string
- type WSHub
- func (h *WSHub) Attach(app *App)
- func (h *WSHub) Close() error
- func (h *WSHub) Publish(topic string, message []byte, opcode OpCode) error
- func (h *WSHub) PublishBatch(msgs []PublishMessage) error
- func (h *WSHub) PublishFrom(ws *WebSocket, topic string, message []byte, opcode OpCode) error
- func (h *WSHub) Start() error
- func (h *WSHub) Subscribe(ws *WebSocket, topic string) bool
- func (h *WSHub) Unsubscribe(ws *WebSocket, topic string) bool
- func (h *WSHub) WebSocket(app *App, pattern string, behavior WebSocketBehavior)
- func (h *WSHub) Wrap(app *App, behavior WebSocketBehavior) WebSocketBehavior
- type WSHubAdapter
- type WSHubMessage
- type WSHubOption
- func WithWSHubAdapter(adapter WSHubAdapter) WSHubOption
- func WithWSHubAdapterErrorHandler(fn func(error)) WSHubOption
- func WithWSHubAdapterPublishTimeout(timeout time.Duration) WSHubOption
- func WithWSHubAdapterQueueSize(size int) WSHubOption
- func WithWSHubAdapterTopicWorkers(workers int) WSHubOption
- func WithWSHubAdapterWorkers(workers int) WSHubOption
- func WithWSHubCloseTimeout(timeout time.Duration) WSHubOption
- func WithWSHubNodeID(id string) WSHubOption
- type WSHubTopicAdapter
- type WebSocket
- func (ws *WebSocket) End(code int, message string)
- func (ws *WebSocket) Publish(topic string, message []byte, opcode OpCode) bool
- func (ws *WebSocket) Send(message []byte, opcode OpCode) bool
- func (ws *WebSocket) SendText(message string) bool
- func (w *WebSocket) SetUserData(v any)
- func (ws *WebSocket) Subscribe(topic string) bool
- func (ws *WebSocket) Unsubscribe(topic string) bool
- func (w *WebSocket) UserData() any
- type WebSocketBehavior
Constants ¶
const ( // NoBodyLimit disables Config.BodyLimit. Use only behind an external // body-size limit, such as a trusted reverse proxy. NoBodyLimit = -1 // NoBodyReadTimeout disables Config.BodyReadTimeout. Use only for tests, // trusted local traffic, or routes protected by an external upload // deadline. NoBodyReadTimeout time.Duration = -1 )
const NoHTTPAdapterBodyLimit int64 = -1
NoHTTPAdapterBodyLimit disables the HTTPAdapter response staging cap. Use only for trusted handlers; streaming or large downloads should use native gogo streaming APIs instead of the adapter.
const NoMultipartPartLimit int64 = -1
NoMultipartPartLimit disables the multipart per-part cap. Use only when Config.BodyLimit or an external proxy still bounds total request size.
const NoRenderLimit int64 = -1
NoRenderLimit disables the Response.Render staging cap. Use only for trusted templates where output size is bounded by the application.
const NoSendFileLimit int64 = -1
NoSendFileLimit disables the SendFile / Download file-size cap. Use only for trusted file-serving routes where path allow-listing, authorization, or an external layer already bounds what may be served.
Variables ¶
var ( // ErrWSHubClosed is returned when a publish or start is attempted after Close. ErrWSHubClosed = errors.New("gogo: websocket hub is closed") // ErrWSHubCloseTimeout is returned when Close cannot drain the adapter // worker within the configured close timeout. ErrWSHubCloseTimeout = errors.New("gogo: websocket hub close timeout") // ErrWSHubAdapterQueueFull is returned by hub publish calls when the // async adapter queue is full. Local subscribers have already been fanned // out. ErrWSHubAdapterQueueFull = errors.New("gogo: websocket hub adapter queue is full") // ErrWSHubUntrackedSocket is returned when PublishFrom cannot identify the // sender. Register routes with WSHub.WebSocket or WSHub.Wrap. ErrWSHubUntrackedSocket = errors.New("gogo: websocket hub socket is not tracked; register route with hub.WebSocket or hub.Wrap") // ErrWSHubInvalidOpCode is returned when a hub publish is called with an // opcode other than Text or Binary. ErrWSHubInvalidOpCode = errors.New("gogo: websocket hub opcode must be Text or Binary") // ErrWSHubTrackFailed is reported when a socket cannot be registered with // the hub during WebSocket open. ErrWSHubTrackFailed = errors.New("gogo: websocket hub could not track socket") )
var DefaultMultipartPartLimit int64 = 8 << 20
DefaultMultipartPartLimit is the process default used by ParseMultipart when MultipartOptions.MaxPartBytes is zero. It remains assignable for backward compatibility with earlier versions; prefer SetDefaultMultipartPartLimit for runtime changes so readers observe the update atomically.
var ErrBodyTimeout = errFramework("body read deadline exceeded")
ErrBodyTimeout is reported by Body when the request body does not finish arriving within Config.BodyReadTimeout. The handler sees the error in its done callback exactly once; later chunks from the slow client are dropped on the floor.
var ErrBodyTooLarge = errFramework("body exceeds max size")
ErrBodyTooLarge is reported by Body when the request body exceeds the caller-supplied max size.
var ErrFileTooLarge = errors.New("gogo: file exceeds MaxSendFileBytes")
ErrFileTooLarge is returned by SendFile / Download when the target file is larger than MaxSendFileBytes.
var ErrHTTPAdapterBodyTooLarge = errors.New("gogo: HTTPAdapter response body exceeds MaxHTTPAdapterBodyBytes")
ErrHTTPAdapterBodyTooLarge is recorded when a wrapped stdlib handler writes more than MaxHTTPAdapterBodyBytes.
var ErrMultipartPartTooLarge = errors.New("gogo: multipart part exceeds max size")
ErrMultipartPartTooLarge is returned when a multipart part exceeds the configured per-part limit.
var ErrNoBody = errors.New("gogo: BodyParser requires a collected body; use a body-async route or ParseBody after Response.Body")
ErrNoBody is returned by Request.BodyParser when the request body has not been collected onto the Request. Body-async routes such as PostAsync, PutAsync, PatchAsync, and DeleteAsync pre-collect the body; sync handlers that collect manually with Response.Body should call ParseBody inside the callback instead.
var ErrRenderTooLarge = errors.New("gogo: rendered template exceeds MaxRenderBytes")
ErrRenderTooLarge is reported when rendered template output exceeds MaxRenderBytes.
var ErrStreamAborted = errors.New("gogo: stream aborted while waiting on backpressure drain")
ErrStreamAborted is returned by AwaitDrain, and by Stream writes that are parked on AwaitDrain, when the client disconnects before the response's backpressure buffer drains.
var ErrUnsupportedMediaType = errors.New("gogo: unsupported media type")
ErrUnsupportedMediaType is returned by ParseBody / BodyParser when the Content-Type header doesn't match any of the parser's supported media types. Handlers can map it to 415.
var MaxHTTPAdapterBodyBytes int64 = 8 << 20
MaxHTTPAdapterBodyBytes caps the response body staged by HTTPAdapter before it is copied into a gogo.Response. NoHTTPAdapterBodyLimit disables the cap.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetMaxHTTPAdapterBodyBytes / GetMaxHTTPAdapterBodyBytes for changes while requests may be running.
var MaxRenderBytes int64 = 8 << 20
MaxRenderBytes caps the bytes Response.Render will stage before sending the rendered body. NoRenderLimit disables the cap. The default bounds accidental or maliciously large template output while staying generous for normal pages.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetMaxRenderBytes / GetMaxRenderBytes for changes while requests may be running.
var MaxSendFileBytes int64 = 100 << 20
MaxSendFileBytes is a sanity cap on the largest file SendFile and Download will agree to serve — a misconfiguration guard, not a memory limit. The streaming body path uses an O(chunk) buffer regardless of file size, so memory pressure no longer scales with the file. Files larger than the cap return ErrFileTooLarge without touching the response; raise this at startup if your workload legitimately serves bigger blobs.
Default 100 MiB.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetMaxSendFileBytes / GetMaxSendFileBytes for changes while requests may be running.
var SendFileBackpressureBytes uint64 = 1 << 20
SendFileBackpressureBytes is the high-water mark for uWS's per-socket send buffer. When the buffer climbs above this value the streaming SendFile path parks on AwaitDrain until uWS notifies it the buffer has drained — this keeps a slow consumer from holding the goroutine hostage AND keeps the kernel buffer bounded at the same level regardless of file size.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetSendFileBackpressureBytes / GetSendFileBackpressureBytes for changes while requests may be running.
var SendFileChunkBytes int = 64 << 10
SendFileChunkBytes is the buffer size used for each disk read + stream write iteration. Memory used per concurrent SendFile call is bounded by this value plus uWS's internal write buffer (which grows up to SendFileBackpressureBytes before AwaitDrain parks the goroutine). 64 KiB matches the typical filesystem read-ahead granularity and uWS's default send-batch size.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetSendFileChunkBytes / GetSendFileChunkBytes for changes while requests may be running.
var StreamBackpressureBytes uint64 = 1 << 20
StreamBackpressureBytes is the high-water mark, in bytes of uWS's per-socket send buffer, at which streamWriter.Write automatically parks the calling goroutine on AwaitDrain. The check fires after every successful chunk write — a slow consumer therefore can't drive the Go producer faster than the kernel can flush.
Default 1 MiB. Set to 0 to disable the automatic check (the handler is then responsible for invoking BufferedAmount / AwaitDrain itself, the pre-default behavior).
The same threshold protects every Stream / SSE caller — file serving paths still use SendFileBackpressureBytes for its own reads-from-disk loop.
Deprecated for runtime mutation: direct assignment remains supported for startup-time configuration. Use SetStreamBackpressureBytes / GetStreamBackpressureBytes for changes while requests may be running.
Functions ¶
func GetDefaultMultipartPartLimit ¶
func GetDefaultMultipartPartLimit() int64
GetDefaultMultipartPartLimit returns the process-wide multipart per-part cap.
func GetMaxHTTPAdapterBodyBytes ¶
func GetMaxHTTPAdapterBodyBytes() int64
GetMaxHTTPAdapterBodyBytes returns the current HTTPAdapter response staging cap.
func GetMaxRenderBytes ¶
func GetMaxRenderBytes() int64
GetMaxRenderBytes returns the current Response.Render staging cap.
func GetMaxSendFileBytes ¶
func GetMaxSendFileBytes() int64
GetMaxSendFileBytes returns the current SendFile / Download file-size cap.
func GetSendFileBackpressureBytes ¶
func GetSendFileBackpressureBytes() uint64
GetSendFileBackpressureBytes returns the current SendFile backpressure high-water mark.
func GetSendFileChunkBytes ¶
func GetSendFileChunkBytes() int
GetSendFileChunkBytes returns the current per-read SendFile buffer size.
func GetStreamBackpressureBytes ¶
func GetStreamBackpressureBytes() uint64
GetStreamBackpressureBytes returns the current automatic Stream / SSE backpressure threshold.
func ParseBody ¶
ParseBody is the lower-level helper behind Request.BodyParser, exposed so sync handlers that collect the body manually (via Response.Body) can deserialize without round-tripping through the Request wrapper.
contentType may include parameters (`application/json; charset=utf-8`) — they are stripped before matching. An empty Content-Type is treated as application/octet-stream and returns ErrUnsupportedMediaType. ParseBody has no App context, so JSON uses encoding/json.Unmarshal; use Request.BodyParser when you want Config.JSONDecoder.
func ParseMultipart ¶
func ParseMultipart(contentType string, body []byte, fn func(*MultipartPart) error) error
ParseMultipart iterates every part of a multipart/form-data body, calling fn for each. Returning a non-nil error from fn stops iteration and surfaces the error verbatim to the caller of ParseMultipart; io.EOF specifically is treated as a graceful early exit and is swallowed.
contentType must include the boundary parameter ("multipart/form-data; boundary=...") — the same header the client sent. Returns ErrUnsupportedMediaType when contentType is not multipart/form-data; other parse errors surface verbatim from mime/multipart.
Memory: each part is read fully into memory before fn fires, capped by GetDefaultMultipartPartLimit unless options override it. Suitable for typical avatar / document uploads up to a few MiB. For large file parts, prefer ParseMultipartStream / Request.MultipartStream so the part can be copied without an extra Data allocation. The request body itself is still governed by Config.BodyLimit before multipart parsing begins.
func ParseMultipartStream ¶
func ParseMultipartStream(contentType string, body []byte, opt MultipartOptions, fn func(*MultipartStreamPart) error) error
ParseMultipartStream iterates multipart parts without materializing each part into a Data slice. The request body is still the collected []byte supplied by the caller, but file parts can be copied directly from the multipart reader to disk or another writer.
func ParseMultipartWithOptions ¶
func ParseMultipartWithOptions(contentType string, body []byte, opt MultipartOptions, fn func(*MultipartPart) error) error
ParseMultipartWithOptions is ParseMultipart with explicit per-part limits.
func RegisterParamType ¶
RegisterParamType makes the typed-param annotation <name> available in route patterns app-wide. The check function returns true for valid values and false for invalid ones; invalid values cause the route wrapper to respond 404 without calling the handler.
Registration is global (not per-App). Call it during process init — typically from an init() function or main() before any routes are registered. Re-registering an existing name (including the built-in types int/uint/uuid/alpha/alnum/slug) overwrites the prior check.
gogo.RegisterParamType("hex", func(s string) bool {
for i := 0; i < len(s); i++ {
c := s[i]
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return false
}
}
return len(s) > 0
})
app.Get("/blobs/:digest<hex>", handler)
func SetDefaultMultipartPartLimit ¶
func SetDefaultMultipartPartLimit(maxBytes int64)
SetDefaultMultipartPartLimit sets the process-wide multipart per-part cap used when MultipartOptions.MaxPartBytes is zero. Set to NoMultipartPartLimit to disable the default cap. Values at or below zero are kept as legacy opt-outs. Prefer explicit MultipartOptions for per-route policies.
func SetMaxHTTPAdapterBodyBytes ¶
func SetMaxHTTPAdapterBodyBytes(maxBytes int64)
SetMaxHTTPAdapterBodyBytes updates the HTTPAdapter response staging cap atomically. Set to NoHTTPAdapterBodyLimit to disable the cap.
func SetMaxRenderBytes ¶
func SetMaxRenderBytes(maxBytes int64)
SetMaxRenderBytes updates the Response.Render staging cap atomically. Set to NoRenderLimit to disable the cap.
func SetMaxSendFileBytes ¶
func SetMaxSendFileBytes(maxBytes int64)
SetMaxSendFileBytes updates the SendFile / Download file-size cap atomically. Set to NoSendFileLimit to disable the cap.
func SetPanicHandler ¶
func SetPanicHandler(fn PanicHandler)
SetPanicHandler registers fn as the global panic handler. Pass nil to restore the default stderr logger. The handler must not panic itself (any panic inside it is recovered silently).
func SetSendFileBackpressureBytes ¶
func SetSendFileBackpressureBytes(backpressureBytes uint64)
SetSendFileBackpressureBytes updates the SendFile buffered-byte high-water mark atomically. Zero restores the default.
func SetSendFileChunkBytes ¶
func SetSendFileChunkBytes(chunkBytes int)
SetSendFileChunkBytes updates the per-read SendFile buffer size atomically. Values at or below zero restore the default.
func SetStreamBackpressureBytes ¶
func SetStreamBackpressureBytes(backpressureBytes uint64)
SetStreamBackpressureBytes updates the automatic Stream / SSE backpressure threshold atomically. Zero disables the automatic check.
func SignCookieValue ¶
SignCookieValue returns value + "." + base64url(HMAC-SHA256(value, secret)) using the FIRST entry of secrets as the signing key. Additional secrets are not used by this function; supply them to VerifyCookieValue instead so a rotation can accept the old key while new cookies are issued with the new one.
Panics on an empty secrets slice or a first secret shorter than 32 bytes — signing without a strong key is never what you want, and silently producing a weakly authenticated cookie would be a security footgun.
signed := gogo.SignCookieValue("alice:42", secret)
res.SetCookie(gogo.Cookie{Name: "session", Value: signed, HttpOnly: true})
func VerifyCookieValue ¶
VerifyCookieValue splits a signed cookie value at the final '.', recomputes the HMAC of the prefix under each supplied secret, and returns the prefix when any secret produces a matching signature. Returns ("", false) when:
- signed is empty or contains no '.',
- the signature segment is not valid base64url,
- no strong secret produces a matching signature,
- secrets is empty or all supplied secrets are shorter than 32 bytes.
Comparison is constant-time so a forged cookie cannot leak the expected signature byte-by-byte through timing.
val, ok := gogo.VerifyCookieValue(req.Cookie("session"), secret, oldSecret)
if !ok {
res.Send(401, "text/plain", "bad session")
return
}
func WaitForSharedWorkers ¶
WaitForSharedWorkers always returns true in stub builds: there are no workers, so the pool is by definition drained the moment the caller asks. Keeps the public signature available across both builds so calling code doesn't have to use build tags.
Types ¶
type Aborted ¶
type Aborted struct {
// contains filtered or unexported fields
}
Aborted is a thread-safe flag set when a client aborts before the response is fully sent. Check Load before touching the Response from a deferred callback.
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is a uWebSockets HTTP application.
func NewApp ¶
NewApp creates a non-TLS uWebSockets app. With no Config the app uses safe production defaults; pass at most one Config to override. The variadic shape is the compatibility contract: NewApp() remains valid, NewApp(Config{...}) applies overrides, and more than one Config returns an error.
func (*App) AllowedMethods ¶
AllowedMethods returns the HTTP methods registered for path (uppercase, canonical order). Use it inside a MethodNotAllowed handler to build the standard Allow header for 405 responses:
app.MethodNotAllowed(func(res *gogo.Response, req *gogo.Request) {
allow := strings.Join(app.AllowedMethods(req.URL()), ", ")
res.Status(405)
res.Header("Allow", allow)
res.Header("Content-Type", "text/plain")
res.End("no\n")
})
Returns nil if the path has no registered routes. Parametric and wildcard routes are included using the same route semantics described on MethodNotAllowed.
func (*App) Close ¶
func (a *App) Close()
Close frees native resources. Call it only after Run has returned, or before Run if the app was never started. Waits for any in-flight ShutdownGracefully force-close goroutine to settle so a delayed timer can't make a cgo call against a freed app pointer.
Drops this App's reference on the shared-dispatch worker pool if it registered any shared fast-path routes. When the last shared App in the process is closed the worker goroutines exit cleanly so long-running supervisors (tests, hot-reload, multi-tenant hosts) don't accumulate dead workers spinning against a ring no app is feeding anymore.
func (*App) Delete ¶
Delete registers a DELETE route. DELETE may carry a body per RFC 9110 §9.3.5 and is subject to BodyLimit.
func (*App) DeleteAsync ¶
func (a *App) DeleteAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
DeleteAsync registers a DELETE route that collects the full request body up to maxBodyBytes, then invokes handler on a goroutine with the collected bytes plus a request snapshot. On bodies that exceed maxBodyBytes the framework sends 413 Payload Too Large automatically; on Config.BodyReadTimeout it sends 408 Request Timeout. In both cases the handler is not called.
func (*App) Get ¶
Get registers a GET route. The target may be:
- Handler / func(*Response, *Request) — dynamic, invoked per request via cgo
- string — static body served by C++ (no cgo per request)
- []byte — same as string
- Reply — static body with explicit status and Content-Type
Static targets are served entirely in C++ with no Go work per request when no matching sync middleware or typed-param constraints need to run.
func (*App) GetAsync ¶
func (a *App) GetAsync(pattern string, handler AsyncHandler)
GetAsync registers a GET route whose handler runs on a goroutine and receives a Response in async mode plus a snapshot Request (URL/method/ query/params/headers captured from the live uWS request before it was freed).
Without middleware, GetAsync uses the zero-cgo shared-memory dispatch path: C++ snapshots the request, pushes onto a lock-free ring, and a long-lived Go worker pool drains it. SendShared in the handler completes the response without any cgo crossing per request.
With middleware registered, GetAsync falls back to a sync cgo handler that runs the middleware chain with the live request, then captures a snapshot and switches to async mode for the user handler. One extra cgo callback per request only when middleware is in use.
func (*App) Group ¶
Group returns a Router scoped to prefix with mws applied to every route subsequently registered through it. Prefix must start with '/' and contain no wildcards; trailing slash is stripped so Group("/api") and Group("/api/") behave identically. Group("/") is equivalent to no prefix.
func (*App) Head ¶
Head registers a HEAD route. HEAD is bodyless and skips the BodyLimit check. Per RFC 9110, HEAD responses must omit the body; the framework does not enforce this — handlers should call res.End("") after writing the headers.
func (*App) Listen ¶
Listen binds the app to the given port and reports whether binding succeeded. The bind interface comes from Config.BindAddr; an empty BindAddr keeps the uWS default of all interfaces (0.0.0.0).
If a NotFound handler is registered, Listen wires it as the catch-all route immediately before binding so user-registered routes retain precedence.
func (*App) MethodNotAllowed ¶
MethodNotAllowed sets the fallback handler for requests whose path matches a registered route but whose method has no registered handler, e.g. app.Get("/users", h) and the client sends POST /users. Literal, parametric, typed-param, and terminal wildcard routes participate in the lookup. A typed-param route counts only when the live path satisfies its constraint; /api/* matches /api/ and descendants, but not bare /api, matching uWS route behavior.
The default fallback writes a 405 response with the standard Allow header. Custom handlers are responsible for their own response and can call AllowedMethods(req.URL()) to build the Allow header.
Calling MethodNotAllowed(nil) clears the handler.
func (*App) Mount ¶
Mount registers routes onto a sub-router rooted at prefix and runs the provided callback against it. It is sugar over App.Group plus the callback pattern that Express / Fiber users expect:
app.Mount("/api/v1", func(api *gogo.Router) {
api.Use(middleware.JWT(opts))
api.Get("/users", listUsers)
api.Post("/users", createUser)
})
Equivalent to:
api := app.Group("/api/v1")
api.Use(middleware.JWT(opts))
api.Get("/users", listUsers)
api.Post("/users", createUser)
Mount returns the Router in case the caller wants to register more routes against it after the callback returns. Calling Mount twice with the same prefix creates two independent Routers — there is no merging across calls.
func (*App) Name ¶
Name tags a previously-registered route pattern with a name so App.URL can perform reverse routing. Route registration methods intentionally do not return fluent route handles; use Name explicitly when a route needs reverse routing. The pattern must be the same (post-strip) form that the route was registered with — typically the literal string you passed to Get / Post / etc., minus any <type> annotations. The simplest usage:
app.Get("/users/:id", showUser)
app.Name("user.show", "/users/:id")
url, _ := app.URL("user.show", map[string]string{"id": "42"})
// url == "/users/42"
Name overwrites any previous mapping for the same name.
func (*App) NotFound ¶
NotFound sets the fallback handler for requests that don't match any registered route. The framework registers it as the lowest-priority catch-all (any-method /*) just before Listen binds — explicit user routes always win. Without a registered NotFound handler uWS falls back to its built-in 404 reply, which has no body and no customisation. Calling NotFound(nil) clears the handler.
Middleware registered before Listen wraps the NotFound handler the same way it wraps any other dynamic route.
func (*App) OnListen ¶
OnListen registers a callback that fires synchronously after Listen binds the socket successfully, before Listen returns. Common uses: logging the bound address, registering with a service discovery agent, sending a "ready" signal to a supervisor. Hooks run in the order they were registered and panic-recover at framework level so a misbehaving hook can't block the rest. Safe to call before or after route registration; not safe to call concurrently with Listen. Calling OnListen(nil) is a no-op.
func (*App) OnShutdown ¶
func (a *App) OnShutdown(fn func())
OnShutdown registers a callback that fires synchronously at the start of Shutdown, ShutdownGracefully, or ShutdownContext (before the C++ close is dispatched to the loop). Use it to flush logs, close DB pools, etc. Hooks run in registration order and run on whatever goroutine called the shutdown API. Hooks fire at most once per App lifecycle, even if multiple shutdown APIs are called. Calling OnShutdown(nil) is a no-op.
func (*App) Options ¶
Options registers an OPTIONS route. OPTIONS is bodyless and skips the BodyLimit check.
func (*App) Patch ¶
Patch registers a PATCH route. PATCH requests carry bodies and are subject to BodyLimit, like Post.
func (*App) PatchAsync ¶
func (a *App) PatchAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
PatchAsync registers a PATCH route that collects the full request body up to maxBodyBytes, then invokes handler on a goroutine with the collected bytes plus a request snapshot. On bodies that exceed maxBodyBytes the framework sends 413 Payload Too Large automatically; on Config.BodyReadTimeout it sends 408 Request Timeout. In both cases the handler is not called.
func (*App) PostAsync ¶
func (a *App) PostAsync(pattern string, maxBodyBytes int, handler PostAsyncHandler)
PostAsync registers a POST route that collects the full request body up to maxBodyBytes, then invokes handler on a goroutine with the collected bytes plus a request snapshot. On bodies that exceed maxBodyBytes the framework sends 413 Payload Too Large automatically; on Config.BodyReadTimeout it sends 408 Request Timeout. In both cases the handler is not called.
func (*App) Publish ¶
Publish broadcasts a WebSocket message to every subscriber of topic. Use it from outside a WebSocket handler — typically a worker goroutine that finished some work and wants to notify connected clients — where calling WebSocket.Publish directly would touch uWS's loop-thread-local TopicTree from the wrong thread.
In RunMultiCore mode the publish is dispatched onto every peer App's loop so subscribers on all cores receive it. The topic + message bytes are copied before scheduling, so the caller's buffers can be reused or reclaimed as soon as Publish returns.
Topics are exact-match strings — uWS's TopicTree v20 does not support MQTT-style "+" / "#" wildcards. Publish to the same string each subscriber used in ws.Subscribe.
opcode picks the WebSocket frame type (Text / Binary). For JSON payloads use Text so browser clients receive them as strings via onmessage.data.
Returns immediately — delivery happens asynchronously on the loop(s). There is no error / delivery-count return because the loop may not have processed the publish yet when this returns; uWS itself does not surface that count back to the publisher. Calls after Shutdown, ShutdownGracefully, or Close are ignored.
Performance — pick the right entry point:
- Inside an Open/Message/Close handler (loop thread): prefer WebSocket.Publish. On a single App it bypasses the cross-thread defer mutex and message copy. In RunMultiCore it still has to schedule one copied peer-loop publish per other App, so the cost is O(peer loops).
- From a worker goroutine, single message: App.Publish. Measures ~750 ns/op on this VM end-to-end including cgo + heap copy + Loop::defer mutex + wakeup.
- From a worker goroutine, two or more messages at once (fan-out, batch notification): App.PublishBatch — one cgo crossing + one defer mutex for the whole batch. Crossover is at N=2 on this VM (PublishBatch beats a Publish loop from there up), climbing to ~8x faster at N=100. See PublishBatch's godoc for the full measured curve.
func (*App) PublishBatch ¶
func (a *App) PublishBatch(msgs []PublishMessage)
PublishBatch broadcasts N WebSocket messages in a single cgo crossing with one Loop::defer (one mutex acquire, one wakeup) on the loop side. The whole batch is packed into one contiguous Go buffer + a parallel POD-only metadata array, copied once into the loop's heap, then iterated under the defer.
Use this when a worker goroutine needs to push many messages at once — for example, a fan-out notification that has to land on multiple topics, or a periodic stats-tick that updates several dashboards. App.Publish in a loop pays the cgo + defer-mutex cost per call; PublishBatch pays it once for the whole batch.
Measured speedup over the equivalent App.Publish loop on this VM (128-byte payload, 5 counts each, median ns per batch):
N=1 0.67x (batch SLOWER — packing overhead > savings) N=2 1.49x N=5 2.61x N=10 2.92x N=50 6.24x N=100 8.41x
Crossover is at N=2 — below that, single App.Publish is faster. Per-publish cost drops from ~750 ns (App.Publish) to ~86 ns at N=100, so batching pays off hard for real fan-out workloads.
Each PublishMessage's Topic and Message bytes are copied before the loop sees them, so caller buffers can be reused immediately. Mixed Text/Binary opcodes in one batch are fine.
Returns immediately. Like Publish, delivery happens later on the loop(s) and there is no per-message delivery-count. In RunMultiCore mode the batch is dispatched once per peer App so subscribers on all cores receive it. Calls after Shutdown, ShutdownGracefully, or Close are ignored.
func (*App) Put ¶
Put registers a PUT route. PUT requests carry bodies and are subject to BodyLimit, like Post.
func (*App) PutAsync ¶
func (a *App) PutAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
PutAsync registers a PUT route that collects the full request body up to maxBodyBytes, then invokes handler on a goroutine with the collected bytes plus a request snapshot. On bodies that exceed maxBodyBytes the framework sends 413 Payload Too Large automatically; on Config.BodyReadTimeout it sends 408 Request Timeout. In both cases the handler is not called.
func (*App) Run ¶
func (a *App) Run()
Run starts the uWebSockets event loop and blocks. Before running, installs the shared-memory drain timer on this loop so SendShared responses can be flushed by the loop thread.
func (*App) SetTemplateEngine ¶
func (a *App) SetTemplateEngine(e TemplateEngine)
SetTemplateEngine installs e as the active template engine for this App. Subsequent Response.Render calls use it. Passing nil clears the engine, which causes Render to respond with 500.
Engines are intended to be configured once at startup. Swapping engines at runtime is safe (the field is published atomically) but not a recommended pattern.
func (*App) Shutdown ¶
func (a *App) Shutdown()
Shutdown stops the app immediately: the listen socket and every active connection are closed at once. Run returns as soon as the loop drains. In-flight responses are dropped — use ShutdownGracefully when you need to wait for active clients to finish.
Safe to call from any goroutine; idempotent. Returns immediately — call Close after Run returns to free native resources. Registered OnShutdown hooks fire synchronously before the close is dispatched.
func (*App) ShutdownContext ¶ added in v0.3.0
ShutdownContext starts a graceful shutdown and blocks until Run exits or ctx is done. It closes the listen socket immediately, lets accepted connections drain naturally, and returns nil when the loop exits.
If ctx is done before the loop exits, ShutdownContext force-closes active connections with Shutdown and returns ctx.Err(). Call Close after Run returns to free native resources. Passing a nil context returns an error.
func (*App) ShutdownGracefully ¶
ShutdownGracefully closes only the listen socket so no new connections arrive, then waits up to timeout for the already-accepted connections to finish their in-flight responses naturally. If the timeout fires before everything drains, the remaining sockets are force-closed via Shutdown so the loop can exit. timeout = 0 disables the force-close (wait indefinitely).
Returns immediately — the wait + force-close run on a background goroutine. Call Close after Run returns. Registered OnShutdown hooks fire synchronously before the listen socket is closed. Close waits for the force-close goroutine to finish before freeing native resources, so it is always safe to call Close after Run returns regardless of how the loop exited.
func (*App) URL ¶
URL builds the path for a named route by substituting params into each :name segment of the pattern. Wildcards (`*`, `**`) cannot be reverse-routed — the function returns an error if the pattern contains them.
app.Get("/users/:userID/posts/:postID", showPost)
app.Name("post.show", "/users/:userID/posts/:postID")
url, err := app.URL("post.show", map[string]string{
"userID": "alice",
"postID": "42",
})
// url == "/users/alice/posts/42"
Returns an error when:
- the name is unknown,
- the pattern references a :param missing from the params map,
- the pattern contains a wildcard (* or **).
func (*App) Use ¶
Use appends middleware to the chain. Each registered route that follows this call wraps its handler in the current chain.
The first argument may optionally be a path pattern (string), scoping the middleware to URLs that fall under that prefix at request time:
app.Use(authMW) // applies to every later route
app.Use("/api/*", authMW) // applies to URLs under /api/
app.Use("/admin", auditMW, rateMW) // /admin and URLs under /admin/
Trailing "/*" or "/**" on the prefix is stripped — "/api/*" and "/api" mean the same thing (prefix = "/api"). A pattern of "/*" or "/" means "every route" (equivalent to no pattern).
Scoped Use matches the live request URL via req.URL(), not the route pattern string. This makes it safe against parametric / wildcard routes (e.g. Get("/api/:section") serving /api/admin will go through a Use("/api/admin", auth) middleware). Cost: one URL string compare per scoped entry per request — global Use (no prefix) still composes at registration with zero per-request cost.
Prefer App.Group(prefix, mws...) for scoping new code: Group binds middleware by Router identity, so the chain composes at registration time and matching is unambiguous without the per-request URL check.
Middlewares run left-to-right — the first argument runs first (outermost). Safe to call multiple times.
func (*App) UseAsync ¶
UseAsync is retained as a thin alias for App.Use to keep existing code compiling. App.Use now handles all middleware: bundled middleware carries its own placement, raw AsyncMiddleware values register in the async chain, and any custom middleware that must block on I/O can be wrapped via middleware.Async(...).
Prefer App.Use in new code — UseAsync exists only for backward compatibility and will be removed once callers migrate.
func (*App) WebSocket ¶
func (a *App) WebSocket(pattern string, behavior WebSocketBehavior)
WebSocket registers a WebSocket route.
type AsyncHandler ¶
AsyncHandler handles a request on a goroutine that is free to block. The Response arrives in async mode with the abort context pre-attached. The Request is a snapshot copied from the live uWS request before it was freed. Shared zero-cgo routes reject snapshots past their fixed caps (URL 256, query 512, each param 64, headers buffer 8 KB); middleware fallback snapshots copy the live request fields via cgo before spawning.
type AsyncMiddleware ¶
type AsyncMiddleware func(next AsyncHandler) AsyncHandler
AsyncMiddleware wraps an AsyncHandler the same way Middleware wraps a Handler, but executes on the goroutine that runs the user's async handler so it is free to block (DB queries, downstream HTTP calls, etc.).
Async middleware applies only to GetAsync and body-async routes such as PostAsync, PutAsync, PatchAsync, and DeleteAsync. Use it when the cross-cutting work itself needs to block; for cheap header / query inspection prefer sync Middleware (smaller per-request overhead and also applicable to sync routes).
Pass data through to the user handler via Request.SetLocal / Request.Local.
type BodyAsyncHandler ¶
BodyAsyncHandler is the handler signature for async routes that collect a request body before dispatching to a goroutine. It receives the response, a snapshot of the request (URL/query/params/headers all captured before uWS freed the live request), and the fully-collected body.
type Config ¶
type Config struct {
// BodyLimit caps the request-body bytes a Post / Any route will
// accept. Enforced at three layers so every intake shape gets the
// same upper bound:
//
// - Content-Length declared: rejected with 413 at arrival on the
// C++ side before any cgo crossing — zero per-request cost
// beyond the existing header lookup.
// - Chunked transfer-encoded + Response.OnData: the Go-side
// accumulator inside OnData totals chunk sizes and emits
// 413 + close as soon as the running total crosses the cap.
// - Chunked transfer-encoded + Response.Body: the caller-supplied
// maxBytes is clamped down by BodyLimit when BodyLimit is
// smaller, so handlers that ask Body(10 MiB) on an app capped
// at 1 MiB top out at 1 MiB.
//
// Zero uses the safe default of 4 MiB. Set to NoBodyLimit to disable
// the cap entirely when an external layer enforces a trusted body-size
// limit.
BodyLimit int
// BodyReadTimeout caps the wall-clock time the framework will
// wait for a request body to finish arriving. Applied per call
// to Response.Body: a timer starts when Body registers its
// chunk listener and fires done(nil, ErrBodyTimeout) if the
// last chunk hasn't landed by the deadline. Defeats slow-loris
// drip uploads where the client keeps the request open but
// sends bytes too slowly to ever exhaust BodyLimit.
//
// Zero uses the safe default of 30s. Reasonable production values
// fall between 10s for API endpoints and 60s+ for legitimate
// upload flows. Set to NoBodyReadTimeout to disable the timeout
// explicitly (not recommended outside tests or trusted local traffic).
// The timer fires on a goroutine that hands the
// cancellation back to the loop thread via Loop.Defer so done()
// and the connection close run serially with onData / onAborted
// — callers don't have to think about races.
BodyReadTimeout time.Duration
// BindAddr is the local interface to bind on. Empty string means
// "all interfaces" (uWS default 0.0.0.0). Use "127.0.0.1" for a
// localhost-only service. Applied at Listen time.
BindAddr string
// CapturePeerIP enables snapshotting the peer IP on the C++ side
// before shared-dispatch / async handlers run. When false (default),
// req.IP() in shared-dispatch GetAsync handlers and in async handlers
// that fell through to the snapshot path will return "". Sync
// handlers always get a usable req.IP() — the lookup is lazy and
// only pays cgo when actually called, so the flag has no effect
// there.
//
// Cost when enabled: one std::string_view format + ~50-byte memcpy
// per shared-dispatch request, plus 64 extra bytes on every
// AsyncCtx. Measured at roughly 2–3% throughput on small responses
// (e.g. /db at ~75K rps); negligible on routes with significant
// per-request work. Enable it when handlers behind GetAsync need to
// read the peer IP; otherwise leave it off.
CapturePeerIP bool
// TrustProxy declares that every immediate peer is a trusted reverse
// proxy (e.g. nginx, an L7 load balancer, a CDN), so the X-Forwarded-*
// headers used by gogo helpers are safe to surface to the application:
//
// - req.Protocol() / req.Secure() honor X-Forwarded-Proto.
// - req.IPs() exposes the normalized X-Forwarded-For chain.
//
// Leave OFF when the server is directly internet-facing, and prefer
// TrustedProxies when only specific proxy addresses should be trusted.
// Otherwise any client can spoof their apparent protocol / origin by
// sending X-Forwarded-* headers. Default false.
TrustProxy bool
// TrustedProxies narrows forwarded-header trust to requests whose
// immediate TCP peer matches one of these IP addresses or CIDR ranges.
// Entries may be single IPs ("127.0.0.1", "2001:db8::1") or CIDR
// prefixes ("10.0.0.0/8", "2001:db8::/32"). When non-empty, this
// allow-list is used instead of the broad TrustProxy switch; forwarded
// headers are ignored for peers outside the list.
//
// Setting TrustedProxies enables CapturePeerIP automatically because
// async/shared-dispatch routes must snapshot the immediate peer before
// they can decide whether proxy headers are trusted.
TrustedProxies []string
// JSONEncoder is used by Response.JSON and Response.JSONP. Nil uses
// encoding/json.Marshal. Override it with a faster compatible encoder
// such as sonic.Marshal, go-json.Marshal, or jsoniter.Marshal when JSON
// reflection cost dominates your handlers.
JSONEncoder JSONEncoder
// JSONDecoder is used by Request.BodyParser for application/json and
// text/json request bodies. Nil uses encoding/json.Unmarshal.
JSONDecoder JSONDecoder
}
Config tunes per-App behavior. All fields are optional; the zero value is a safe production default. Pass to NewApp; values are applied at app creation and bind time. The struct is intentionally narrow — knobs only get added here when they need a single, app-wide value.
Panic recovery is intentionally not part of Config. The supported panic hook is SetPanicHandler, which is process-wide because recovery sites include package-level workers and callbacks that are not owned by a single App.
Connection-level timeouts and limits ¶
A few knobs that look like they belong here are deliberately not exposed:
HTTP idle timeout (TCP connection sits open without sending a request, or keep-alive between requests). Fixed by uWebSockets at 10 seconds via the HTTP_IDLE_TIMEOUT_S constant in HttpContext.h — a connection that goes silent for 10 s is closed automatically. Exposing this as a config knob would require patching vendored uWS source or wiring up the uWS filter mechanism through the C++ bridge; the default is aggressive enough that this hasn't been done yet. If you need a longer idle for a legitimate long-poll-style workload, use a WebSocket route (WebSocketBehavior.IdleTimeout is configurable).
Maximum concurrent connections. Not implemented in this framework because the bound that matters in practice is the OS file-descriptor limit (`ulimit -n`) and any reverse proxy in front (nginx `limit_conn_zone`, etc.). uWS holds an idle TCP connection in ~10 KB of RAM, so 100k connections is ~1 GB — usually the FD cap fires long before that. If you genuinely need application-level admission control, terminate at a proxy and apply limits there.
Slow-loris on the request body itself IS covered: see BodyReadTimeout below.
type Cookie ¶
type Cookie struct {
Name string
Value string
Path string
Domain string
MaxAge int // seconds; <0 means delete, 0 means session, >0 explicit
Expires string
Secure bool
HttpOnly bool
SameSite SameSite
}
Cookie configures a Set-Cookie header. Zero-value fields are omitted — browsers fall back to their defaults (session cookie, no Path, etc.).
type HTMLTemplateOptions ¶
type HTMLTemplateOptions struct {
// Root is the directory containing template files. Required.
// Walked once at startup so deeply-nested layouts work without
// explicit registration.
Root string
// Suffix filters which files in Root are treated as templates.
// Default ".tmpl". Files with other extensions are ignored
// during the walk.
Suffix string
// Reload, when true, re-parses every template on each Render
// call. Useful in development so edits show up without a
// restart. Off by default — production deployments parse once
// at startup.
Reload bool
// FuncMap registers helper functions exposed to every template.
// Pair with the engine's auto-escaping by avoiding helpers that
// return raw HTML; if you must, return template.HTML and
// understand the XSS risk.
FuncMap htmltmpl.FuncMap
}
HTMLTemplateOptions configures NewHTMLTemplateEngine.
type Handler ¶
Handler handles a single HTTP request.
The Request and Response values are only valid for the duration of the callback. Do not store them or use them from another goroutine.
func HTTPAdapter ¶
HTTPAdapter wraps a net/http.Handler so it can be registered on a gogo route. The adapter:
- builds a net/http.Request from the gogo.Request (URL, method, headers; body left nil),
- runs the handler against a httptest.ResponseRecorder,
- copies the recorded status, headers, and body to the gogo.Response via res.Send.
Useful for migrating routes a-handler-at-a-time from a stdlib net/http codebase, or for serving stdlib-shaped handlers (expvar.Handler, net/http/pprof.Handler, …) under gogo. Responses are buffered before being sent through gogo. The adapter accepts http.Flusher for compatibility with stdlib handlers, but Flush only commits the staged status code; it does not stream bytes to the client. Port streaming or large-download handlers to native gogo APIs instead.
app.Get("/debug/vars", gogo.HTTPAdapter(expvar.Handler()))
app.Get("/debug/pprof/*", gogo.HTTPAdapter(http.HandlerFunc(pprof.Index)))
Body access: gogo's Get path does not collect request bodies, so the wrapped handler sees an empty body. For methods that carry payloads register via PostAsync and call HTTPAdapter from inside a wrapper that builds the http.Request with the collected body:
app.PostAsync("/upload", 8<<20, func(res *gogo.Response, req *gogo.Request, body []byte) {
httpReq := httptest.NewRequest("POST", req.URL(), bytes.NewReader(body))
copyHeadersIntoHTTPReq(httpReq, req)
rec := httptest.NewRecorder()
legacyHandler.ServeHTTP(rec, httpReq)
res.Header("Content-Type", rec.Header().Get("Content-Type"))
res.Send(rec.Code, "", rec.Body.String())
})
func HTTPAdapterWithBody ¶
HTTPAdapterWithBody is the variant that ships the collected body into the wrapped handler. Use it from PostAsync (or any path where you have access to the full request body):
app.PostAsync("/legacy", 8<<20, func(res *gogo.Response, req *gogo.Request, body []byte) {
gogo.HTTPAdapterWithBody(legacyHandler, body)(res, req)
})
The returned Handler is a closure that captures body; do not reuse it across requests.
type JSONDecoder ¶
JSONDecoder is the unmarshaling function used by App-scoped JSON body parsing helpers. Its shape matches encoding/json.Unmarshal and common third-party drop-ins.
type JSONEncoder ¶
JSONEncoder is the marshaling function used by App-scoped JSON helpers. Its shape matches encoding/json.Marshal and common third-party drop-ins.
type LimitedTemplateEngine ¶
type LimitedTemplateEngine interface {
TemplateEngine
RenderLimited(w *bytes.Buffer, name string, data any, maxBytes int64) error
}
LimitedTemplateEngine is an optional extension for engines that can stop rendering before a response grows past a framework cap. Engines that do not implement it still work through TemplateEngine; Response.Render checks the final buffer size before sending.
type Loop ¶
type Loop struct {
// contains filtered or unexported fields
}
Loop is a uWebSockets event loop. Use Defer to schedule work back onto the loop thread from any goroutine.
type Middleware ¶
Middleware wraps a Handler with cross-cutting behavior (auth, logging, CORS, etc.). The returned Handler is invoked per request; call next(res, req) to continue the chain or write a response and return to short-circuit.
Middleware composes at registration time, so there is no per-request allocation. Middleware applies to routes registered AFTER the call to App.Use that introduced it; reordering Use and route registration changes the effective chain for those routes.
Middleware runs on the uWS loop thread and MUST NOT block — no DB queries, no remote calls. For middleware that needs to block (e.g. resolving a user from a session token via a DB lookup), use AsyncMiddleware with UseAsync; it runs on a goroutine and is free to block.
Static replies (Reply, string, []byte targets of App.Get) use their C++ fast path only when no matching sync middleware or typed-param constraint needs a Go-side handler. GetAsync and small PostAsync routes keep their shared-memory fast path when only async-capable middleware applies; sync-only middleware makes them fall back to a wrapped sync entry point.
type MultiCoreHandle ¶
type MultiCoreHandle struct {
// contains filtered or unexported fields
}
MultiCoreHandle controls a group of App instances started by RunMultiCore. Shutdown stops all of them; Wait blocks until every Run loop has exited.
func RunMultiCore ¶
func RunMultiCore(n int, port int, setup func(app *App)) (*MultiCoreHandle, error)
RunMultiCore spawns n independent App instances on dedicated OS threads. Each instance binds to the given port using SO_REUSEPORT and lets the kernel distribute accepted sockets across worker loops. This is the low-overhead default; use RunMultiCoreWithOptions and MultiCoreBalanced when predictable per-loop connection placement matters more than raw accept-path overhead.
setup is called once per App, on the thread that instance will run on, to register routes / middleware / etc.
setup MUST register the same routes on every App for consistent behavior; the framework just calls setup(app) and trusts user code to be deterministic. Heavy shared state (DB pools, caches) and any fallible initialization should be created ONCE outside RunMultiCore and captured into the handler closures so per-App initialization stays cheap. If setup panics, RunMultiCore converts it to an error and closes any Apps created so far.
Returns a MultiCoreHandle that can Shutdown or Wait. Returns an error if any App fails to start; in that case already-started Apps are shut down before returning.
func RunMultiCoreWithOptions ¶ added in v1.3.1
func RunMultiCoreWithOptions(n int, port int, setup func(app *App), opts RunMultiCoreOptions) (*MultiCoreHandle, error)
RunMultiCoreWithOptions is RunMultiCore with explicit listener distribution and worker-budget hint controls.
func (*MultiCoreHandle) Shutdown ¶
func (h *MultiCoreHandle) Shutdown()
Shutdown initiates graceful stop on every App in the group. Idempotent; safe to call from any goroutine.
func (*MultiCoreHandle) Wait ¶
func (h *MultiCoreHandle) Wait()
Wait blocks until every App in the group has exited its Run loop and freed native resources. Returns immediately once all loops have finished.
type MultiCoreMode ¶ added in v1.3.1
type MultiCoreMode int
MultiCoreMode selects how RunMultiCoreWithOptions spreads accepted connections across worker loops.
const ( // MultiCoreAuto keeps the default RunMultiCore behavior. Today it maps to // MultiCoreReusePort because that avoids the cross-loop socket adoption // overhead of balanced mode. MultiCoreAuto MultiCoreMode = iota // MultiCoreReusePort lets every App bind the same port with uSockets' // default SO_REUSEPORT behavior. It is the lowest-overhead mode, but the // kernel decides connection placement and may not use every loop evenly for // short loopback benchmarks or low-cardinality client address sets. MultiCoreReusePort // MultiCoreBalanced round-robins accepted sockets across every App loop via // uWebSockets child Apps. It gives predictable per-loop connection // placement, at the cost of extra native handoff work on accepted sockets. MultiCoreBalanced )
func (MultiCoreMode) String ¶ added in v1.3.1
func (mode MultiCoreMode) String() string
type MultipartOptions ¶
type MultipartOptions struct {
// MaxPartBytes caps bytes read for each individual part. Zero uses
// GetDefaultMultipartPartLimit; NoMultipartPartLimit disables the
// per-part cap.
MaxPartBytes int64
}
MultipartOptions configures ParseMultipartWithOptions and ParseMultipartStream.
type MultipartPart ¶
type MultipartPart struct {
// Name is the form field name (the "name" attribute of the
// originating <input>). Empty when the part has no
// Content-Disposition name parameter.
Name string
// FileName is the original filename for file parts (the
// "filename" attribute of Content-Disposition). Empty for
// non-file parts.
FileName string
// ContentType is the value of the part's Content-Type header.
// Defaults to "" when omitted by the client — handlers that
// care should fall back to detecting from FileName or Data.
ContentType string
// Data is the raw bytes of this part. For file parts: the file
// contents. For value parts: the field value bytes.
Data []byte
// Header carries every header the client sent on this part —
// Content-Type, Content-Disposition, and any custom headers
// like Content-Transfer-Encoding. Use for advanced inspection;
// the convenience fields above cover the common cases.
Header textproto.MIMEHeader
}
MultipartPart is one chunk of a parsed multipart/form-data body. Returned to the callback passed to ParseMultipart / Request.Multipart.
The Data slice is valid after the callback returns, but retaining it also retains that part's bytes. Use MultipartStream when file parts should be copied to disk or another writer without an extra per-part allocation.
func (*MultipartPart) IsFile ¶
func (p *MultipartPart) IsFile() bool
IsFile reports whether this part carries a file upload (has a FileName). Convenience for the common dispatch on file-vs-value parts.
func (*MultipartPart) SaveAt ¶
func (p *MultipartPart) SaveAt(dst string) error
SaveAt writes the part's Data to dst using os.WriteFile with mode 0o644. dst is opened with O_WRONLY|O_CREATE|O_TRUNC and is closed before SaveAt returns. Returns the error from os.WriteFile when the write fails.
Convenience for the common "save uploaded file to disk" path. SECURITY: SaveAt overwrites existing files and follows symlinks. Do not pass a path derived from client input unless you have already constrained it to a safe directory. Prefer SaveInto for file uploads, or SaveAtNew when the destination must not already exist.
func (*MultipartPart) SaveAtNew ¶
func (p *MultipartPart) SaveAtNew(dst string) error
SaveAtNew writes the part's Data to dst only when dst does not already exist. The file is opened with O_CREATE|O_EXCL so existing files and symlinks are not overwritten.
func (*MultipartPart) SaveInto ¶
func (p *MultipartPart) SaveInto(dir string) (string, error)
SaveInto writes the part's Data into dir, using the basename of p.FileName as the on-disk filename. Path components in FileName are stripped so a malicious client can't escape the target directory (".." or absolute paths are rejected). Returns the full path of the written file along with any os.WriteFile error.
Returns an error when FileName is empty (not a file part) or when the basename collapses to "." / "..".
type MultipartStreamPart ¶
type MultipartStreamPart struct {
Name string
FileName string
ContentType string
Header textproto.MIMEHeader
Reader io.Reader
}
MultipartStreamPart exposes a multipart part as a stream. It avoids the extra per-part allocation performed by ParseMultipart's Data field. The Reader is valid only during the callback.
func (*MultipartStreamPart) IsFile ¶
func (p *MultipartStreamPart) IsFile() bool
type PanicHandler ¶
type PanicHandler func(recovered any)
PanicHandler is invoked when user code panics inside an HTTP, async, WebSocket, defer, or body callback. HTTP paths emit a best-effort 500 when a response is still available. The argument is the recovered value (the panic payload).
The framework ships a default handler that prints the panic value plus a goroutine stack trace to stderr, so production deployments never have a panic disappear silently. Call SetPanicHandler with a custom function to route panics elsewhere (structured logger, error tracker), or pass nil to restore the default.
type PostAsyncHandler ¶
type PostAsyncHandler = BodyAsyncHandler
PostAsyncHandler is kept for source compatibility with earlier releases.
type PublishMessage ¶
PublishMessage is one entry in an App.PublishBatch call.
Topic is the exact subscriber topic string (no wildcards — see App.Publish). Message is the payload bytes. OpCode picks Text vs Binary framing per-message, so a single batch can mix the two.
type Reply ¶
type Reply struct {
Status int // defaults to 200 when zero
ContentType string // omits Content-Type header when empty
Body string
}
Reply is a static response captured once at registration time. Routes registered with this target are served entirely by the C++ event loop with no cgo callback per request — use it for /health, /version, cached config, or any constant response.
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request wraps a uWebSockets request. Sync handlers receive a Request backed by the live uWS HttpRequest; async/shared handlers receive a Request backed by a snapshot copied into the AsyncCtx before the original request was freed.
Both modes expose the same accessors. The shared snapshot has fixed capacity per field (URL 256, query 512, params 64 each up to 8, headers 8 KB total); requests past those caps are rejected with 431 before reaching user code.
func (*Request) Body ¶
Body returns the fully collected request body for body-async routes such as PostAsync, PutAsync, PatchAsync, and DeleteAsync; for other routes it returns nil. The slice is owned by the framework — do not retain it past the handler call.
func (*Request) BodyParser ¶
BodyParser deserializes the request body into out based on the request's Content-Type header. Supported media types:
- application/json — Config.JSONDecoder, defaulting to encoding/json
- application/x-www-form-urlencoded — form decoding into struct fields tagged with `form:"name"` (falls back to lower-cased field name when the tag is missing).
- multipart/form-data — same form-field decoding for non-file parts. File parts are ignored by BodyParser; use ParseMultipart (separate helper) to iterate them.
out must be a non-nil pointer (typically to a struct). Returns ErrNoBody when the body has not been collected, or ErrUnsupportedMediaType for a media type the parser does not handle. JSON / form parse errors surface verbatim from their respective packages so handlers can inspect them. JSON bodies use the App's Config.JSONDecoder when one is configured.
func (*Request) Context ¶
Context returns a context.Context bound to this request's lifetime. It is canceled (with a non-nil Err) when the client aborts the connection before the response is sent, so downstream calls that accept a context — db.QueryContext, http.NewRequestWithContext, rate-limited goroutines — will short-circuit instead of doing wasted work on behalf of a vanished caller.
The context is lazy: created on the first call and reused for subsequent calls on the same request. Handlers that never touch it pay nothing. It is canceled both on client abort and on handler completion (via the pool reset), so callbacks registered via context.AfterFunc fire reliably even on the success path.
For requests constructed outside the dispatch pipeline (e.g. test fixtures that allocate a Request directly), Context() returns context.Background() — usable but non-cancelable.
func (*Request) Cookie ¶
Cookie returns the value of a named cookie from the Cookie header, or "" if the cookie is absent. Linear scan; cache the value if you need it more than once. Cookie names are case-sensitive per RFC 6265.
func (*Request) CookieSigned ¶
CookieSigned reads the named cookie and verifies its signature against secrets. Returns the original (pre-signing) value when any secret validates, ("", false) otherwise — the same way Cookie returns "" when a cookie is absent.
user, ok := req.CookieSigned("session", secret)
if !ok {
res.Send(401, "text/plain; charset=utf-8", "unauthorized")
return
}
func (*Request) Get ¶
Get is an alias for Header (case-insensitive header lookup). Mirrors the req.get(name) helper that fiber / express users reach for first.
func (*Request) Header ¶
Header returns a request header value. Header lookups in async/shared handlers parse the snapshot buffer on every call; cache the value if you need it multiple times.
In sync mode the C++ dispatcher packs request headers into a stack scratch blob before calling into Go, so this scan is allocation-free and pays zero cgo per call. Ordinary misses return from Go; only requests whose headers overflow the 8 KB scratch buffer fall back to the uWS getHeader helper via cgo.
func (*Request) Headers ¶
Headers iterates every request header pair, lowercase name first. fn returns false to stop early — same convention as sync.Map.Range. Returns the number of headers visited.
In sync mode this walks the per-call scratch blob the C++ dispatcher packs into the request when the blob is complete. Requests whose header set overflows that scratch space fall back to one native full-header dump so trailing headers are not silently hidden. Async handlers walk the snapshot blob captured before the live request was freed. Names are returned in the order uWS parsed them, which matches the order on the wire.
Useful for middleware that copies headers verbatim (tracing context propagation, raw audit logs, etc.) without paying one Header(name) call per known header.
req.Headers(func(name, value string) bool {
out.Header(name, value)
return true
})
func (*Request) Hostname ¶
Hostname returns the host portion of the Host header, with any ":port" suffix stripped. Returns "" if the request has no Host header.
func (*Request) IP ¶
IP returns the formatted peer IP for this connection. For routes behind a proxy use IPs() and pick from the X-Forwarded-For chain instead — this returns the immediate TCP peer, which will be the proxy itself.
Sync handlers lazily cgo into uWS on first read and cache the result for subsequent reads. Async / shared handlers serve from the snapshot captured at request arrival.
func (*Request) IPs ¶
IPs parses the X-Forwarded-For header into a slice of normalized IPs in the order the proxies appended them (leftmost = original client). Returns nil if the header is absent, empty, or contains no valid IP entries. Empty and malformed entries are skipped; IPv4-mapped IPv6 addresses are unmapped.
Returns nil when the immediate peer is not trusted by Config.TrustProxy or Config.TrustedProxies. Without a trusted peer, the X-Forwarded-For header is attacker-controlled and any IP in it should be treated as untrusted input, not exposed via this helper. Callers that genuinely need the raw header value on an internet-facing server (rare, and almost always a logging mistake) can read it via req.Header("x-forwarded-for") and parse it themselves.
func (*Request) Local ¶
Local fetches a value previously stored with SetLocal. Returns nil if the key is absent.
func (*Request) Method ¶
Method returns the HTTP method ("get", "post", ...). uWS lower-cases it during parsing. Sync handlers serve this from the pre-cached method pointer the bridge stashed at handler entry; first call allocates a Go string, subsequent calls return the cached value — no cgo on the hot path.
func (*Request) Multipart ¶
func (r *Request) Multipart(fn func(*MultipartPart) error) error
Multipart is the Request-side wrapper for ParseMultipart. Use it from body-async handlers where the body is pre-collected; sync handlers should collect the body via Response.Body first and call ParseMultipart directly.
Returns ErrNoBody when the body has not been collected, or ErrUnsupportedMediaType when the request's Content-Type is not multipart/form-data.
func (*Request) MultipartStream ¶
func (r *Request) MultipartStream(opt MultipartOptions, fn func(*MultipartStreamPart) error) error
MultipartStream iterates multipart parts as streams.
func (*Request) MultipartWithOptions ¶
func (r *Request) MultipartWithOptions(opt MultipartOptions, fn func(*MultipartPart) error) error
MultipartWithOptions is Multipart with explicit limits.
func (*Request) Param ¶
Param looks up a route parameter by name. The name is the identifier written after ':' in the route pattern — e.g. for `/users/:id/posts/:postID` the names are "id" and "postID".
Returns "" if name was never declared in the route pattern (typo, or registered via a code path that bypasses the framework's wrapper). Use Parameter(index) for positional access when you know the index ahead of time.
app.Get("/users/:id/posts/:postID", func(res *gogo.Response, req *gogo.Request) {
id := req.Param("id")
post := req.Param("postID")
...
})
func (*Request) ParamInt ¶
ParamInt parses Param(name) as a signed decimal integer. Returns def when the param is missing or doesn't parse. Mirrors QueryInt.
func (*Request) ParamInt64 ¶ added in v0.2.0
ParamInt64 parses Param(name) as a signed decimal int64. Returns def when the param is missing or doesn't parse. Mirrors QueryInt64.
func (*Request) Parameter ¶
Parameter returns a route parameter by index. Returns "" for negative or out-of-range indices. Snapshot mode caps at 8 parameters; sync mode caches indices 0..3 inline (the bridge pre-fills them at handler entry — no cgo on first read) and falls back to the cgo getParameter helper for indices 4+.
func (*Request) ParameterInt ¶
ParameterInt parses the route parameter at index as a base-10 int. Missing or non-numeric values fall back to def. Positional twin of ParamInt(name, def); use whichever matches your access style.
func (*Request) ParameterInt64 ¶
ParameterInt64 parses the route parameter at index as a base-10 int64. Missing or non-numeric values fall back to def. Positional twin of ParamInt64(name, def); use whichever matches your access style.
func (*Request) Protocol ¶
Protocol returns "http" or "https". The framework itself only speaks plaintext — gogo is intended to run behind a TLS-terminating gateway such as nginx, an L7 load balancer, or a CDN. When the request's immediate peer is trusted by Config.TrustProxy or Config.TrustedProxies, the X-Forwarded-Proto header from the gateway is honored so the application sees the original client protocol; otherwise the answer is always "http" so an untrusted client can't spoof its way to appearing as https.
func (*Request) Query ¶
Query returns the raw query string portion of the URL with the leading '?' stripped. Returns "" if the request has no query string.
For parsed access, prefer QueryParam(key) for single keys or pass the result to net/url.ParseQuery for a full map.
Sync handlers serve from the pre-cached query pointer (no cgo) on first call; subsequent calls hit the cache.
func (*Request) QueryBool ¶
QueryBool parses the named query parameter as a boolean. Accepts "1", "true", "t", "yes", "y", "on" as true and "0", "false", "f", "no", "n", "off" as false (all case-insensitive). Missing or unrecognized values fall back to def. Empty string ("?flag&") is treated as missing — pass def=true if you want the bare-flag idiom.
func (*Request) QueryInt ¶
QueryInt parses the named query parameter as a base-10 int and returns it. Missing or non-numeric values fall back to def. Negative values are accepted.
func (*Request) QueryInt64 ¶
QueryInt64 parses the named query parameter as a base-10 int64. Missing or non-numeric values fall back to def.
func (*Request) QueryParam ¶
QueryParam returns the value of a single query parameter. Returns "" if the key is absent. Snapshot mode parses the raw query string on each call; cache the result if you need it more than once. Returns "" for an empty key.
func (*Request) Secure ¶
Secure reports whether the connection is encrypted (TLS / HTTPS). Honors X-Forwarded-Proto only when the immediate peer is trusted by Config.TrustProxy or Config.TrustedProxies.
func (*Request) SetLocal ¶
SetLocal stores a request-scoped value under key. Intended for passing state from middleware down to the handler (e.g. an authenticated user resolved by async middleware). The value lives only as long as the request — the map is cleared when the Request returns to its pool.
SetLocal is not safe for concurrent use within a single request; treat the Request as owned by whatever goroutine is currently running it.
func (*Request) Truncated ¶
Truncated reports whether an async request snapshot exceeded one of gogo's fixed capture buffers. The shared fast path rejects truncated requests before invoking handlers; this remains useful for diagnostics and future snapshot paths.
func (*Request) URL ¶
URL returns the request URL path. Query string is exposed separately via Query(); URL() does not include it.
In sync mode the bridge stashes the URL bytes uWS already parsed onto the Request at handler entry, so the first call materializes a Go string from those bytes (one allocation, no cgo). Subsequent calls return the cached string. Async/shared handlers read from the captured snapshot.
type Response ¶
type Response struct {
// contains filtered or unexported fields
}
Response wraps a uWebSockets response.
func (*Response) Append ¶
Append adds a header value without replacing existing ones. Identical in effect to Header — included for parity with fiber/express idiom where Set replaces and Append accumulates. uWS doesn't support replace, so both helpers do the same thing.
Use for multi-value headers: Set-Cookie, Vary, Link, etc.
func (*Response) Async ¶
func (r *Response) Async(fn func())
Async marks the response for asynchronous handling and runs fn on a new goroutine. After calling Async, subsequent Status/Header/Write calls buffer Go-side and End flushes the buffered response back onto the event loop with a single cork. fn may block freely.
Call Async at most once per response, and before any synchronous Status/Header/Write/End calls. The outer handler should return immediately after calling Async.
func (*Response) AwaitDrain ¶
AwaitDrain blocks the caller until BufferedAmount falls below threshold, or returns nil immediately if it's already below. It samples uWS's buffered byte counter at a short interval from the producer goroutine. The polling fallback is intentional: uWS's onWritable signal is edge-triggered and can be missed when a buffer drains before the callback is armed, which would otherwise park the stream permanently.
Only valid while a Stream is in flight (r.async != nil); calling outside that scope returns nil with no work done.
Returns ErrStreamAborted when the client disconnects before the buffer drains. Callers in a streaming loop should propagate the error to break out of their generator.
func (*Response) Body ¶
Body collects the full request body and invokes done once it has arrived. If the body exceeds the effective limit, done is called with err = ErrBodyTooLarge and the response is closed without sending. If the client aborts before the body completes, done is not called; use OnAborted or Request.Context for abort cleanup. Call inside the route handler before it returns; done runs on the loop thread (spawn a goroutine for blocking work).
The effective limit is the LOWER of maxBytes and Config.BodyLimit when BOTH are positive. A handler that asks for 10 MiB on an app configured with BodyLimit=1 MiB tops out at 1 MiB — the app cap wins. This mirrors the OnData and Content-Length gates so all three intake paths enforce the same upper bound; without the clamp here, a chunked upload (which sidesteps the C++ Content-Length pre-check) could exceed the app's BodyLimit whenever the handler's local cap was larger.
maxBytes <= 0 is honored literally and NOT widened by Config.BodyLimit: Body(0, ...) accepts a zero-byte body and rejects everything else; Body(-1, ...) rejects every chunk. Clamping is one-directional — the app cap can tighten the caller's request, never loosen it.
func (*Response) BufferedAmount ¶
BufferedAmount returns the byte count uWS has accepted for sending but not yet flushed to the kernel socket. Grows when the client isn't draining fast enough — the standard backpressure signal — and shrinks as the OS acknowledges sends.
Intended use is from inside a Stream callback to decide whether to pause emitting more bytes:
res.Stream(200, "text/event-stream", func(w io.Writer) error {
for event := range events {
// Yield to the loop when uWS has > 1 MiB buffered;
// otherwise a slow client can OOM the server with
// queued chunks.
if err := res.AwaitDrain(1 << 20); err != nil {
return err
}
if _, err := w.Write([]byte(event)); err != nil {
return err
}
}
return nil
})
The read is sampled from a worker goroutine without a loop hop — uWS's internal counter is an atomic-aligned size_t in uSockets, so reads are coherent but may lag by a few microseconds. Adequate for throttling decisions; do not use for transactional accounting.
func (*Response) Cork ¶
func (r *Response) Cork(fn func())
Cork batches all response writes inside fn into a single packet. Required when sending a response from a deferred callback to avoid uWebSockets warning about uncorked writes.
func (*Response) Download ¶
Download is SendFile plus Content-Disposition: attachment, which prompts browsers to save the body to disk instead of rendering it inline. filename overrides the suggested name in the header; pass "" to default to filepath.Base(path). The filename is wrapped in quoted-string form with quote / backslash escaped per RFC 6266; non-ASCII filenames receive an RFC 5987 filename* parameter so non-Latin-1 names survive transport.
SECURITY: path handling matches SendFile — no sanitization is performed, so never build path from untrusted request input; see the SendFile doc for the safe base-directory pattern.
func (*Response) Header ¶
Header writes a response header.
Each Header call appends to the wire output; calling Header twice with the same key emits two header lines (no replace semantic — uWS does not support that). For multi-value headers (Set-Cookie, Vary, Link) call Header / Append repeatedly with the same key.
In async mode Content-Type is short-circuited onto the async fast path (stored on r.async directly), and any other header is buffered in pendingHeaders. flushAsync routes pendingHeaders through uwsgo_res_defer_send_with_headers, which writes them between the status line and the body. That means the shared-memory fast path (zero cgo) is only available for responses that set Content-Type alone; the moment a handler attaches any extra header the response goes through the cgo defer-send shim instead.
func (*Response) JSON ¶
JSON marshals v and sends it with Content-Type: application/json. If marshalling fails the response is replaced with a generic 500 and the underlying marshal error is reported through the panic handler so the programmer sees it server-side without leaking type / package names to the network. With the default encoder, marshal failures happen for unsupported value shapes (channels, functions, cyclic structures), so failures here usually indicate a bug in caller code.
func (*Response) JSONBytes ¶
JSONBytes writes a pre-marshaled JSON body. Skips json.Marshal so handlers that already hold an encoded payload — cached responses, proxied bytes from another service, custom-encoder output (sonic, segmentio, etc.) — don't pay the reflection cost a second time.
The caller is responsible for the bytes being valid JSON; the framework does not validate. Content-Type is set to application/json automatically.
func (*Response) JSONP ¶
JSONP writes a JSONP response — the JSON-encoded value v wrapped in a function call named callback, served with Content-Type application/javascript. Useful for cross-origin reads from older clients that pre-date CORS; most modern apps should prefer JSON + proper CORS configuration via middleware.CORS.
The callback name is validated to contain only the JavaScript identifier characters [A-Za-z0-9_$.] so an attacker can't slip `</script>` or a closing parenthesis into the response and pivot the JSONP payload into an XSS sink. An invalid callback (empty or containing other characters) returns 400 with no body.
The marshaled JSON is also escaped against U+2028 / U+2029 — line separators that are legal in JSON but break JavaScript parsing when not escaped (each becomes a literal newline outside a string).
app.Get("/api/users", func(res *gogo.Response, req *gogo.Request) {
cb := req.QueryParam("callback")
res.JSONP(cb, []User{...})
})
func (*Response) JSONStream ¶
JSONStream emits a JSON body through a streaming encoder, avoiding the staging-buffer allocation that json.Marshal makes for the whole document. Useful for large arrays, NDJSON-style feeds, or any response whose JSON would otherwise dominate the handler's memory peak.
fn receives a *json.Encoder writing through a chunked response stream — each Encode call emits one JSON value followed by a newline (json.Encoder's default). For a single top-level array the caller is responsible for writing the framing characters themselves; for newline-delimited feeds Encode is enough. JSONStream intentionally uses encoding/json's streaming encoder rather than Config.JSONEncoder, whose contract is whole-value marshal. For custom codec output, marshal each value yourself and write through Response.Stream or send pre-marshaled bytes with Response.JSONBytes.
Async only — call from GetAsync, a body-async route, or wrap a sync handler in Response.Async. The underlying Response.Stream applies the default backpressure cap (StreamBackpressureBytes), so a slow consumer parks the producer goroutine rather than spiking memory.
func (*Response) Loop ¶
Loop returns the event loop that owns this response. Capture it inside the handler before spawning a goroutine. The returned Loop is safe to use from any goroutine; the Response itself is not.
func (*Response) OnAborted ¶
OnAborted registers an abort callback and returns an atomic flag that is set to true if the client disconnects before the response is sent. Must be called synchronously inside the route handler when responding asynchronously.
func (*Response) OnData ¶
OnData registers a body-chunk callback. fn is invoked once per chunk uWS receives, with isLast == true on the final chunk. Must be called inside the route handler, before it returns. The callback runs on the loop thread — spawn a goroutine inside it if the work can block.
Each chunk slice is freshly allocated; the caller owns it and may retain references after fn returns. OnData takes one wrapper ref at registration. The ref is released on isLast — at which point the cgo handle for the lambda is also freed. Body's onAborted handler releases this ref on early abort (when the last chunk would never fire). OnData registers a body-chunk callback. Each chunk is freshly allocated; the caller owns it. fn runs on the loop thread.
OnData takes one wrapper ref at registration and releases it on the final chunk (isLast == true). When the connection aborts before isLast fires the ref is held until the response is destroyed; for the Body() collector that case is covered explicitly via its own onAborted.
func (*Response) OnFinish ¶
func (r *Response) OnFinish(fn func())
OnFinish registers fn to fire after the response's handler — and any goroutine spawned via Response.Async — has fully completed. This is the safe hook for middleware that needs to persist or flush state derived from the request:
// Inside middleware, before calling next(res, req):
defer res.OnFinish(func() { saveSession(s) })
next(res, req)
The hook decides automatically which mode applies:
- If the handler never upgraded to async (sync route, no Response.Async call), fn runs synchronously now — the handler has already returned so its state is stable.
- If the handler went async via Response.Async or the route was GetAsync, fn is queued and runs inside finishAsync after the goroutine completes.
- If the async goroutine finished BEFORE the middleware reached OnFinish (fast handlers), fn runs inline so it's never orphaned.
Multiple registrations fire in FIFO order. Panics inside fn are captured by the framework's panic handler and do not prevent later callbacks from running, mirroring net/http's http.ResponseWriter recovery posture.
Without this hook, middleware that does `next(res, req); persist(state)` and a handler that calls Response.Async would silently persist pre-async state — the goroutine's mutations land after persist already ran.
Drain timing:
- Sync response: callbacks queue and fire from releaseRef, which runs from the framework's HTTP-handler defer AFTER any panic recovery and Send(500). This is what makes Logger / Metrics see status=500 when the handler panics rather than the pre-panic statusCode value.
- Async response (Response.Async or GetAsync): callbacks fire from finishAsync as soon as the goroutine completes, then releaseRef finds the queue empty when it eventually recycles.
- Late-registration race in async mode: if the goroutine drained and reset r.finished before the middleware reached OnFinish, the callback runs inline — the response wrapper is still alive because the caller still holds a ref.
func (*Response) Redirect ¶
Redirect sends an HTTP redirect to location with the given status code. Standard codes: 301 (moved permanently), 302 (found / temporary, common default), 303 (see other — POST → GET), 307 (temp, preserves method), 308 (permanent, preserves method). Status 0 defaults to 302.
CRLF / NUL in location is treated as a programming or input-validation bug: the framework refuses to write the bad header and responds 500 instead of panicking. This keeps a hostile request from amplifying into a panic-handler trigger when handler code passes user input straight to Redirect (e.g. res.Redirect(req.QueryParam("next"), 302)).
SECURITY: gogo does NOT validate that location stays within your own host. Passing user-controlled input here without a host allow-list is an open-redirect bug — attackers can craft links that look like they land on your site but bounce to a phishing page. Sanitize the target (compare against a known list of paths or hostnames) before calling Redirect.
In async mode this schedules a Cork on the loop so the status, Location header, and empty body go out as a single packet; do not call Send / End on the same response afterwards.
func (*Response) Render ¶
Render renders a named template with data and writes the result as a 200 text/html response. Set Content-Type via res.Header before calling Render if the engine emits something other than HTML (XML, plain text, etc.).
Errors from the engine — missing template, parse failure, execution failure — surface as a 500 response with no body detail leaked to the client; the error itself is routed to the panic logger so operators can diagnose.
app.Get("/users/:id", func(res *gogo.Response, req *gogo.Request) {
u, err := db.LoadUser(req.Param("id"))
if err != nil {
res.Send(404, "text/plain", "not found")
return
}
res.Render("user/profile", map[string]any{"user": u})
})
func (*Response) SSE ¶
SSE prepares the response as a Server-Sent Events stream and runs fn against a writer that knows the SSE frame format. Auto- installed headers:
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive X-Accel-Buffering: no (defeats nginx proxy_buffering)
Internally delegates to Response.Stream, so SSE inherits its constraints: async-only (call from GetAsync, a body-async route, or res.Async), streaming chunked-encoded body, panic-on-sync-handler.
fn returns when the stream is complete — typically because the upstream event source closed, a context cancellation fired, or the client disconnected while the producer was parked on backpressure. Use errors.Is(err, ErrStreamAborted) to treat disconnects as normal client churn. The error fn returns is the error SSE returns; the stream itself is always closed cleanly regardless.
func (*Response) Send ¶
Send writes status code, an optional Content-Type header, and body in one call. The framework picks the lowest-overhead path based on the handler's mode:
- Sync handler (Get / Post / Any): one cgo crossing into uWS.
- Async handler (GetAsync / small PostAsync) with body up to 8 KB: ZERO cgo per request — written to shared-memory inline buffers and pushed onto the App's response ring; the loop drains it.
- Async handler with body > 8 KB: cgo Loop::defer with the full payload, status, and Content-Type.
Pass an empty contentType to omit the header.
func (*Response) SendFile ¶
SendFile reads path from disk and writes it as the response body. Content-Type is picked from the file extension via mime.TypeByExtension; for unknown extensions the first 512 bytes are passed through http.DetectContentType. Last-Modified and a weak ETag derived from (size, mtime) are set automatically.
Conditional requests are honored: If-None-Match against the ETag and If-Modified-Since against the file mtime each short-circuit to 304 Not Modified with no body. Single-byte range requests on req (`Range: bytes=start-end`, `bytes=start-`, `bytes=-suffix`) return 206 Partial Content with the requested slice; multi-range and non-bytes units fall through to the full 200.
The helper refuses files larger than MaxSendFileBytes (default 100 MiB) and returns ErrFileTooLarge. Returns an error when path is missing, a directory, or unreadable; the response is left untouched in that case so the caller can decide what to send.
Memory usage is bounded by SendFileChunkBytes (default 64 KiB) + SendFileBackpressureBytes (default 1 MiB) per concurrent call, regardless of file size. The body is streamed via chunked transfer-encoding using Response.Stream, so a slow consumer applies backpressure to the read loop rather than buffering the entire file.
From a sync handler SendFile transparently upgrades to async via Response.Async so the uWS loop thread isn't blocked on disk I/O; the function returns nil immediately after the open/stat/range prep, and the body streams from a goroutine.
SECURITY: path is opened exactly as given — the framework performs no sanitization, so a path built from request input (route params, query strings, form fields) lets clients traverse outside the intended directory with ".." segments or absolute paths. Never pass untrusted input directly; resolve it against a fixed base directory first and verify the result stays inside it:
p := filepath.Join(base, filepath.Clean("/"+req.Param("name")))
Cleaning rooted at "/" strips every ".." before the join, so p cannot escape base. Symlinks under base are still followed; keep the tree free of links pointing outside it. See docs/file-serving.md for the full rooted-path and symlink guidance.
func (*Response) SetBodyEncoder ¶
func (r *Response) SetBodyEncoder(encode func(body []byte, contentType string) (encoded []byte, contentEncoding string))
SetBodyEncoder installs a body transformer to apply right before the response body is flushed to uWS. The encoder receives the concatenated bytes from any Write / End / Send / JSON calls, and returns the encoded body together with a Content-Encoding value to attach (or empty string to skip encoding and emit the original bytes unchanged).
Compression middleware uses this hook to transparently gzip / brotli the handler's output without requiring handlers to be aware of the encoding. The encoder runs in both sync and async modes; when the encoder emits a Content-Encoding header, the response is routed through the defer-send-with-headers C shim (async fast shared-memory path is bypassed because it has no slot for extra headers).
Only one encoder per response — calling SetBodyEncoder a second time replaces the first. Once the encoder has run (on End or Send) it is consumed; subsequent writes go through the normal direct path.
func (*Response) SetBodyEncoderLimit ¶
func (r *Response) SetBodyEncoderLimit(maxBytes int, encode func(body []byte, contentType string) (encoded []byte, contentEncoding string))
SetBodyEncoderLimit is SetBodyEncoder plus a staging-buffer cap. When maxBytes is positive and the buffered body would grow beyond it, the encoder is bypassed and the response continues unencoded from the original bytes. This lets middleware such as Compress bound both compression work and the pre-compression staging buffer.
func (*Response) SetCookie ¶
SetCookie writes a Set-Cookie response header. Every field that reaches the wire is validated against the relevant grammar before serialization:
- Name: RFC 6265 cookie-name (token grammar, no CTL/separator)
- Value: RFC 6265 cookie-octet
- Path / Domain / Expires: reject CTLs and ";" so user-controlled input cannot inject additional attributes by smuggling a semicolon (e.g. Path="/; Domain=evil.com; Secure=false")
- SameSite: exact match against "", "Strict", "Lax", "None"
Invalid characters panic — they typically indicate a programming bug or untrusted input reaching a Cookie field unprotected. SetCookieSigned routes through SetCookie so this validation covers signed cookies too.
Multiple calls append multiple Set-Cookie headers; browsers handle them independently.
func (*Response) SetCookieSigned ¶
SetCookieSigned signs c.Value with the first secret, replaces c.Value with the signed form, and writes the cookie via SetCookie. All of SetCookie's validation and header semantics apply (Path / Domain / Max-Age / Secure / HttpOnly / SameSite).
Pair with Request.CookieSigned on the read side to verify and recover the original value:
res.SetCookieSigned(gogo.Cookie{
Name: "session",
Value: "alice:42",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: gogo.SameSiteStrict,
MaxAge: 3600,
}, secret)
func (*Response) Status ¶
Status sets the HTTP status code. The standard reason phrase from net/http is appended automatically (e.g. 200 → "200 OK", 404 → "404 Not Found"); unknown codes are written as the bare number.
func (*Response) StatusCode ¶
StatusCode returns the last status code passed to Status / Send / JSON. Returns 200 (the uWS default) if no status was ever set. Useful from middleware that needs to observe how a handler responded — e.g. a logger that records the final status.
func (*Response) Stream ¶
Stream writes a chunked HTTP/1.1 response by handing the caller an io.Writer that buffers each Write call onto the uWS loop's deferred-write queue. Order is preserved (FIFO), so chunks reach the wire in the same order the writer emits them.
Stream is meant for Server-Sent Events, NDJSON feeds, log tails, and similar long-running responses where you don't know the full body up front and you don't want to materialize it before the first byte goes out. Call it from a GetAsync or body-async handler. On a sync route call res.Async() first; on a plain sync handler streaming would block the event-loop thread, which is almost never what you want.
app.GetAsync("/events", func(res *gogo.Response, req *gogo.Request) {
err := res.Stream(200, "text/event-stream", func(w io.Writer) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
if _, err := fmt.Fprintf(w, "data: tick %d\n\n", i); err != nil {
return err
}
}
return nil
})
if err != nil { /* logged via reportPanic */ }
})
Headers buffered via res.Header before Stream go out in the initial response frame. After Stream returns, the response is closed — do not call Send / End / Stream again on the same response.
uWS automatically applies HTTP/1.1 transfer-encoding: chunked when no Content-Length is set, which is the expected mode for streaming. If the client disconnects mid-stream the underlying AsyncCtx is marked aborted and subsequent Write calls become silent no-ops on the C side. If the stream is parked on backpressure, Write and AwaitDrain return ErrStreamAborted so callers can stop their generator early.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router scopes middleware and a path prefix to a subtree of routes. Created by App.Group or Router.Group. Routes registered through a Router have the Router's prefix prepended and inherit the Router's middleware stack on top of the App's global / scoped middleware.
Group scope is identity-based — a route is wrapped because it is registered through this Router, not because its pattern string starts with some prefix. That avoids the pattern/URL mismatch that App.Use(prefix, mw) has to guard against at request time, so Group middleware composes at registration with zero per-request cost.
Prefer Group over App.Use(prefix, mw) for scoping new code.
func (*Router) DeleteAsync ¶
func (r *Router) DeleteAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
DeleteAsync registers a DELETE route under this Router that collects the body up to maxBodyBytes then runs handler on a goroutine. On bodies over the cap the framework sends 413; on Config.BodyReadTimeout it sends 408. In both cases the handler is not called.
func (*Router) Get ¶
Get registers a GET route under this Router. Target follows the same rules as App.Get: Handler, func, Reply, string, []byte. Static targets take the zero-cgo path only when neither middleware nor typed-param constraints need to run; otherwise the static body is served by a synthetic dynamic handler.
func (*Router) GetAsync ¶
func (r *Router) GetAsync(pattern string, handler AsyncHandler)
GetAsync registers a GET route under this Router that runs on a goroutine. Uses the zero-cgo shared-memory dispatch path only when no sync-only middleware (group or app) touches this route.
func (*Router) Group ¶
Group creates a nested Router. The child prefix is appended to the parent's prefix and middleware is inherited then extended — mws here run inside the parent group's middleware.
func (*Router) Name ¶
Name tags a route pattern under this Router's prefix for App.URL reverse routing. The pattern is the same one you pass to Router.Get/Post/etc.; the router prefix is applied automatically.
api := app.Group("/api/v1")
api.Get("/users/:id", showUser)
api.Name("api.user.show", "/users/:id")
url, _ := app.URL("api.user.show", map[string]string{"id": "42"})
// url == "/api/v1/users/42"
func (*Router) PatchAsync ¶
func (r *Router) PatchAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
PatchAsync registers a PATCH route under this Router that collects the body up to maxBodyBytes then runs handler on a goroutine. On bodies over the cap the framework sends 413; on Config.BodyReadTimeout it sends 408. In both cases the handler is not called.
func (*Router) PostAsync ¶
func (r *Router) PostAsync(pattern string, maxBodyBytes int, handler PostAsyncHandler)
PostAsync registers a POST route under this Router that collects the body up to maxBodyBytes then runs handler on a goroutine. On bodies over the cap the framework sends 413; on Config.BodyReadTimeout it sends 408. In both cases the handler is not called.
func (*Router) PutAsync ¶
func (r *Router) PutAsync(pattern string, maxBodyBytes int, handler BodyAsyncHandler)
PutAsync registers a PUT route under this Router that collects the body up to maxBodyBytes then runs handler on a goroutine. On bodies over the cap the framework sends 413; on Config.BodyReadTimeout it sends 408. In both cases the handler is not called.
func (*Router) Use ¶
Use appends middleware to this Router. Applies to every route subsequently registered through this Router (or any child Group created after this call). Bundled middleware uses its placement hint the same way it does with App.Use: sync-only middleware runs on the sync route path, async-only middleware runs only on async routes, and PlaceBoth middleware runs once in the right chain for each route type.
func (*Router) UseAsync ¶
func (r *Router) UseAsync(mws ...AsyncMiddleware)
UseAsync appends async middleware to this Router. Applies only to GetAsync and body-async routes registered through this Router.
func (*Router) WebSocket ¶
func (r *Router) WebSocket(pattern string, behavior WebSocketBehavior)
WebSocket registers a WebSocket route under this Router. Middleware does not run around WebSocket upgrade — uWS does not expose a chain at the upgrade boundary.
type RunMultiCoreOptions ¶ added in v1.3.1
type RunMultiCoreOptions struct {
// Mode controls accepted-socket distribution. Zero selects
// MultiCoreAuto.
Mode MultiCoreMode
// WorkerHintLoops overrides the loop count used to size the default
// GetAsync worker pool. Zero keeps the mode default: reuseport uses one
// loop, balanced uses n loops. Use SetWorkerCount for an exact worker
// count instead of a loop-count hint.
WorkerHintLoops int
}
RunMultiCoreOptions configures RunMultiCoreWithOptions.
type SSEEvent ¶
type SSEEvent struct {
// ID is the event identifier (the `id:` field). Browsers
// stash the last received ID and re-send it as the
// Last-Event-ID header when they reconnect after a network
// blip, so handlers can resume from that point.
ID string
// Event names the event type (the `event:` field). Defaults
// to "message" in browser EventSource listeners; set this
// when you want client code to listen for a specific name
// via addEventListener.
Event string
// Data is the event payload (the `data:` field). Accepted:
// nil — empty data
// string — sent verbatim (multi-line strings auto-
// split into per-line data: emissions)
// []byte — same as string
// error — err.Error() text
// anything else — json.Marshal'd
Data any
// Retry sets the `retry:` field (milliseconds). Tells the
// browser EventSource to wait this long before reconnecting
// after a disconnect. 0 omits the field (browser default,
// ~3 s).
Retry int
}
SSEEvent is one Server-Sent Events frame. Every field is optional; an SSEEvent with only Data set behaves the same as Send(data).
type SSEStream ¶
type SSEStream struct {
// contains filtered or unexported fields
}
SSEStream is the writer handed to the SSE callback. Send / SendEvent / Comment / Ping all write directly to the underlying HTTP stream — there's no batching, so a Send call equals one SSE frame on the wire.
func (*SSEStream) Comment ¶
Comment writes a comment frame — lines starting with ':' that SSE-compliant clients silently consume. The canonical use is a keepalive ping every ~30 s so idle proxies / NAT tables don't time out the connection. Multi-line text is split into multiple `:` lines.
s.Comment("keepalive") // : keepalive\n\n
s.Comment("multi\nline") // : multi\n: line\n\n
func (*SSEStream) Ping ¶
Ping is a zero-argument keepalive shorthand: ": ping\n\n". Suitable to call periodically from a ticker inside the SSE callback to keep the connection warm across proxies that time out idle TCP.
func (*SSEStream) Send ¶
Send writes data as a one-line SSE frame with the default event name ("message"). Shorthand for SendEvent(SSEEvent{Data: data}).
s.Send("hello") // data: hello\n\n
s.Send(map[string]int{"n": 1}) // data: {"n":1}\n\n
s.Send([]byte("raw")) // data: raw\n\n
Multi-line strings are emitted as one `data:` per line per the SSE spec — browsers re-assemble them into a single payload at the EventSource.onmessage boundary.
func (*SSEStream) SendEvent ¶
SendEvent writes a full SSE frame, honoring every populated field of e. The output ordering matches the spec:
id: <ID>\n (omitted when ID == "") event: <Event>\n (omitted when Event == "") retry: <Retry>\n (omitted when Retry == 0) data: <line>\n (one per line of Data) \n (blank line terminates the frame)
Multi-line Data values produce one data: line each, exactly as EventSource expects.
type SameSite ¶
type SameSite string
SameSite is the value of the SameSite cookie attribute. Empty means the attribute is not emitted (browser default applies).
type TemplateEngine ¶
TemplateEngine is the contract every templating implementation satisfies. Render writes the named template, parameterized by data, into w. Returning an error tells the framework to surface a 500 to the client (with the error reported via the panic logger); nil means the bytes in w are the rendered response.
Engines are responsible for context-aware escaping; the framework performs no additional sanitization on the bytes that Render emits.
func NewHTMLTemplateEngine ¶
func NewHTMLTemplateEngine(opt HTMLTemplateOptions) TemplateEngine
NewHTMLTemplateEngine builds an html/template-backed engine. Templates are named by their path relative to Root, with the suffix stripped — `views/user/profile.tmpl` is invoked as `user/profile`.
type TestServer ¶
type TestServer struct {
// contains filtered or unexported fields
}
TestServer is a running App bound to a loopback port — built to be driven from unit tests. The constructor takes a setup callback that registers routes, middleware, and any other per-App state; Close stops the server and waits for the loop goroutine to exit.
The server runs on a real port so every cgo path the framework takes in production fires the same way under test (middleware chains, sync wrapping, snapshot-then-async dispatch, the zero-cgo shared-memory ring, …). The trade-off vs an in-process dispatcher is per-request latency (~50–100 µs vs ~1 µs) and the need for the OS to allocate one port per server — both acceptable for typical unit tests.
func NewTestServer ¶
func NewTestServer(setup func(*App)) (*TestServer, error)
NewTestServer starts an App on a free port, runs the setup callback before Listen, and waits for the listening socket to accept connections before returning. Native builds own the uWS loop on an internal locked goroutine, so tests do not need runtime.LockOSThread. NewTestServer serializes TestServer lifetimes because the native binding has process-wide shared worker and ring state; this keeps parallel tests from running multiple native Apps in the same process at once. The returned TestServer drives the running App via its http.Client; Close shuts the App down cleanly.
ts, err := gogo.NewTestServer(func(app *gogo.App) {
app.Get("/users/:id", showUser)
app.Use(middleware.Logger())
})
if err != nil { t.Fatal(err) }
defer ts.Close()
req := httptest.NewRequest("GET", "/users/42", nil)
resp, err := ts.Do(req)
if err != nil { t.Fatal(err) }
body, _ := io.ReadAll(resp.Body)
// assert body / resp.StatusCode / resp.Header
func NewTestServerT ¶
func NewTestServerT(tb testing.TB, setup func(*App)) *TestServer
NewTestServerT starts a TestServer and registers Close with tb.Cleanup. It fails the test immediately when setup, Listen, or readiness checks fail.
ts := gogo.NewTestServerT(t, func(app *gogo.App) {
app.Get("/ping", func(res *gogo.Response, req *gogo.Request) {
res.Send(200, "text/plain", "pong")
})
})
resp, err := ts.Get("/ping")
func NewTestServerTWithOptions ¶ added in v0.6.0
func NewTestServerTWithOptions(tb testing.TB, setup func(*App) error, opts TestServerOptions) *TestServer
NewTestServerTWithOptions starts a TestServer with options and registers Close with tb.Cleanup. It fails the test immediately when setup returns an error, panics, Listen fails, or readiness checks fail.
func NewTestServerWithOptions ¶ added in v0.6.0
func NewTestServerWithOptions(setup func(*App) error, opts TestServerOptions) (*TestServer, error)
NewTestServerWithOptions starts a TestServer using opts and a setup callback that can fail without panicking. setup runs before Listen and is the place to register routes, middleware, fallback handlers, named routes, lifecycle hooks, and other startup-time App state.
ts, err := gogo.NewTestServerWithOptions(func(app *gogo.App) error {
if err := installRoutes(app); err != nil {
return err
}
return nil
}, gogo.TestServerOptions{StartupTimeout: 10 * time.Second})
func (*TestServer) App ¶
func (ts *TestServer) App() *App
App exposes the underlying *App for runtime interactions and inspection, such as publishing to a topic, calling Shutdown, or reading registered route metadata. Route and middleware registration belongs in the setup callback before the helper calls Listen; registering routes or middleware through App after the TestServer has started is outside the TestServer contract.
func (*TestServer) Client ¶
func (ts *TestServer) Client() *http.Client
Client returns the *http.Client wired to talk to this server. Cookies / redirects / other client-side behaviour can be tuned by setting fields on the returned client; the call site shares the same client across requests.
func (*TestServer) Close ¶
func (ts *TestServer) Close()
Close shuts the App down gracefully and waits for the loop goroutine to return. Safe to call from any goroutine, but only the first call does work; subsequent calls are no-ops.
func (*TestServer) Do ¶
Do rewrites the request URL to point at the test server, then sends it via the test client. The request method, headers, and body are sent unchanged; the URL's scheme and host are overwritten and the path / raw query are preserved.
Use this for requests built via httptest.NewRequest (which sets the URL to a placeholder host) or any custom *http.Request:
req := httptest.NewRequest("POST", "/login", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := ts.Do(req)
func (*TestServer) Get ¶
func (ts *TestServer) Get(path string) (*http.Response, error)
Get is a convenience wrapper for a GET to the given path (relative to the server URL).
func (*TestServer) Port ¶
func (ts *TestServer) Port() int
Port returns the loopback port the server is listening on.
func (*TestServer) URL ¶
func (ts *TestServer) URL() string
URL returns the http://host:port base for this server. Use it to build URLs manually when you need control over the path.
type TestServerOptions ¶ added in v0.6.0
type TestServerOptions struct {
// Config is passed to NewApp after the helper fills BindAddr with
// 127.0.0.1. TestServer always binds loopback; leave BindAddr empty, or
// set it to 127.0.0.1 explicitly.
Config Config
// StartupTimeout bounds setup, Listen, and readiness checks. The zero value
// uses the default five-second timeout.
StartupTimeout time.Duration
// Client is used by Do, Get, and Post. Nil installs the helper's default
// client. Close calls Client.CloseIdleConnections before shutting down the
// server.
Client *http.Client
}
TestServerOptions configures NewTestServerWithOptions.
The zero value is ready to use: the server binds to 127.0.0.1, startup waits up to five seconds, and Client returns an http.Client with keep-alives disabled and a ten-second request timeout.
type UpgradeContext ¶
type UpgradeContext struct {
// contains filtered or unexported fields
}
UpgradeContext is passed to WebSocketBehavior.Upgrade for every incoming WebSocket handshake. It carries a snapshot of the request (URL, query, headers, peer IP, offered subprotocols) and exposes Accept / Reject to drive the response.
Lifetime is the duration of the Upgrade callback only. Do NOT store the pointer; the underlying C++ context is freed the moment the callback returns.
func (*UpgradeContext) Accept ¶
func (c *UpgradeContext) Accept(protocol string)
Accept completes the WebSocket handshake. protocol is the subprotocol to echo back in the Sec-WebSocket-Protocol response header — pass one of Protocols() or "" to skip negotiation.
After Accept returns, the connection is live and the Open callback will fire shortly. Calling Accept more than once on the same context is a no-op.
func (*UpgradeContext) Header ¶
func (c *UpgradeContext) Header(name string) string
Header reads a request header by name (case-insensitive). The header blob is parsed on every call — cache if you need a header multiple times.
func (*UpgradeContext) IP ¶
func (c *UpgradeContext) IP() string
IP returns the canonical peer IP address as a printable string.
func (*UpgradeContext) Method ¶
func (c *UpgradeContext) Method() string
Method returns the HTTP method of the upgrade request — always "get" in practice for a real WebSocket handshake, but exposed for completeness.
func (*UpgradeContext) Protocols ¶
func (c *UpgradeContext) Protocols() []string
Protocols returns the list of subprotocols the client offered in the Sec-WebSocket-Protocol header. Whitespace around each entry is trimmed. Pick one with Accept(name) to echo it back; passing "" accepts without naming a protocol.
func (*UpgradeContext) Query ¶
func (c *UpgradeContext) Query() string
Query returns the raw query string portion of the URL (no leading '?'). Use QueryParam to read a specific key.
func (*UpgradeContext) QueryParam ¶
func (c *UpgradeContext) QueryParam(name string) string
QueryParam reads a named query parameter. Linear scan; cache the result if you need it more than once.
func (*UpgradeContext) Reject ¶
func (c *UpgradeContext) Reject(status int, body string)
Reject refuses the upgrade with an HTTP status and plain-text body. The connection is closed; no open / message / close callbacks fire. Calling Reject more than once is a no-op.
if !validToken(ctx.Header("authorization")) {
ctx.Reject(401, "unauthorized")
return
}
func (*UpgradeContext) SetUserData ¶
func (c *UpgradeContext) SetUserData(v any)
SetUserData stashes a Go value on the upgrade context. On Accept the framework wraps it in a cgo.Handle and attaches it to the resulting WebSocket so handlers can read it via ws.UserData(). The handle is released automatically when the WebSocket closes.
Calling SetUserData(nil) clears any previously-stashed value.
func (*UpgradeContext) URL ¶
func (c *UpgradeContext) URL() string
URL returns the upgrade request URL path (no query string).
type WSHub ¶
type WSHub struct {
// contains filtered or unexported fields
}
WSHub coordinates WebSocket topic publishes across all App instances attached to this process, and optionally across processes through an adapter such as RedisWSHubAdapter.
Register routes through hub.WebSocket so the hub can tell which App owns each socket. That lets PublishFrom skip the sender without calling WebSocket.Publish from a non-loop goroutine.
func NewWSHub ¶
func NewWSHub(opts ...WSHubOption) *WSHub
NewWSHub creates a WebSocket hub. With no adapter, it still fans out across every App attached in the current process, which is enough for RunMultiCore.
func (*WSHub) Attach ¶
Attach includes app in process-local fan-out. It is called automatically by WebSocket, but is useful when routes are registered manually. PublishFrom still needs sockets registered through WebSocket or Wrap. Attach does not start the adapter; call Start at boot when you want fail-fast behavior.
func (*WSHub) Close ¶
Close stops the adapter subscription and adapter workers. It is idempotent and does not close any attached App.
func (*WSHub) Publish ¶
Publish broadcasts to every local App attached to the hub, then queues the message for the adapter if present. It is safe to call from any goroutine. Adapter publish failures are reported asynchronously through WithWSHubAdapterErrorHandler.
func (*WSHub) PublishBatch ¶
func (h *WSHub) PublishBatch(msgs []PublishMessage) error
PublishBatch broadcasts many messages with one loop-local batch per attached App, then queues each message for the adapter. For local fan-out this keeps the same batching advantage as App.PublishBatch without re-entering RunMultiCore peer fan-out. Adapter publish failures are reported asynchronously through WithWSHubAdapterErrorHandler.
func (*WSHub) PublishFrom ¶
PublishFrom broadcasts from a WebSocket handler and skips the sender. It is safe to call from any goroutine, but ws must have been registered through WebSocket or Wrap so the hub can identify the sender. Adapter publish failures are reported asynchronously through WithWSHubAdapterErrorHandler.
func (*WSHub) Start ¶
Start starts the adapter subscription, if an adapter is configured. It is idempotent after a successful start and can be retried after transient adapter failures. Publish, PublishBatch, PublishFrom, and topic reconciliation also start the adapter lazily.
func (*WSHub) Subscribe ¶
Subscribe enrolls ws in topic. It is a small convenience wrapper around WebSocket.Subscribe so user code can stay hub-centered.
func (*WSHub) Unsubscribe ¶
Unsubscribe removes ws from topic.
func (*WSHub) WebSocket ¶
func (h *WSHub) WebSocket(app *App, pattern string, behavior WebSocketBehavior)
WebSocket attaches app to the hub and registers a WebSocket route whose callbacks are wrapped so PublishFrom can avoid duplicate local delivery.
func (*WSHub) Wrap ¶
func (h *WSHub) Wrap(app *App, behavior WebSocketBehavior) WebSocketBehavior
Wrap returns a WebSocketBehavior that tracks socket ownership. Use this when a Router is registering the WebSocket route:
router.WebSocket("/ws", hub.Wrap(app, behavior))
type WSHubAdapter ¶
type WSHubAdapter interface {
Start(ctx context.Context, deliver func(WSHubMessage)) error
Publish(ctx context.Context, msg WSHubMessage) error
Close() error
}
WSHubAdapter bridges WSHub messages across processes or hosts.
Implementations call deliver for every message received from outside the hub. The hub ignores messages carrying its own NodeID, so adapters can use brokers such as Redis Pub/Sub that echo a process's publish back to all subscribers, including itself.
Start must be safe to call more than once. After it has successfully started receiving, later Start calls should return nil without creating duplicate receive loops. When Start returns an error, the hub treats the adapter as not started and may retry. The deliver callback is safe for adapters to call from adapter-owned goroutines, including concurrently; any ordering guarantee is provided by the adapter and broker.
Publish should respect ctx and return when the message has been accepted by the adapter or broker, or when delivery cannot be attempted. The hub does not retry failed Publish calls after they leave its queue. Close must be idempotent and should stop receive loops, unblock context-aware operations, and release adapter-owned resources.
type WSHubMessage ¶
WSHubMessage is the transport-neutral message shape used by WSHubAdapter.
type WSHubOption ¶
type WSHubOption func(*WSHub)
WSHubOption customizes a WSHub.
func WithWSHubAdapter ¶
func WithWSHubAdapter(adapter WSHubAdapter) WSHubOption
WithWSHubAdapter installs a distributed adapter, for example Redis. The adapter is started by Start or lazily by the first adapter publish or topic reconciliation.
func WithWSHubAdapterErrorHandler ¶
func WithWSHubAdapterErrorHandler(fn func(error)) WSHubOption
WithWSHubAdapterErrorHandler receives asynchronous adapter errors from the hub worker, including Start, Publish, and topic reconciliation failures. The default reports rate-limited errors through SetPanicHandler.
func WithWSHubAdapterPublishTimeout ¶
func WithWSHubAdapterPublishTimeout(timeout time.Duration) WSHubOption
WithWSHubAdapterPublishTimeout bounds each adapter Publish, Subscribe, and Unsubscribe operation. This keeps shutdown from waiting indefinitely on a slow or half-open Redis connection.
func WithWSHubAdapterQueueSize ¶
func WithWSHubAdapterQueueSize(size int) WSHubOption
WithWSHubAdapterQueueSize sets the bounded async adapter queue used by Publish, PublishBatch, and PublishFrom. Larger queues absorb Redis/network bursts without blocking the WebSocket loop thread; a full queue returns ErrWSHubAdapterQueueFull after local fan-out has already happened.
func WithWSHubAdapterTopicWorkers ¶
func WithWSHubAdapterTopicWorkers(workers int) WSHubOption
WithWSHubAdapterTopicWorkers sets how many goroutines reconcile adapter topic subscriptions. The default is 1. Multiple workers may reconcile different topics in parallel, but the same topic is never reconciled concurrently.
func WithWSHubAdapterWorkers ¶
func WithWSHubAdapterWorkers(workers int) WSHubOption
WithWSHubAdapterWorkers sets how many goroutines call adapter.Publish for queued messages. The default is 1, which preserves adapter publish call order from the hub queue. Larger values can improve Redis throughput when cross-process message ordering is not required.
func WithWSHubCloseTimeout ¶
func WithWSHubCloseTimeout(timeout time.Duration) WSHubOption
WithWSHubCloseTimeout bounds each Close wait phase for adapter workers. Close first lets queued adapter work drain, then cancels the hub context and waits again before returning ErrWSHubCloseTimeout.
func WithWSHubNodeID ¶
func WithWSHubNodeID(id string) WSHubOption
WithWSHubNodeID sets the process/node identifier used to ignore messages that this hub already delivered locally before publishing to an adapter.
type WSHubTopicAdapter ¶
type WSHubTopicAdapter interface {
WSHubAdapter
Subscribe(ctx context.Context, topic string) error
Unsubscribe(ctx context.Context, topic string) error
}
WSHubTopicAdapter is an optional extension for adapters that can subscribe only to topics with local subscribers. WSHub calls these methods asynchronously from its adapter worker when the first local socket subscribes to a topic and when the last local socket leaves.
Topic operations are reconciled to the latest local state, not replayed as a subscribe/unsubscribe event log. The hub retries failed topic operations until they succeed or the hub closes.
type WebSocket ¶
type WebSocket struct {
// contains filtered or unexported fields
}
WebSocket wraps a uWebSockets WebSocket connection. Its methods touch uWS loop-thread-local state; use them only from WebSocket callbacks on the owning loop. Do not retain a WebSocket and call Send / SendText / End from worker goroutines. For cross-goroutine fan-out use App.Publish, App.PublishBatch, or WSHub.
func (*WebSocket) End ¶
End closes the WebSocket connection from the owning uWS loop thread. Calling End from a worker goroutine is not supported.
func (*WebSocket) Publish ¶
Publish broadcasts message to every OTHER subscriber of topic, including subscribers on peer RunMultiCore loops. uWS deliberately excludes the publishing socket from its own broadcast — per WebSocket.h: "Publish as sender, does not receive its own messages even if subscribed to relevant topics" — so if you want every subscriber including the caller, use App.Publish instead.
opcode picks the WebSocket frame type (Text or Binary). Returns true when the message was queued for delivery to at least one subscriber on the publishing socket's local loop. In RunMultiCore mode, peer-loop fanout is fire-and-forget and is not reflected in this return value.
This is the fast path for the publishing socket's local loop: no cross-thread defer, no message copy, just a cgo crossing into uWS's TopicTree publish. In RunMultiCore, peer loops are reached by scheduling one copied publish per other App, so the peer fan-out cost is O(peer loops). App.Publish exists for the worker-goroutine case where you don't have a live WebSocket pointer on the loop thread.
Calling this from off the loop thread (e.g. a goroutine you spawned from a handler) corrupts uWS state — the WebSocket pointer is only valid on the loop.
func (*WebSocket) Send ¶
Send sends a WebSocket message from the owning uWS loop thread. It returns false when uWS did not queue the message, for example because the socket is closing or already above its backpressure limit.
func (*WebSocket) SendText ¶
SendText sends a text WebSocket message from the owning uWS loop thread. It returns false under the same conditions as Send.
func (*WebSocket) SetUserData ¶
SetUserData overwrites the per-socket user-data value after the connection is established (e.g. from Open). Releases the previous handle if there was one; pass nil to clear without installing a replacement.
func (*WebSocket) Subscribe ¶
Subscribe enrolls this WebSocket as a subscriber to topic. Future App.Publish / WebSocket.Publish calls targeting the same topic string deliver to this connection. Returns true when the subscription is now active (either added by this call or already in place).
Topics are exact-match strings — uWS's TopicTree in v20 does not support MQTT-style "+" / "#" wildcards. Build your own fan-out scheme (e.g. subscribe to every relevant topic at connect time) if you need pattern matching.
Must be called from inside an Open / Message / Close handler — the underlying TopicTree is loop-thread-local; calling Subscribe from a worker goroutine corrupts uWS state.
func (*WebSocket) Unsubscribe ¶
Unsubscribe removes this WebSocket's subscription to topic. Returns true when a subscription existed and was removed. Like Subscribe, must be called from a WebSocket handler.
type WebSocketBehavior ¶
type WebSocketBehavior struct {
// Open runs after the handshake succeeds.
Open func(*WebSocket)
// Message runs for incoming text and binary messages.
Message func(*WebSocket, []byte, OpCode)
// Close runs after the connection closes. Use it for cleanup only;
// the socket is no longer a live send target.
Close func(*WebSocket, int, []byte)
// MaxPayloadLength is the largest single incoming message the
// server will accept. Frames over this cap cause uWS to close the
// connection. Default 16 MiB.
MaxPayloadLength int
// IdleTimeout is the maximum time a WebSocket may sit idle (no
// frames in either direction) before uWS closes it. Default 120s.
IdleTimeout time.Duration
// MaxBackpressure is the bytes uWS will queue per-socket for a
// slow consumer before closing the connection. Protects the
// loop from being held hostage by a single non-draining client.
// Default 64 KiB. The public WebSocket API exposes this cap and
// the bool returned by Send / SendText; it does not expose a
// per-socket BufferedAmount, AwaitDrain, or drain callback.
MaxBackpressure int
// DisablePings turns off uWS's built-in ping/pong keepalive.
// Default (zero) leaves automatic pings ON so an idle connection
// doesn't get reaped by NAT boxes; set true only if your client
// drives its own heartbeat. Ping/pong callbacks are not part of
// the public API; use Message for application-level heartbeats.
DisablePings bool
// UnsafeAutoUpgrade restores uWS's legacy default of accepting every
// WebSocket handshake when Upgrade is nil. The secure default rejects
// browser handshakes that carry an Origin header unless the app installs
// an explicit Upgrade callback. Only set this for public, non-cookie
// endpoints where cross-origin WebSocket access is intentional.
UnsafeAutoUpgrade bool
// Upgrade runs synchronously on the uWS loop thread for every
// incoming WebSocket handshake before the connection is
// established. The callback inspects request headers / query /
// peer IP / offered subprotocols and MUST call ctx.Accept(...) or
// ctx.Reject(...) before returning — failing to do either causes
// the framework to refuse the upgrade with a 500 (and log the
// misuse).
//
// Typical uses:
//
// - subprotocol negotiation: pick one of ctx.Protocols()
// - cookie / token auth at handshake time, with the resolved
// identity stashed via ctx.SetUserData(user) for the rest
// of the connection's lifetime
// - rejecting based on origin / referrer / API quota
//
// SECURITY: when Upgrade is nil the framework accepts non-browser
// handshakes that omit Origin, but rejects browser handshakes that
// carry Origin. The HTTP middleware chain (CORS in particular) does
// NOT cover the WebSocket upgrade path, so a Same-Origin Policy gate
// that protects fetch() does NOT protect WebSocket(). If the endpoint
// uses session cookies or any other ambient credential, an attacker
// page can open ws://yoursite/... from the user's browser and ride the
// user's session — Cross-Site WebSocket Hijacking (CSWSH).
//
// At minimum verify the Origin header against an allow-list. The
// middleware package ships middleware.WebSocketAuth for the
// common case (origin check + optional bearer / cookie /
// subprotocol gating).
Upgrade func(*UpgradeContext)
// contains filtered or unexported fields
}
WebSocketBehavior contains callbacks and per-route limits for a WebSocket endpoint. Callbacks run on the owning uWS loop thread and must not block. Limit fields default to safe production values when zero; pick explicit numbers when you need different limits, do not leave them at zero hoping for "unlimited".
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
adapters
|
|
|
examples
|
|
|
authmw
command
authmw demonstrates a production-ish middleware stack:
|
authmw demonstrates a production-ish middleware stack: |
|
graceful
command
graceful demonstrates signal-driven single-app graceful shutdown.
|
graceful demonstrates signal-driven single-app graceful shutdown. |
|
hello
command
hello demonstrates the smallest gogo~ server: one sync route and one async route, plus a static reply served entirely from C++.
|
hello demonstrates the smallest gogo~ server: one sync route and one async route, plus a static reply served entirely from C++. |
|
httpadapter
command
httpadapter demonstrates using stdlib net/http handlers inside gogo.
|
httpadapter demonstrates using stdlib net/http handlers inside gogo. |
|
multicore
command
|
|
|
restapi
command
restapi demonstrates a tiny in-memory REST API with GET/POST/JSON + route params + query strings.
|
restapi demonstrates a tiny in-memory REST API with GET/POST/JSON + route params + query strings. |
|
sse
command
sse demonstrates the simplest SSE pattern in gogo: register an async route, install Server-Sent Events via res.SSE, and emit events on a timer with a keepalive ping every few seconds.
|
sse demonstrates the simplest SSE pattern in gogo: register an async route, install Server-Sent Events via res.SSE, and emit events on a timer with a keepalive ping every few seconds. |
|
upload
command
upload demonstrates POST body collection with a max-body limit.
|
upload demonstrates POST body collection with a max-body limit. |
|
websocket
command
websocket demonstrates a browser-friendly WebSocket route:
|
websocket demonstrates a browser-friendly WebSocket route: |
|
internal
|
|
|
mwhint
Package mwhint carries the placement metadata that the bundled middleware in gogo/middleware uses to tell gogo.App.Use which chain a middleware belongs to.
|
Package mwhint carries the placement metadata that the bundled middleware in gogo/middleware uses to tell gogo.App.Use which chain a middleware belongs to. |
|
Package middleware bundles common HTTP middleware ready to drop into any gogo App or Router.
|
Package middleware bundles common HTTP middleware ready to drop into any gogo App or Router. |