Go HTTP Framework

Built for
raw speed.
Shipped clean.

Breeze is an event-driven HTTP framework for Go built on gnet. Zero-allocation hot paths, a composable middleware chain, native WebSocket support with a built-in hub, and Swagger docs — out of the box.

Faster than goFiber
~0
Allocs in hot path
9
Built-in middlewares
main.go

Every decision made
for performance.

Not another wrapper around net/http. Breeze is built from the ground up on a non-blocking event loop with an obsession over allocations.

gnet Event Loop
Non-blocking I/O with one event-loop goroutine per CPU core. RoundRobin load balancing across reactors. No goroutine-per-connection.
🔀
Zero-Alloc Router
Path segment matching uses a stack-allocated [16]string. Param indexes are pre-computed at registration. The hot loop never allocates on a miss.
🧠
Zero-Copy Parsing
unsafe.String converts raw bytes to strings without copying. Header keys are lowercased in-place. No bytes.Split — just indexed scans.
🔧
Worker Pool
Fixed goroutine pool with 16× buffered channel absorbs bursts. Queue-full fallback spawns a goroutine rather than blocking the event loop.
🔌
Native WebSocket
RFC 6455 WebSocket built directly into the gnet event loop. Zero goroutine-per-connection. Built-in hub for broadcast, per-client send, and connection count.
📄
OpenAPI 3.1 Built-In
Annotate routes with Go structs. Breeze reflects types at startup to generate a live Swagger spec. No YAML. No codegen. Just /swagger.
🛡️
9 Middleware Suite
JWT auth with refresh tokens, rate limiting, compression (Brotli/Gzip/Deflate), ETag cache, CORS, security headers, logger, panic recovery.
📁
File Uploads
Multipart form parsing with size limits and content-type sniffing built into Context. One-line save-to-disk API. No stdlib http.Request required.
🔒
Copy-On-Write Headers
Shared package-level header maps for JSON/HTML/text responses. SetHeader upgrades to a private copy lazily — zero allocation unless you mutate.

3× faster.
Not a typo.

Measured on a simple JSON endpoint, same machine, same payload. The gap widens further under pipelining and high concurrency.

Breeze ~630k req/s
Fiber ~210k req/s
Echo ~140k req/s
Gin ~120k req/s
net/http ~85k req/s

* Approximate. Run benchmarks in your environment for authoritative numbers.

Throughput vs std goFiber on plaintext endpoints
~0
Heap allocs per request on static routes
16×
Worker pool channel buffer — never stalls the event loop
512B
Compact threshold — GC can reclaim large receive buffers

Production-ready.
Day one.

Every middleware is a HandlerFunc — composable, testable, and zero-dependency on each other.

go
import (
    "github.com/golang-jwt/jwt/v5"
    middleware "github.com/nelthaarion/breeze/middlewares"
)

// Generate tokens
accessToken, _ := middleware.GenerateJWT(
    "secret", jwt.MapClaims{"user_id": "abc", "role": "admin"},
    15*time.Minute, nil,
)
refreshToken, _ := middleware.GenerateRefreshToken("refresh-secret", jwt.MapClaims{
    "user_id": "abc",
}, 7*24*time.Hour, nil)

// Apply middleware
router.Use(middleware.JWTAuthMiddleware(middleware.JWTOptions{
    AccessSecret:       "secret",
    RefreshSecret:      "refresh-secret",
    SigningMethod:      jwt.SigningMethodHS256,
    EnableRefreshToken: true,         // auto-rotate expired access tokens
    RequiredRoles:      []string{"admin"},
    UserContextKey:     "user",        // stored in ctx.Param("user")
    ClaimsValidator: func(claims jwt.MapClaims) bool {
        return claims["active"] == true
    },
    OnUnauthorized: func(ctx *breeze.Context, err error) {
        ctx.Status(401)
        ctx.JSON(map[string]string{"error": err.Error()})
    },
}))
go
router.Use(middleware.CORSMiddleware(middleware.CORSOptions{
    AllowOrigins:     "https://myapp.com",
    AllowMethods:     "GET,POST,PUT,DELETE,OPTIONS",
    AllowHeaders:     "Content-Type,Authorization",
    AllowCredentials: "true",
    MaxAge:           "86400",  // preflight cache: 24h
}))

// OPTIONS preflight is handled automatically → 204 No Content
go
// 100 requests per minute per IP
router.Use(middleware.NewRateLimiter(middleware.RateLimiterOptions{
    Requests: 100,
    Per:      time.Minute,
    Message:  "Slow down — rate limit exceeded",
}))

// Different limits per route
router.Handle(breeze.POST, "/login", loginHandler,
    middleware.NewRateLimiter(middleware.RateLimiterOptions{
        Requests: 5,
        Per:      time.Minute,
    }),
)
// Returns 429 Too Many Requests when exceeded
go
// Automatically picks best encoding from Accept-Encoding header
// Priority: br (Brotli) → gzip → deflate → none
router.Use(middleware.CompressionMiddleware())

// Sets Content-Encoding response header automatically:
//   br       → github.com/andybalholm/brotli
//   gzip     → compress/gzip (stdlib)
//   deflate  → compress/flate (stdlib)
go
// Safe defaults in one line
router.Use(middleware.DefaultSecurityMiddleware())

// Or fully custom
router.Use(middleware.SecurityMiddleware(middleware.SecurityOptions{
    ContentSecurityPolicy:     "default-src 'self'",
    XFrameOptions:             "DENY",
    XContentTypeOptions:       "nosniff",
    StrictTransportSecurity:   "max-age=63072000; includeSubDomains; preload",
    ReferrerPolicy:            "no-referrer",
    XXSSProtection:            "1; mode=block",
    CrossOriginEmbedderPolicy: "require-corp",
    CrossOriginOpenerPolicy:   "same-origin",
    CacheControl:              "no-store, no-cache, must-revalidate",
}))
go
// 1. Enable Swagger (before routes)
router.Use(middleware.SwaggerMiddleware(router, middleware.SwaggerOptions{
    Title:    "My API",
    Version:  "1.0.0",
    JSONPath: "/swagger.json",
    UIPath:   "/swagger",
}))

// 2. Annotate routes with Go structs — no YAML, no codegen
router.Handle(breeze.POST, "/users", createUser,
    middleware.DocPOST("/users", swagger.RouteDoc{
        Title: "Create user",
        Tags:  []string{"Users"},
        Input: []swagger.InputGroup{
            {Type: swagger.InputBody, Fields: CreateUserRequest{}, Required: true},
        },
        Output:       UserResponse{},
        OutputStatus: 201,
    }),
)

// Visit /swagger → live Swagger UI
// Visit /swagger.json → raw OpenAPI 3.1 JSON
go
// Register a WebSocket endpoint — returns the shared WSHub
app := breeze.New(router, pool)

hub := app.WebSocket("/ws", &breeze.WSHandlerFunc{
    Connect: func(conn *breeze.WSConn) {
        conn.SendText("welcome")
    },
    Message: func(conn *breeze.WSConn, opcode byte, payload []byte) {
        hub.BroadcastText(string(payload))  // echo to all clients
    },
    Close: func(conn *breeze.WSConn, code uint16, reason string) {
        hub.BroadcastText("a user left")
    },
})

// Query active connections from any HTTP handler
router.Handle(breeze.GET, "/ws/stats", func(ctx *breeze.Context) {
    ctx.JSON(map[string]int64{"connections": hub.Count()})
})

Installation

Requires Go 1.24.3 or later.

shell
go get github.com/nelthaarion/breeze

The module pulls in gnet v2 for the event loop, go-json for fast JSON marshaling, brotli for compression, and golang-jwt/jwt for authentication. No transitive surprises.

Quick Start

A complete working server in under 20 lines.

go
package main

import (
    "runtime"
    "github.com/nelthaarion/breeze"
    middleware "github.com/nelthaarion/breeze/middlewares"
)

func main() {
    router := breeze.NewRouter()

    router.Use(middleware.RecoveryMiddleware())
    router.Use(middleware.LoggingMiddleware())

    router.Handle(breeze.GET, "/", func(ctx *breeze.Context) {
        ctx.JSON(map[string]string{"status": "ok"})
    })

    router.Handle(breeze.GET, "/users/:id", func(ctx *breeze.Context) {
        ctx.JSON(map[string]string{"id": ctx.Param("id")})
    })

    pool := breeze.NewWorkerPool(runtime.NumCPU())
    app  := breeze.New(router, pool)
    app.Run(3000, true)  // port, multiCore
}

Server Config

app.Run(port, multiCore) starts the gnet event loop with sensible defaults baked in.

OptionValueEffect
TCPNoDelayenabledDisables Nagle's algorithm — lower latency for small messages
MulticoreconfigurableSpawns one event-loop per CPU core when true
LoadBalancingRoundRobinDistributes connections evenly across event loops
go
app.Run(8080, true)   // multicore — one loop per CPU, recommended
app.Run(8080, false)  // single-core — useful for dev / debugging

Router

Create a router, register global middleware, then define routes. Routes support static segments, named parameters, and wildcards.

Methods

go
router := breeze.NewRouter()

// Global middleware (runs on every route)
router.Use(middleware.LoggingMiddleware())

// Static segment
router.Handle(breeze.GET,    "/health",        healthHandler)

// Named parameter — :id available via ctx.Param("id")
router.Handle(breeze.GET,    "/users/:id",     getUser)
router.Handle(breeze.POST,   "/users",         createUser)
router.Handle(breeze.PUT,    "/users/:id",     updateUser)
router.Handle(breeze.DELETE, "/users/:id",     deleteUser)

// Wildcard — *filepath captures everything after /files/
router.Handle(breeze.GET, "/files/*filepath", fileHandler)

// Per-route middleware (runs only on this route)
router.Handle(breeze.POST, "/admin/action", adminHandler,
    authMiddleware, auditMiddleware,
)

Route Matching Priority

Routes are matched in registration order. A static segment always beats a named parameter when both could match — register more specific routes first.

PatternMatchesParams
/users/users
/users/:id/users/abcid=abc
/users/:id/posts/users/abc/postsid=abc
/files/*path/files/a/b/c.txtpath=a/b/c.txt

Context

Every handler receives a *breeze.Context. It carries the connection, parsed request, response, route params, and controls the middleware chain.

Response Helpers

go
// JSON — sets Content-Type: application/json, status 200
ctx.JSON(map[string]any{"id": 1, "name": "Alice"})

// Plain text
ctx.WriteString("Hello, World!")

// HTML
ctx.HTML([]byte("<h1>Hello</h1>"))

// Override status code
ctx.Status(201)  // call after JSON/WriteString/HTML

// Add/override a header
ctx.SetHeader("X-Request-Id", "uuid-123")

Middleware Chain

go
// Advance to the next handler in the chain
ctx.Next()

// Short-circuit — skip all remaining handlers
ctx.Abort()

// Typical middleware pattern
func AuthMiddleware(ctx *breeze.Context) {
    token := ctx.Req.Header["authorization"]
    if token == "" {
        ctx.Status(401)
        ctx.WriteString("Unauthorized")
        ctx.Abort()  // ← stops the chain here
        return
    }
    ctx.Next()  // ← continue to next handler
}

Request

ctx.Req is a *breeze.HTTPRequest parsed from raw bytes with zero unnecessary allocations.

go
// Method, Path, Headers
method := ctx.Req.Method          // breeze.Method ("GET", "POST", …)
path   := ctx.Req.Path            // "/users/123"
ct     := ctx.Req.Header["content-type"]  // headers are lowercased

// Body
var payload CreateUserRequest
json.Unmarshal(ctx.Req.Body, &payload)

// Query string
page  := ctx.Query("page")   // "2"
limit := ctx.Query("limit")  // "20"

Response

ctx.Res is a *breeze.HTTPResponse. Breeze serializes it to raw HTTP/1.1 bytes using strconv.AppendInt — no fmt.Sprintf, pre-sized buffer, array-indexed status text.

FieldTypeNotes
StatusintHTTP status code (200, 404, …)
Headersmap[string]stringCopy-on-write — safe to mutate via SetHeader
Body[]byteRaw response body

You rarely set ctx.Res directly — use the helpers (JSON, WriteString, HTML, Status, SetHeader) instead.

Params & Query

go
// Route: /users/:id/posts/:postId
userID := ctx.Param("id")         // "abc"
postID := ctx.Param("postId")     // "42"

// Query: /search?q=go&page=2
q    := ctx.Query("q")            // "go"
page := ctx.Query("page")        // "2"

// Middleware can pass data downstream via params
ctx.SetParam("userID", "from-auth")
// in handler:
uid := ctx.GetParam("userID")   // "from-auth"

// Get a snapshot of all params
all := ctx.GetParams()           // map[string]string (copy)

Logger middleware

Logs method, path, status, and elapsed time for every request. Output goes to stdout in RFC3339 format.

go
router.Use(middleware.LoggingMiddleware())
// Output: [Breeze][2026-06-23T12:00:00Z] GET /users -> 200 (1.2ms)

Panic Recovery middleware

Wraps the entire handler chain in a defer/recover. On panic: logs the value + full stack trace, sets 500, calls ctx.Abort(). Your server never crashes from a handler bug.

go
router.Use(middleware.RecoveryMiddleware())
// Register first so it wraps everything else

CORS middleware

Sets Access-Control-* headers and handles OPTIONS preflight automatically.

go
router.Use(middleware.CORSMiddleware(middleware.CORSOptions{
    AllowOrigins:     "*",
    AllowMethods:     "GET,POST,PUT,DELETE",
    AllowHeaders:     "Content-Type,Authorization",
    ExposeHeaders:    "X-Request-Id",
    AllowCredentials: "true",
    MaxAge:           "86400",
}))

Helmet middleware

Sets 12 security-hardening HTTP headers. Use the opinionated defaults or configure each header individually.

go
// One-liner safe defaults
router.Use(middleware.DefaultSecurityMiddleware())

// Custom
router.Use(middleware.SecurityMiddleware(middleware.SecurityOptions{
    ContentSecurityPolicy:   "default-src 'self'; img-src *",
    XFrameOptions:           "SAMEORIGIN",
    StrictTransportSecurity: "max-age=31536000",
}))
HeaderDefault
Content-Security-Policydefault-src 'self'
X-Frame-OptionsDENY
X-Content-Type-Optionsnosniff
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload
X-XSS-Protection1; mode=block
Referrer-Policyno-referrer
Cache-Controlno-store, no-cache, must-revalidate

JWT Auth middleware

Full JWT authentication with optional refresh token rotation, RBAC, and custom claims validation. Tokens are looked up from the Authorization: Bearer header by default.

go
// Generate
token, _   := middleware.GenerateJWT("secret", jwt.MapClaims{
    "user_id": "u-123", "role": "admin",
}, 15*time.Minute, nil)

refresh, _ := middleware.GenerateRefreshToken("refresh-secret",
    jwt.MapClaims{"user_id": "u-123"}, 7*24*time.Hour, nil)

// Validate
router.Use(middleware.JWTAuthMiddleware(middleware.JWTOptions{
    AccessSecret:       "secret",
    RefreshSecret:      "refresh-secret",
    EnableRefreshToken: true,  // new token returned in X-New-Access-Token
    RequiredRoles:      []string{"admin", "editor"},
    UserContextKey:     "user",
}))

Custom Token Extraction

go
// Read from cookie instead of header
TokenLookup: func(ctx *breeze.Context) (string, string, error) {
    access  := ctx.Req.Header["x-access-token"]
    refresh := ctx.Req.Header["x-refresh-token"]
    if access == "" {
        return "", "", fmt.Errorf("token missing")
    }
    return access, refresh, nil
},

Rate Limiter middleware

In-memory token counter per remote IP. Resets after each Per window. Returns 429 Too Many Requests with a configurable message.

go
// Global limit
router.Use(middleware.NewRateLimiter(middleware.RateLimiterOptions{
    Requests: 100,
    Per:      time.Minute,
}))

// Strict limit on a specific route
router.Handle(breeze.POST, "/login", loginHandler,
    middleware.NewRateLimiter(middleware.RateLimiterOptions{
        Requests: 5,
        Per:      time.Minute,
        Message:  "Too many login attempts",
    }),
)

ETag Cache middleware

Computes an MD5 ETag from the response body. On subsequent requests with a matching If-None-Match, returns 304 Not Modified with an empty body — saving bandwidth.

go
cache := middleware.NewETagCache()
router.Use(cache.ETagMiddleware())

// Client flow:
// → GET /data
// ← 200 ETag: "abc123"
// → GET /data  If-None-Match: "abc123"
// ← 304 Not Modified (empty body)

Compression middleware

Negotiates the best encoding from the client's Accept-Encoding header. Priority: Brotli → Gzip → Deflate → none.

go
router.Use(middleware.CompressionMiddleware())
// Sets Content-Encoding: br / gzip / deflate automatically

Swagger / OpenAPI 3.1 new

Annotate routes at registration time using Go structs. Breeze reflects the types to produce a live OpenAPI 3.1 spec — no YAML, no code generation, no build step.

go
// Types — use json + description + example struct tags
type CreateUserRequest struct {
    Name  string `json:"name"  description:"Full name"    example:"Alice"`
    Email string `json:"email" description:"Email address" example:"alice@example.com"`
    Age   int    `json:"age"   description:"Age in years" example:"30"`
}
type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Enable once before routes
router.Use(middleware.SwaggerMiddleware(router, middleware.SwaggerOptions{
    Title:    "Users API",
    Version:  "2.0.0",
    JSONPath: "/swagger.json",
    UIPath:   "/swagger",
}))

// Annotate each route
router.Handle(breeze.POST, "/users", createUser,
    middleware.DocPOST("/users", swagger.RouteDoc{
        Title: "Create user",
        Tags:  []string{"Users"},
        Input: []swagger.InputGroup{
            {Type: swagger.InputBody, Fields: CreateUserRequest{}, Required: true},
        },
        Output:       UserResponse{},
        OutputStatus: 201,
    }),
)

router.Handle(breeze.GET, "/users/:id", getUser,
    middleware.DocGET("/users/:id", swagger.RouteDoc{
        Title: "Get user",
        Tags:  []string{"Users"},
        Input: []swagger.InputGroup{
            {Type: swagger.InputParams, Fields: struct{ ID string `json:"id"` }{}},
            {Type: swagger.InputQuery,  Fields: struct{ Fields string `json:"fields,omitempty"` }{}},
        },
        Output: UserResponse{},
    }),
)

InputGroup Types

ConstantSourceOpenAPI Location
swagger.InputBodyJSON request bodyrequestBody
swagger.InputQueryURL query paramsquery
swagger.InputParamsPath parameterspath
swagger.InputHeaderRequest headersheader

WebSocket built-in

Breeze implements RFC 6455 WebSocket directly inside the gnet event loop — no goroutine-per-connection, no third-party WS library. After the HTTP upgrade handshake, the connection is promoted to WebSocket mode. All subsequent traffic for that file descriptor bypasses the HTTP parser entirely and is handled by the WebSocket frame engine.

WebSocket routes are registered on the *Breeze app (not the router) using app.WebSocket(), which returns the shared *WSHub immediately. The hub can be injected into your handler at construction time and used from any goroutine.

go
package main

import (
    "fmt"
    "runtime"
    "github.com/nelthaarion/breeze"
)

// ── Struct-based handler ─────────────────────────────────────────────
type ChatHandler struct {
    hub *breeze.WSHub
}

func (h *ChatHandler) OnConnect(conn *breeze.WSConn) {
    fmt.Printf("[ws] connected: %s (total: %d)\n", conn.RemoteAddr(), h.hub.Count())
    h.hub.BroadcastExcept(breeze.WsOpText, []byte("a user joined"), conn)
}

func (h *ChatHandler) OnMessage(conn *breeze.WSConn, opcode byte, payload []byte) {
    if opcode == breeze.WsOpText {
        msg := fmt.Sprintf("[%s]: %s", conn.RemoteAddr(), string(payload))
        h.hub.BroadcastText(msg)
    } else {
        conn.SendBinary(payload) // echo binary back
    }
}

func (h *ChatHandler) OnClose(conn *breeze.WSConn, code uint16, reason string) {
    h.hub.BroadcastText("a user left")
}

func main() {
    router := breeze.NewRouter()
    pool   := breeze.NewWorkerPool(runtime.NumCPU())
    app    := breeze.New(router, pool)

    // WebSocket() registers the route and returns the hub immediately.
    chat := &ChatHandler{}
    chat.hub = app.WebSocket("/ws", chat)

    app.Run(3000, true)
}

Architecture note

On every incoming data event, Breeze checks a sync.Map (O(1), lock-free) to see if the file descriptor belongs to a promoted WebSocket connection. If it does, the HTTP parser is never invoked — the data goes straight to the WebSocket frame engine. This means WebSocket connections carry zero HTTP overhead after the upgrade and HTTP connections carry zero WebSocket overhead.

Handler Interface websocket

Implement WSHandler on a struct, or use the inline WSHandlerFunc shorthand. Fields left nil in WSHandlerFunc are silently skipped.

go
// Full interface — implement on your own struct
type WSHandler interface {
    OnConnect(conn *WSConn)
    OnMessage(conn *WSConn, opcode byte, payload []byte)
    OnClose(conn *WSConn, code uint16, reason string)
}

// Inline shorthand — nil fields are no-ops
app.WebSocket("/ws/echo", &breeze.WSHandlerFunc{
    Connect: func(conn *breeze.WSConn) {
        conn.SendText("echo server ready")
    },
    Message: func(conn *breeze.WSConn, opcode byte, payload []byte) {
        conn.Send(opcode, payload)  // reflect back verbatim
    },
    // Close field omitted — no-op on disconnect
})

Opcode constants

ConstantValueUse
breeze.WsOpText0x1UTF-8 text message
breeze.WsOpBinary0x2Binary message (any payload)

Ping, Pong, and Close opcodes are handled automatically by the engine and are never surfaced to OnMessage.

WSConn websocket

*WSConn is the per-client handle passed to every callback. It wraps the underlying gnet.Conn with WebSocket framing and is safe to call from any goroutine.

go
// Send a text message
conn.SendText("hello")

// Send binary data
conn.SendBinary([]byte{0x01, 0x02})

// Send with explicit opcode
conn.Send(breeze.WsOpText, []byte("raw"))

// Graceful close — sends a Close frame, then closes the TCP connection
// Code 1000 = Normal Closure, 1001 = Going Away, 1008 = Policy Violation, …
conn.Close(1000, "bye")

// Remote address of the client
addr := conn.RemoteAddr()  // e.g. "192.168.1.5:54321"
MethodSignatureNotes
Send(opcode byte, payload []byte) errorLow-level send with explicit opcode
SendText(msg string) errorConvenience wrapper for text frames
SendBinary(msg []byte) errorConvenience wrapper for binary frames
Close(code uint16, reason string)Sends Close frame; idempotent
RemoteAddr() stringClient IP:port string

Message fragmentation

Breeze handles RFC 6455 fragmentation transparently. Continuation frames are accumulated in a per-connection buffer and OnMessage is only called once the final frame (FIN=1) arrives. The default maximum payload size per message is 4 MiB. Protocol errors or oversized payloads trigger an automatic Close with code 1002 or 1009.

WSHub websocket

*WSHub is the shared broadcast hub — one instance per *Breeze app, regardless of how many WebSocket routes are registered. It is safe to call from any goroutine. Broadcast builds the WebSocket frame once and delivers it to all clients concurrently via the worker pool.

go
// Retrieve the hub from the app at any point after WebSocket() is called
hub := app.Hub()  // returns nil if no WS routes registered

// Broadcast text to every connected client
hub.BroadcastText("server restarting in 10s")

// Broadcast binary to every connected client
hub.BroadcastBinary(protobufPayload)

// Broadcast with explicit opcode
hub.Broadcast(breeze.WsOpText, []byte("raw"))

// Broadcast to all clients except one (e.g. exclude the sender)
hub.BroadcastExcept(breeze.WsOpText, []byte(msg), senderConn)

// Active connection count (atomic read — no lock)
n := hub.Count()  // int64

// Expose connection count via HTTP
router.Handle(breeze.GET, "/ws/stats", func(ctx *breeze.Context) {
    ctx.JSON(map[string]int64{"connections": hub.Count()})
})
MethodSignatureNotes
Broadcast(opcode byte, payload []byte)Frame built once, delivered to all
BroadcastText(msg string)Convenience wrapper
BroadcastBinary(msg []byte)Convenience wrapper
BroadcastExcept(opcode byte, payload []byte, skip *WSConn)Skips one connection
Count() int64Atomic, no lock

Multiple WebSocket routes

All routes registered with app.WebSocket() share the same hub. BroadcastText reaches clients on all WS paths. If you need isolated rooms, implement per-room filtering in OnMessage using BroadcastExcept or maintain your own client sets.

go
// Multiple endpoints — same hub is returned each time
hub1 := app.WebSocket("/ws/chat", chatHandler)
hub2 := app.WebSocket("/ws/events", eventsHandler)
// hub1 == hub2 == app.Hub()

Close Codes websocket

Breeze sends and receives standard RFC 6455 close codes. Your OnClose callback receives the code and reason string extracted from the Close frame. If the TCP connection drops without a Close frame (network loss, RST), Breeze synthesises code 1006 with reason "abnormal closure".

CodeNameWhen
1000Normal ClosureClean close initiated by either side
1001Going AwayServer shutting down or client navigating away
1002Protocol ErrorMalformed frame — Breeze closes automatically
1003Unsupported DataUnknown opcode — Breeze closes automatically
1006Abnormal ClosureTCP disconnect without Close frame (synthesised)
1008Policy ViolationApplication-level rejection (use in your handler)
1009Message Too BigPayload exceeds wsMaxPayloadDefault (4 MiB)
1011Internal ErrorUnexpected server-side condition
go
// Reject unauthenticated connections after upgrade
func (h *SecureHandler) OnConnect(conn *breeze.WSConn) {
    if !isAuthenticated(conn) {
        conn.Close(1008, "policy violation: not authenticated")
        return
    }
    conn.SendText("authenticated")
}

// Inspect why a connection closed
func (h *SecureHandler) OnClose(conn *breeze.WSConn, code uint16, reason string) {
    switch code {
    case 1000:
        fmt.Println("clean disconnect")
    case 1006:
        fmt.Println("connection dropped unexpectedly")
    default:
        fmt.Printf("closed code=%d reason=%q\n", code, reason)
    }
}

File Uploads

Parse multipart forms and save files to disk — no dependency on http.Request.

go
// Parse all parts at once
files, fields, err := ctx.ParseMultipart(10 << 20) // 10 MB max per file
if err != nil {
    ctx.Status(400); ctx.WriteString(err.Error()); return
}

uf := files["avatar"][0]
fmt.Println(uf.Filename, uf.ContentType, uf.Size)

// Or save directly to disk
saved, err := ctx.SaveUploadedFile("avatar", "./uploads/avatar.jpg", 5<<20)
if err != nil {
    ctx.Status(400); ctx.WriteString(err.Error()); return
}
ctx.JSON(map[string]string{"saved": saved})

UploadedFile Fields

FieldTypeDescription
FieldstringForm field name
FilenamestringOriginal filename from the browser
ContentTypestringFrom Content-Type header or auto-sniffed
Sizeint64Size in bytes
Content[]byteRaw file bytes

Static Files

go
// Serve ./public/* at /assets/*
router.ServeStatic("/assets", "./public")

// Auto-serve ./public/index.html at GET / (no explicit route needed)
// Directory traversal is sanitized with filepath.Clean
// MIME types resolved by extension, fallback to content sniffing

Worker Pool

Off-loads handler execution from the gnet event loop so slow handlers don't block all connections on a reactor.

go
// Size to NumCPU (good for I/O-bound handlers)
pool := breeze.NewWorkerPool(runtime.NumCPU())

// Custom concurrency
pool := breeze.NewWorkerPool(32)

// Graceful shutdown (wait up to 5s for in-flight tasks)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool.Shutdown(ctx)

// Pass nil to skip the pool and use raw goroutines
app := breeze.New(router, nil)
ScenarioBehaviour
Queue has capacityTask enqueued normally (channel buffer = workers × 16)
Queue full (burst)Falls back to go task() — event loop is never blocked
pool is nilUses go exec() per request