zip

package module
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 32 Imported by: 0

README

zip

Docs: zip · part of the ZAP Protocol

The ZAP-native Go web framework. Built on Fiber v3 / fasthttp, with a terse route API, typed handlers that project to OpenAPI and MCP, and ZAP as the primary transport — HTTP is a secondary view of the same routes.

zap-proto.io · Docs · fiber · Spec

ONE framework. ONE Listen verb. Routes defined once, served over every transport.

package main

import (
    "github.com/zap-proto/zip"
    "github.com/zap-proto/zip/middleware"
)

func main() {
    app := zip.New(zip.Config{})
    app.Use(middleware.Recover(), middleware.RequestID())

    app.Get("/health", func(c *zip.Ctx) error {
        return c.JSON(200, map[string]string{"status": "ok"})
    })

    v1 := app.Group("/v1")
    v1.Get("/users/:id", func(c *zip.Ctx) error {
        return c.JSON(200, map[string]string{
            "id":   c.Param("id"),
            "org":  c.Org(),  // gateway-minted X-Org-Id
            "user": c.User(), // gateway-minted X-User-Id
        })
    })

    _ = app.Listen(":9653", "http://:8080") // ZAP primary + HTTP extra, one verb
}

Install

go get github.com/zap-proto/zip

Module path github.com/zap-proto/zip. Requires Go 1.26+.

Features

  • Terse routesapp.Get(path, fn) is the primary API; handlers are func(c *zip.Ctx) error.

  • Transport is a value, not a method — one verb, app.Listen(addrs...), and the address scheme selects the transport (mirrors net.Listen):

    app.Listen(":9653")                  // ZAP (bare addr = the primary)
    app.Listen(":9653", "http://:8080")  // ZAP + HTTP in one call
    app.Listen("http://:8080")           // HTTP only
    app.Listen("/run/hanzo/app.sock")    // ZAP on a unix socket
    app.Listen("quic://:443")            // any RegisterTransport'd protocol
    

    ZAP (TLS 1.3 + post-quantum) is the default; HTTP is built in; zip.RegisterTransport(scheme, zip.Transport{Serve, Dial}) adds any future protocol with zero change to Listen or Mount. The scheme names the protocol and the address names where it is spoken — a path is a unix socket, a host:port is TCP — so one wire never needs two schemes.

  • Composition is one verbService is func(*App) error: a unit that attaches its own routes, middleware and shutdown hooks. A constructor taking dependencies and returning one is the same thing curried, so a composition root only ever sees Service.

    app.Add(billing.New(deps), search.New(deps))
    
  • Plugins — a service builds as its own binary and loads at run time — because a plugin is also just a Service, where it runs is a deployment decision rather than a code change:

    //go:embed bin/billing
    var billingBin []byte
    
    app.Add(
        billing.New(deps),                                                     // linked in
        zip.Load("/v1/billing", zip.Plugin{Name: "billing", Bin: billingBin}), // its own binary
        zip.Load("/v1/ml",      zip.Plugin{Name: "ml", Addr: mlAddr}),         // already running
    )
    

    A plugin is an ordinary zip app — no SDK, no schema — started on its own unix socket and reached over ZAP. The host links zip and a transport, never a plugin's dependency graph, so its link time doesn't grow when a plugin does, plugins build in parallel, and go:embed keeps the deployment a single artifact. app.Reload(name, bin) swaps a running plugin for a new build without dropping a request: the replacement must be listening before any traffic moves to it, so a bad build can't take the route down, and routes register once and resolve their target per request, so repeated reloads stay flat in memory. app.Mount(prefix, addr) is the same delegation without the process management.

  • One registry, three projectionszip.Get[In, Out](app, path, fn) registers one operation that becomes a REST route, an OpenAPI 3.1 doc (/.well-known/openapi.json, Swagger UI at /docs), and a Model Context Protocol tool at /mcp (JSON-RPC 2.0). Same schema, same handler. Because /mcp is an ordinary route, ZAP-native MCP is automatic. On by default; Config.MCP.Disabled to suppress.

  • Precedence is a property of the pattern — routing comes from the zap-proto/fiber fork: the most specific pattern wins regardless of registration order (static ≻ :param ≻ *), and ambiguous equal-specificity overlaps panic at startup instead of silently shadowing.

  • Identity built-inc.Org() / c.User() / c.UserEmail() / c.IsAdmin() read JWT-validated X-* headers set by the gateway; handlers never parse tokens.

  • MiddlewareRecover, RequestID, Logger, Timeout, MaxBody, CORS, RateLimit, Telemetry, Breaker in zip/middleware.

  • WebSocket & streamingwsx.Upgrade(fn) over fasthttp/websocket; c.SendStreamWriter for SSE / chunked responses.

  • Extension routesapp.Module("POST /v1/eval", "wasm", "./policy") mounts a sandboxed extension (wasm / goja / pyvm / starlark / v8go / native) as a route.

  • Embedded JS/TS runtime — run a (req, res)-shaped JS/TS handler in-process via goja (pure Go, no CGO); esbuild transpiles TS ahead of it, for incremental migration to native Go.

  • Drop-in migrationapp.All("/legacy/*", zip.AdaptNetHTTP(h)) fronts any http.Handler as one wildcard route; it obeys the same precedence, so a native route added later still wins.

  • Stdlib JSON only — every JSON path goes through one internal helper backed by encoding/json/v2 when built with GOEXPERIMENT=jsonv2 (Go 1.25+), else encoding/json. No third-party JSON library.

Documentation

The full guide — Ctx reference, the route-precedence contract, middleware, extension-runtime mounts, and versioning — is at zap-proto.dev/docs/zip. Runnable examples live in examples/.

License

MIT — see LICENSE.

Documentation

Overview

Package zip is Hanzo's canonical Go web framework. Built on Fiber v3 / fasthttp. ZAP-typed handlers. Multi-language extension support via HIP-0105.

ONE framework, ZERO escape hatches. zip IS fast.

app := zip.New(zip.Config{Logger: luxlog.NewLogger("svc")})
app.Use(middleware.Recover(), middleware.RequestID())
app.Get("/health", func(c *zip.Ctx) error {
    return c.JSON(200, fiber.Map{"ok": true})
})
app.Listen(":9653", "http://:8080") // ZAP primary + HTTP extra, one verb

Public surface — types/functions exposed at the package root:

type App, Config, Ctx, Handler
func New(Config) *App
func Get[I, O](app *App, path string, fn func(ctx, *I) (*O, error))
func Post[I, O](app *App, path string, fn func(ctx, *I) (*O, error))
...

All other behavior lives in subpackages: `middleware`, `runtime`.

Index

Constants

View Source
const AddrEnv = "ZIP_ADDR"

AddrEnv is the variable a host sets to tell a plugin where to listen. A plugin reads it through Addr.

View Source
const DefaultScheme = "zap"

DefaultScheme is the transport a bare address (no "scheme://") uses. ZAP is the primary transport (TLS 1.3 + post-quantum, gRPC's replacement), so the path of least resistance is ZAP-native.

View Source
const JSONVariant = jsonenc.Variant

JSONVariant reports which JSON implementation zip is using in this build — "encoding/json/v2" when compiled with GOEXPERIMENT=jsonv2, "encoding/json" otherwise. Exposed for cmd/cloud startup logs and for tests that need to assert the variant. Per HIP-0106 the wire stack is "JSON only at edge, ZAP between services"; this constant tells operators which JSON impl is on the edge.

Variables

This section is empty.

Functions

func Addr added in v1.9.0

func Addr(fallback string) string

Addr returns the address this process was asked to serve on, or fallback when it was started directly rather than by a host. This is the whole plugin side of the contract.

func Delete

func Delete[In, Out any](app *App, path string, fn TypedHandler[In, Out], opts ...OpOption)

Delete registers a DELETE typed handler at path.

func Get

func Get[In, Out any](app *App, path string, fn TypedHandler[In, Out], opts ...OpOption)

Get registers a GET typed handler at path.

func Patch

func Patch[In, Out any](app *App, path string, fn TypedHandler[In, Out], opts ...OpOption)

Patch registers a PATCH typed handler at path.

func Post

func Post[In, Out any](app *App, path string, fn TypedHandler[In, Out], opts ...OpOption)

Post registers a POST typed handler at path.

func Put

func Put[In, Out any](app *App, path string, fn TypedHandler[In, Out], opts ...OpOption)

Put registers a PUT typed handler at path.

func RegisterTransport added in v1.1.0

func RegisterTransport(scheme string, t Transport)

RegisterTransport adds (or replaces) a transport keyed by address scheme, so any future protocol slots into both Listen and Mount with ZERO change to either API. Call before Listen or Mount.

zip.RegisterTransport("quic", zip.Transport{
	Serve: func(addr string, h fasthttp.RequestHandler) zip.Server {
		return myquic.NewServer(addr, h)
	},
	Dial: func(addr string) zip.Client { return myquic.NewClient(addr) },
})

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the zip application. It wraps *fiber.App and exposes the zip handler signature alongside generic typed handlers.

func New

func New(cfg Config) *App

New constructs an App with the given config. Defaults are applied for any zero-valued field.

func (*App) Add added in v1.9.0

func (a *App) Add(services ...Service) error

Add composes services into the app in order, stopping at the first error and naming which position failed. Order matters exactly as much as it does for hand-written route registration: earlier routes are registered earlier, and specificity still decides which one answers.

func (*App) All

func (a *App) All(path string, handlers ...Handler) Router

All registers a handler for any HTTP method.

func (*App) Authorize added in v1.8.3

func (a *App) Authorize(fn Authorizer)

Authorize installs fn as the op-invoke authorization hook. It is the op-level counterpart to Use: Use wraps the whole request with transport middleware, which for a body request sees only the raw bytes; Authorize runs one decision on the DECODED typed input of every op, REST and MCP alike — the seam a mounted subsystem gates on so the value it authorizes is the value the handler binds. Call once while mounting, before Listen. A nil fn clears it (every decoded request then runs unauthorized).

func (*App) Delete

func (a *App) Delete(path string, handlers ...Handler) Router

func (*App) Fiber

func (a *App) Fiber() *fiber.App

Fiber returns the underlying *fiber.App. Use for one-off escape into Fiber-only APIs (rare). Prefer staying on the zip surface.

func (*App) Get

func (a *App) Get(path string, handlers ...Handler) Router

Get / Post / Put / Patch / Delete / Head / Options / All register routes. Chains are in wrapping order: middleware first, the final handler last.

func (*App) Group

func (a *App) Group(prefix string, handlers ...Handler) Router

Group creates a path-prefixed router group. The returned Router is the one way to register nested routes under a prefix — register leaves and further Groups directly on it; middleware scoped to the group goes on via its Use.

func (*App) Head

func (a *App) Head(path string, handlers ...Handler) Router

func (*App) Listen added in v1.1.0

func (a *App) Listen(addrs ...string) error

Listen serves the app on one or more addresses and blocks until all listeners stop or the first one errors. The address scheme selects the transport; a bare address uses ZAP (DefaultScheme). This is the ONE and only way to serve a zip app — no per-transport methods.

func (*App) Logger

func (a *App) Logger() luxlog.Logger

Logger returns the App's logger.

func (*App) Module

func (a *App) Module(methodPath, runtimeName, modulePath string) error

Module mounts a single HIP-0105 extension at the given method+path — the one way to put an extension on the app. The `methodPath` form is "METHOD /path" (e.g. "POST /v1/validate"), matching the idiom. `runtime` selects the backing engine ("wasm" | "goja" | "pyvm" | "starlark" | "v8go" | "native"); `modulePath` is the directory containing the extension.json manifest.

app.Module("POST /v1/policy/eval", "wasm", "./extensions/policy")
app.Module("POST /v1/transform",   "pyvm", "./extensions/transform")
app.Module("POST /v1/webhook",     "goja", "./extensions/webhook")

The extension's exported function name is inferred from the path's last non-{param} segment, lowercased (e.g. "POST /v1/policy/eval" → "eval").

func (*App) Mount

func (a *App) Mount(prefix, addr string) error

Mount delegates every request under prefix to the service at addr. Listen serves here; Mount delegates there — one registry, one scheme vocabulary, opposite directions. A bare address uses ZAP (DefaultScheme), so a colocated service is mounted by address alone:

app.Mount("/v1/billing", "billing.hanzo.svc:9653")      // ZAP
app.Mount("/v1/legacy",  "http://legacy.internal:8080") // HTTP

The mounted service keeps the whole path — /v1/billing/invoices arrives as /v1/billing/invoices — so its routes ARE the surface, exactly as they are when it is linked in. Nothing is re-encoded in between: the inbound fasthttp request object is handed to the client and the reply is written straight back, so a mount costs a connection, not a copy.

This is how a service ships as its own binary. The mounting binary links zip and a transport, never the mounted service's dependency graph, so its link time does not grow when that service does, and the service redeploys without relinking it. Moving a service between Mount and a linked-in route tree is a deployment decision, not a code change.

func (*App) OnShutdown added in v1.4.0

func (a *App) OnShutdown(fn func(context.Context) error)

OnShutdown registers fn as a teardown hook, run during Shutdown / ShutdownWithContext. This is the one teardown primitive zip exposes: subsystems register their own cleanup at mount time, and reverse-mount teardown falls out for free (see the ordering note below).

Ordering. Hooks run LAST in the shutdown sequence — after listeners stop accepting and after in-flight requests drain — and in LIFO order (reverse registration = reverse mount order). Draining first means a subsystem's teardown never races the requests still using it; LIFO means a dependency mounted before its dependents is torn down after them.

Errors. Every hook runs even if an earlier one fails; all hook errors (and the drain error) are aggregated with errors.Join and returned from Shutdown.

Concurrency. Registration is safe from multiple goroutines. A nil fn is ignored. Registering after Shutdown has begun is a no-op: the hook is dropped (never run) and a warning is logged — there is no longer a shutdown to hook into, and running it immediately would give OnShutdown two meanings depending on timing. Register teardown at mount time, before Shutdown.

func (*App) OpenAPISpec added in v1.10.3

func (a *App) OpenAPISpec() map[string]any

OpenAPISpec returns the OpenAPI 3.1 document for every typed op registered on this app — the SAME value served at /.well-known/openapi.json.

It is exported so a service can render its published contract from the routes it actually registers, in a build step rather than from a running server. A spec generated any other way is a second source of truth, and the whole point of deriving it here is that there is only one.

func (*App) Options

func (a *App) Options(path string, handlers ...Handler) Router

func (*App) Patch

func (a *App) Patch(path string, handlers ...Handler) Router

func (*App) Plugins added in v1.13.0

func (a *App) Plugins() []PluginStatus

Plugins reports every plugin this host has loaded, ordered by name so a diff between two hosts is stable. Safe to call while requests are in flight.

func (*App) Post

func (a *App) Post(path string, handlers ...Handler) Router

func (*App) Prepare added in v1.8.3

func (a *App) Prepare()

Prepare installs the deferred projections (the OpenAPI document and the MCP tool surface) without starting a listener, so a test can drive them through Fiber().Test exactly as a served app exposes them. Listen calls it too; both share one guard, so it runs at most once however it is reached.

func (*App) Put

func (a *App) Put(path string, handlers ...Handler) Router

func (*App) Reload added in v1.9.0

func (a *App) Reload(name string, bin []byte) error

Reload replaces the running plugin named name with a new build, without dropping a request. bin is the new binary; nil reuses what the plugin was loaded with, which is how you restart a crashed or wedged one.

The new process is started and proven to be listening before any request moves to it. If it fails to come up, the old one is still serving and the error is returned — a bad build cannot take the route down. Once the swap happens the old process keeps serving for Plugin.Drain so in-flight requests finish, then is killed, reaped, its connections closed and its directory removed.

func (*App) Shutdown

func (a *App) Shutdown() error

Shutdown gracefully stops every transport, then runs teardown hooks. The process is ending, so hooks receive context.Background() — no cancellation or deadline. Use ShutdownWithContext to bound teardown. Idempotent: a second call is a no-op and hooks run at most once.

func (*App) ShutdownWithContext

func (a *App) ShutdownWithContext(ctx context.Context) error

ShutdownWithContext is Shutdown bounded by ctx: ctx bounds the in-flight drain and is passed to every teardown hook (values and deadline). Shares Shutdown's once-guard, so mixing the two still runs hooks once.

func (*App) TestCtx added in v1.7.1

func (a *App) TestCtx(method, path string) *Ctx

TestCtx returns a detached *Ctx over a synthetic request — the unit-test analog of a live request context, for calling a Handler directly. Integration tests should prefer app.Fiber().Test(req), which exercises routing and the full middleware chain; this exists for the narrower "call this one handler with locals seeded" idiom. The Ctx is not pooled; do not release it.

func (*App) Unload added in v1.9.0

func (a *App) Unload(name string) error

Unload stops the plugin named name. Its routes stay registered and answer 503 until a Reload brings it back — the route table is never mutated, which is what keeps repeated load/unload cycles flat.

func (*App) Use

func (a *App) Use(handlers ...Handler) Router

Use registers zip-style middleware. Each Handler runs in order; calling c.Next() (via c.Continue) chains to the next handler.

func (*App) With added in v1.5.0

func (a *App) With(mw ...Middleware) Router

With returns a Router whose subsequent leaf registrations (Get/Post/…/All) have mw wrapped around the handler at registration time — pure composition (RateLimit(CSRF(handler))). It does NOT touch the global Use stack and does NOT route through c.Next(); it is the per-route counterpart to Use. Routes registered on the returned Router still obey specificity precedence exactly like any other route.

app.With(RateLimit, CSRF).Post("/v1/keys", mintKey)

type Authorizer added in v1.8.3

type Authorizer func(ctx context.Context, op Op, in any) error

Authorizer authorizes a decoded, validated typed request at the op-invoke seam — the ONE point every projection of a typed handler funnels through. It runs after the request is decoded into the op's typed In and validated, and BEFORE the handler runs, over REST and MCP alike, so the value it authorizes is exactly the value the handler will act on: there is no second parse of the body for it to diverge from. in is the *In the handler will receive.

Returning a non-nil error aborts the op before the handler runs, and that error is the response — return a zip.Err* (e.g. ErrForbidden) for a clean status.

type Client added in v1.8.4

type Client interface {
	Do(req *fasthttp.Request, resp *fasthttp.Response) error
}

Client is the call side of a transport: anything that can complete a request. *zaphttp.Transport and *fasthttp.HostClient already satisfy it, so giving a scheme a Dial is a one-liner.

type Config

type Config struct {
	// Logger is the luxfi/log Logger zip uses internally. Required.
	// If nil, a default one is created via luxlog.NewLogger("zip").
	Logger luxlog.Logger

	// Loader is the HIP-0105 extension runtime loader. nil disables
	// app.Module() — only native handlers will work. The interface is
	// satisfied by *extruntime.Loader from hanzoai/base/plugins/extruntime;
	// zip does NOT take a hard dep on hanzoai/base.
	Loader runtime.Loader

	// AllowedRuntimes restricts which extension runtimes app.Module()
	// will accept (e.g. ["goja","wazero"] for hard multi-tenant safety).
	// nil = allow whatever the Loader has registered.
	AllowedRuntimes []string

	// ServerHeader is sent as the Server: response header (default "zip").
	// Set to "-" to suppress.
	ServerHeader string

	// BodyLimit is the maximum request body size (default 4 MiB).
	BodyLimit int

	// AppName forwards to fiber.Config.AppName.
	AppName string

	// DisableStartupMessage suppresses Fiber's startup banner.
	DisableStartupMessage bool

	// ErrorHandler is the catch-all error handler. Defaults to zip.errorHandler
	// which renders {error, code, status} JSON.
	ErrorHandler fiber.ErrorHandler

	// Concurrency caps the maximum number of concurrent connections the
	// server will accept. Default 0 means fasthttp's own default
	// (256*1024). Ops should cap this at the per-replica budget — see
	// `~/work/hanzo/hips/docs/SCALE_STANDARD.md`. With Hanzo's verified
	// 8 KiB/conn budget, 100_000 sits at ~800 MiB inside a 1 GiB pod.
	Concurrency int

	// ReadBufferSize is fasthttp's per-conn request-read buffer (default
	// 4 KiB). Raise only for header-heavy upstreams; raising it inflates
	// the per-conn memory budget and breaks the conn-memory regression
	// gate (see SCALE_STANDARD.md §8).
	ReadBufferSize int

	// WriteBufferSize is fasthttp's per-conn response-write buffer
	// (default 4 KiB). Raise only for streaming-heavy responses; same
	// budget caveat as ReadBufferSize.
	WriteBufferSize int

	// OpenAPI configures the auto-generated /.well-known/openapi.json
	// served when typed handlers are registered.
	OpenAPI OpenAPIConfig

	// MCP configures the Model Context Protocol tool surface auto-derived from
	// typed handlers (Get/Post[In,Out]). Enabled by default — it's free (the
	// same op registry that feeds OpenAPI), served over every transport. Set
	// MCP.Disabled to suppress.
	MCP MCPConfig
}

Config configures the zip App. Most fields pass through to Fiber's own Config; a few zip-specific knobs control runtime loading.

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

Ctx wraps fiber.Ctx and adds the Hanzo identity surface (Org/User/Email from gateway-minted X-* headers per HIP-0026), a per-request luxfi/log logger, and typed Deps access.

func (*Ctx) App

func (c *Ctx) App() *App

App returns the parent App.

func (*Ctx) Bind

func (c *Ctx) Bind(v any) error

Bind parses the request body into v based on Content-Type (JSON by default) and runs struct-tag validation (required/min/max/minlen/maxlen). Returns a *HTTPError(400) when either step fails so handlers can return the error directly.

func (*Ctx) BindQuery

func (c *Ctx) BindQuery(v any) error

BindQuery parses query parameters into v and runs validation.

func (*Ctx) BindURI

func (c *Ctx) BindURI(v any) error

BindURI parses URL params into v and runs validation.

func (*Ctx) Body

func (c *Ctx) Body() []byte

Body returns the raw request body.

func (*Ctx) Bytes

func (c *Ctx) Bytes(code int, b []byte) error

Bytes writes raw bytes.

func (*Ctx) Context

func (c *Ctx) Context() context.Context

Context returns the standard context.Context (deadline + cancellation).

func (*Ctx) Continue

func (c *Ctx) Continue() error

Continue is an alias for Next() with the standard middleware idiom.

func (*Ctx) Fiber

func (c *Ctx) Fiber() fiber.Ctx

Fiber returns the underlying fiber.Ctx for one-off escape into Fiber-only APIs.

func (*Ctx) Header

func (c *Ctx) Header(name string) string

Header returns a request header.

func (*Ctx) Host added in v1.7.3

func (c *Ctx) Host() string

Host returns the request Host (authority) from the Host header, port included when present. It honors X-Forwarded-Host ONLY when the app is configured to trust proxies — which zip does NOT do (there is no TrustProxy knob on zip.Config), so a client-supplied X-Forwarded-Host is ignored and cannot spoof the value. Used for white-label brand-by-host resolution (see middleware.ProductionHeaders); keep it un-trusted-proxy so the Server brand cannot be forged from a request header.

func (*Ctx) IsAdmin

func (c *Ctx) IsAdmin() bool

IsAdmin returns the X-User-IsAdmin gateway claim as a bool.

func (*Ctx) JSON

func (c *Ctx) JSON(code int, v any) error

JSON writes the value as JSON with status code.

func (*Ctx) Locals

func (c *Ctx) Locals(key any, value ...any) any

Locals returns or sets a per-request value.

func (*Ctx) Log

func (c *Ctx) Log() luxlog.Logger

Log returns the request-scoped logger. Middleware that adds request_id, org, user, etc. via Locals can enrich this by calling SetLog.

func (*Ctx) Method

func (c *Ctx) Method() string

Method returns the request method.

func (*Ctx) Next

func (c *Ctx) Next() error

Next yields to the next handler in the chain. Use sparingly from zip middleware — middleware bodies usually call c.Continue() at the end, not Next() mid-handler.

func (*Ctx) NoContent

func (c *Ctx) NoContent(code int) error

NoContent writes the status code with no body.

func (*Ctx) Org

func (c *Ctx) Org() string

Org returns the X-Org-Id from the JWT-validated gateway. Empty when no gateway is in front (local dev / direct ingress).

func (*Ctx) Param

func (c *Ctx) Param(name string) string

Param returns a URL path parameter.

func (*Ctx) Path

func (c *Ctx) Path() string

Path returns the request path.

func (*Ctx) Query

func (c *Ctx) Query(name string) string

Query returns a URL query parameter.

func (*Ctx) Redirect added in v1.7.0

func (c *Ctx) Redirect(code int, location string) error

Redirect sends an HTTP redirect to location with the given status code.

func (*Ctx) RequestID

func (c *Ctx) RequestID() string

RequestID returns the value of X-Request-Id (set by the RequestID middleware).

func (*Ctx) SendStream

func (c *Ctx) SendStream(r io.Reader) error

SendStream streams data from r to the client (e.g. for SSE).

func (*Ctx) SendStreamWriter

func (c *Ctx) SendStreamWriter(fn func(w *bufio.Writer)) error

SendStreamWriter writes streaming output via a bufio.Writer (Server-Sent Events / chunked transfer). Forwards to fiber.Ctx.SendStreamWriter.

func (*Ctx) SetContext added in v1.7.3

func (c *Ctx) SetContext(ctx context.Context)

SetContext replaces the request's context.Context — the boundary idiom: a middleware derives a request-scoped context (values, gates, deadlines) ONCE and every later c.Context() returns it. One context per request, one setter.

func (*Ctx) SetHeader

func (c *Ctx) SetHeader(name, value string)

SetHeader sets a response header.

func (*Ctx) SetLog

func (c *Ctx) SetLog(l luxlog.Logger)

SetLog replaces the request logger (typically by middleware that wants to attach request-id / org / user fields).

func (*Ctx) Status

func (c *Ctx) Status(code int) *Ctx

Status sets the response status. Chains.

func (*Ctx) String

func (c *Ctx) String(code int, s string) error

String writes a plain-text response.

func (*Ctx) User

func (c *Ctx) User() string

User returns the X-User-Id from the JWT-validated gateway.

func (*Ctx) UserEmail

func (c *Ctx) UserEmail() string

UserEmail returns the X-User-Email from the JWT-validated gateway.

type HTTPError

type HTTPError struct {
	Status int    `json:"status"`
	Code   string `json:"code,omitempty"`
	Msg    string `json:"error"`
}

HTTPError is the canonical error type zip understands. Returning one causes the error handler to send a JSON {error, code, status} body.

func ErrBadRequest

func ErrBadRequest(msg string) *HTTPError

Common shortcuts.

func ErrConflict

func ErrConflict(msg string) *HTTPError

func ErrForbidden

func ErrForbidden(msg string) *HTTPError

func ErrInternal

func ErrInternal(msg string) *HTTPError

func ErrNotFound

func ErrNotFound(msg string) *HTTPError

func ErrUnauthorized

func ErrUnauthorized(msg string) *HTTPError

func Errorf

func Errorf(status int, format string, args ...any) *HTTPError

Errorf builds an HTTPError with the given status and message.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type Handler

type Handler func(c *Ctx) error

Handler is zip's request handler signature. Returning an error causes Fiber's error chain to write a JSON response.

func AdaptNetHTTP

func AdaptNetHTTP(h http.Handler) Handler

AdaptNetHTTP wraps an http.Handler so it can be served on a zip router as an ordinary zip.Handler. To front a whole foreign subtree, take a Group for the prefix and register a wildcard on it — this is THE way to bring net/http code onto zip:

app.Group("/legacy").All("/*", zip.AdaptNetHTTP(httpHandler))

Group is what carries the prefix; Mount does not do this. Mount delegates a prefix to a REMOTE address over a transport, so it takes a string, not a handler. A local handler is composed, not delegated.

The wildcard stays an ordinary route, so most-specific-wins precedence still applies: a route registered on the group afterwards still beats it for its exact path.

g := app.Group("/legacy")
g.All("/*", zip.AdaptNetHTTP(httpHandler)) // everything else
g.Get("/health", nativeHealth)             // still wins

A bare func is adapted through http.HandlerFunc, which is itself an http.Handler — there is no separate func-shaped adapter:

app.Get("/legacy", zip.AdaptNetHTTP(http.HandlerFunc(myFunc)))

Migration tool — costs ~5% perf vs native Fiber. Replace with native zip handlers when feasible.

func AdaptNetHTTPMiddleware

func AdaptNetHTTPMiddleware(mw func(http.Handler) http.Handler) Handler

AdaptNetHTTPMiddleware wraps a stdlib middleware (func(http.Handler) http.Handler) as a zip.Handler — the net/http-middleware bridge. Because it IS middleware, it goes on a Group's Use rather than on a route:

app.Group("/legacy").Use(zip.AdaptNetHTTPMiddleware(mw))

Migration tool — costs ~5% perf vs native Fiber.

func Static added in v1.5.0

func Static(fsys fs.FS, opts ...StaticOption) Handler

Static returns a leaf Handler that serves files from fsys. Mount it on a wildcard route; the "*" capture selects the file:

app.Get("/assets/*", zip.Static(assets))                       // embed.FS
app.Get("/app/*", zip.Static(os.DirFS("dist"), zip.WithIndex("index.html")))

Contract:

  • The subpath is cleaned and checked with fs.ValidPath; any ".." escape or absolute path is rejected fail-closed with 404 — Static can never read outside fsys.
  • A missing file yields c.Next(), so a later more-specific route or a SPA catch-all still wins — never a 500.
  • Sets Content-Type (by extension), Content-Length and Last-Modified; honours HEAD and If-Modified-Since (304). Nothing else — no compression, no byte ranges, no directory listing.

fsys is any fs.FS: an embed.FS for baked-in assets or os.DirFS(dir) for a directory on disk. Both are traversal-safe by construction; the fs.ValidPath gate is defence in depth on top of that.

type MCPConfig added in v1.1.0

type MCPConfig struct {
	// Disabled suppresses the /mcp route (MCP is on by default — it's free).
	Disabled bool
	// Path overrides the mount path (default "/mcp").
	Path string
	// Name is the server name reported to MCP clients (default AppName, else "zip").
	Name string
}

MCPConfig configures the auto-derived MCP surface.

type Middleware added in v1.5.0

type Middleware = func(next Handler) Handler

Middleware is a composable request transformer in the classic wrapping form: given the next Handler it returns a Handler that runs around it. This is a DIFFERENT tool from Use — they do different jobs and compose freely:

  • Use(Handler...) registers GLOBAL / prefix middleware. It runs for every matched route (or every route under a Group) in DECLARATION order and chains via c.Next(). Reach for it for ambient cross-cutting concerns that apply broadly: logging, recovery, request-id.

  • Middleware + With + Chain wrap ONE leaf handler explicitly, at registration time, with no c.Next() indirection. Reach for it when a specific endpoint needs a specific pipeline:

    app.With(RateLimit, CSRF).Post("/v1/keys", mintKey)

    wraps mintKey as RateLimit(CSRF(mintKey)): RateLimit is outermost and runs first, CSRF next, the handler last; any layer short-circuits by returning without calling next.

A Middleware body is written by hand, no framework glue:

func RequireCSRF(next zip.Handler) zip.Handler {
    return func(c *zip.Ctx) error {
        if !validCSRF(c) {
            return c.String(403, "bad csrf") // short-circuit
        }
        return next(c) // continue
    }
}

func Chain added in v1.5.0

func Chain(mw ...Middleware) Middleware

Chain composes middleware left-to-right into one Middleware. Chain(a, b, c) nests as a(b(c(handler))): a is outermost (runs first inbound, last outbound), c innermost, wrapping the handler directly. Chain() with no arguments is the identity middleware.

type Op added in v1.8.3

type Op struct {
	Method      string
	Path        string
	OperationID string
}

Op is the stable identity of a registered typed handler, handed to an Authorizer so the decision can key on the operation as well as the input. OperationID is the resolved id the OpenAPI document and the MCP tool surface share — the explicit WithOperationID, else the method+path default.

type OpOption

type OpOption func(*registeredOp)

OpOption configures a typed handler registration (OpenAPI metadata).

func WithOperationID

func WithOperationID(id string) OpOption

WithOperationID sets the operation ID in OpenAPI.

func WithSummary

func WithSummary(s string) OpOption

WithSummary sets the operation summary in OpenAPI.

func WithTags

func WithTags(tags ...string) OpOption

WithTags sets the operation tags in OpenAPI.

type OpenAPIConfig

type OpenAPIConfig struct {
	// Title appears in the OpenAPI info block.
	Title string
	// Description appears in the OpenAPI info block.
	Description string
	// Version appears in the OpenAPI info block (e.g. "v1.0.0").
	Version string
	// Disabled suppresses the /.well-known/openapi.json route and /docs.
	Disabled bool
}

OpenAPIConfig configures the auto-generated /.well-known/openapi.json endpoint zip serves when typed handlers are registered.

type Plugin added in v1.9.0

type Plugin struct {
	Name string   // identifies it in log lines and names its socket
	Addr string   // already running here — start nothing, just mount
	Bin  []byte   // the binary, normally go:embed'd
	Path string   // ...or where it lives on disk
	URL  string   // ...or a release artifact to fetch (requires Sum)
	Args []string // passed after argv[0]
	Env  []string // added to the child's environment

	// Sum is the hex SHA-256 of the binary at URL, and is REQUIRED with it.
	// Fetching code over a network and executing it is the one place a plugin
	// host becomes an arbitrary-code-execution vector, so an unverified
	// download is refused rather than trusted.
	//
	// It doubles as the cache key: a binary already present under this digest
	// is reused, so a restart costs no download and a rollback to a previously
	// run version is free and offline.
	Sum string

	// Dir is where an embedded binary is extracted and its socket created.
	// Empty means the system temp dir, which on many hosts is a tmpfs — i.e.
	// RAM. A plugin binary is tens to hundreds of megabytes, so extracting one
	// there spends real memory and fails outright when the tmpfs is full. Point
	// this at disk for anything but a small plugin.
	Dir string

	// Start bounds how long to wait for the plugin to listen. Zero means 10s.
	// A plugin that has not bound by then is a startup failure, not a slow
	// one — nothing is mounted onto a process that never came up.
	Start time.Duration

	// Drain is how long a replaced process keeps serving after a Reload, so
	// requests already in flight on it finish. Zero means 5s.
	Drain time.Duration

	// Lazy defers starting the child until the first request actually reaches
	// one of its prefixes. Routes register at Load either way, so the surface
	// is identical — only the process is deferred.
	//
	// This is what makes many plugins affordable. A host composing 69 services
	// eagerly pays 69 processes, 69 resident sets and 69 startup times at boot
	// for a set that is mostly idle; lazily it pays for the ones traffic
	// actually reaches. The cost moves to the first request, which is why it is
	// opt-in: a latency-critical prefix should stay eager.
	Lazy bool
}

Plugin is a service that ships as its own binary. Exactly one of Addr, Bin, or Path says where to find it:

Addr — already running there; nothing is started, and Reload does not apply
Bin  — the binary itself, normally go:embed'd
Path — the binary on disk

type PluginStatus added in v1.13.0

type PluginStatus struct {
	Name string `json:"name"`

	// Prefix is the FIRST subtree this plugin answers — the one a log line
	// names it by. Prefixes is every subtree, and a plugin may own several.
	// Reporting only the first would understate the blast radius of taking
	// this plugin down, which is the question a fleet view exists to answer.
	Prefix   string   `json:"prefix"`
	Prefixes []string `json:"prefixes,omitempty"`

	// Source is where the binary came from: "embedded", "path", "url", or
	// "remote" for an instance this host did not start.
	Source string `json:"source"`

	// Version is the artifact's SHA-256 when it was installed from a URL —
	// the only version identifier that cannot drift from the bits actually
	// running, since it IS the bits. Empty for the other sources.
	Version string `json:"version,omitempty"`

	// Addr is the socket or address serving it.
	Addr string `json:"addr,omitempty"`

	// PID is the child process, or 0 when this host did not start it.
	PID int `json:"pid,omitempty"`

	// Running is false after Unload, or after a child exited and no Reload has
	// replaced it. Its routes stay registered and answer 503, so a false here
	// is the difference between "not deployed" and "deployed but down".
	Running bool `json:"running"`

	// Since is when the CURRENT instance started — it resets on Reload, so it
	// reports the age of what is running, not of the mount.
	Since time.Time `json:"since,omitzero"`

	// Reloads counts successful swaps since Load. A climbing number on one
	// host and not its peers is the signal that a rollout is uneven.
	Reloads int `json:"reloads"`

	// Restarts counts times the supervisor brought this plugin back after it
	// died on its own. Distinct from Reloads, which are deliberate: a nonzero
	// Restarts is a plugin crashing, and a climbing one is a crash loop.
	Restarts int `json:"restarts"`

	// Usage is what this plugin costs right now, read from the kernel.
	Usage Usage `json:"usage,omitzero"`
}

PluginStatus is one loaded plugin as the host currently sees it.

type Router

type Router interface {
	Use(handlers ...Handler) Router

	// Route registration takes ONE chain in wrapping order: zero or more
	// middleware first, the final handler LAST. fiber wants handler-first;
	// splitChain flips it in exactly one place.
	Get(path string, handlers ...Handler) Router
	Post(path string, handlers ...Handler) Router
	Put(path string, handlers ...Handler) Router
	Patch(path string, handlers ...Handler) Router
	Delete(path string, handlers ...Handler) Router
	Head(path string, handlers ...Handler) Router
	Options(path string, handlers ...Handler) Router
	All(path string, handlers ...Handler) Router

	Group(prefix string, handlers ...Handler) Router

	// Fiber returns the underlying fiber.Router for one-off escape.
	Fiber() fiber.Router
}

Router is the path-mounting surface shared by *App and Group. All concrete routes flow through toFiberHandler — fiber.Ctx is the underlying type the framework's users never see directly.

type Server added in v1.1.0

type Server interface {
	ListenAndServe() error
	Close() error
}

Server is a running transport listener bound to one address. Both zap-proto/http.Server and the built-in HTTP server satisfy it, as does any custom transport.

type Service added in v1.9.0

type Service func(*App) error

Service is a unit of functionality that attaches itself to an app: its routes, its middleware, its shutdown hooks. One type covers every way a unit can be composed, which is what makes where it runs a deployment decision instead of a code change:

app.Add(
    billing.New(deps),                                   // linked into this binary
    zip.Load("/v1/search", zip.Plugin{Name: "search",     // its own binary, embedded
        Bin: searchBin}),
    zip.Load("/v1/ml", zip.Plugin{Name: "ml",             // already running elsewhere
        Addr: os.Getenv("ML_ADDR")}),
)

A constructor that takes dependencies and returns a Service is the same thing curried — func(deps) Service — so a unit's dependencies are bound where they are known and the composition root only ever sees Service.

func Load added in v1.9.0

func Load(p Plugin, prefixes ...string) Service

Load returns the Service that makes p serve prefix. When p.Addr is set it is mounted directly. Otherwise the binary is started as a child process listening on its own unix socket in a private 0700 directory — no port to allocate, and filesystem permissions are the ACL.

It returns a Service rather than taking an *App so a plugin and a linked-in service have the SAME type, and composing either is the same line.

The child is stopped and its directory removed on Shutdown, in LIFO order with every other hook, so a host that exits cleanly leaves nothing behind. prefixes is variadic because a service often owns more than one route subtree — o11y answers both /v1/o11y and /v1/sentry — and a single-prefix Load silently 404s the others. One call declares everything the plugin owns.

type StaticOption added in v1.5.0

type StaticOption func(*staticConfig)

StaticOption configures Static. Zero options serve fsys as-is.

func WithFallback added in v1.7.2

func WithFallback(name string) StaticOption

WithFallback serves name (e.g. "index.html") when the requested file does not exist — the SPA deep-link idiom: client-side routes resolve to the app shell instead of 404/next-route. Traversal-invalid paths still fail closed.

func WithIndex added in v1.5.0

func WithIndex(name string) StaticOption

WithIndex serves name (e.g. "index.html") for directory and root requests. Without it a directory request falls through via c.Next().

func WithStripPrefix added in v1.5.0

func WithStripPrefix(prefix string) StaticOption

WithStripPrefix derives the fs path from the request path with prefix removed, instead of from the route's "*" capture. Use it when the captured subpath is not the fs path — e.g. a versioned URL served from an unversioned tree: WithStripPrefix("/static/v2/") maps /static/v2/app.js to app.js.

type Transport added in v1.8.4

type Transport struct {
	Serve func(addr string, handler fasthttp.RequestHandler) Server
	Dial  func(addr string) Client
}

Transport is one address scheme in both directions: Serve terminates bytes arriving here, Dial originates bytes going there. They are the same concept — a wire — so they live in one value rather than two registries that drift. A scheme may leave either half nil; Listen and Mount each say which they needed.

type TypedHandler

type TypedHandler[In, Out any] func(ctx context.Context, in *In) (*Out, error)

TypedHandler is the generic handler signature: takes an *In, returns (*Out, error). zip generates OpenAPI 3.1 spec from the In/Out types and registers a Fiber route that unmarshals body → In, runs the handler, and marshals Out → JSON response.

type Usage added in v1.15.0

type Usage struct {
	// CPU is total user+system time this instance has consumed since it
	// started. It only ever climbs, so a rate is the interesting derivative.
	CPU time.Duration `json:"cpuNs,omitzero"`
	// RSS is resident memory in bytes — the honest number for "what does this
	// plugin cost", as opposed to virtual size.
	RSS int64 `json:"rssBytes,omitzero"`
	// Threads and FDs are the two limits a busy service hits first, and both
	// are leaks when they climb without bound.
	Threads int `json:"threads,omitzero"`
	FDs     int `json:"fds,omitzero"`
}

Usage is a plugin's resource cost, measured rather than self-reported.

This exists because a plugin is a process. A linked-in subsystem cannot be measured this way at any price — one process has one RSS and one CPU clock, and nothing inside it can be attributed to a subsystem. Running a service as its own process makes "which app is eating the memory" a question with an exact answer instead of an argument.

Zero means not measured (no /proc, or nothing running), not measured-as-zero.

Directories

Path Synopsis
examples
express-in-zip command
express-in-zip — the proof point: a legacy Express-shaped TypeScript handler running inside zip with ZERO rewrite.
express-in-zip — the proof point: a legacy Express-shaped TypeScript handler running inside zip with ZERO rewrite.
hello command
Hello, zip — minimal example.
Hello, zip — minimal example.
migrate-from-beego command
migrate-from-beego example — beego → zip migration via http.Handler adapter.
migrate-from-beego example — beego → zip migration via http.Handler adapter.
migrate-from-chi command
migrate-from-chi example — chi → zip via stdlib adapter.
migrate-from-chi example — chi → zip via stdlib adapter.
migrate-from-gin command
migrate-from-gin example — mechanical port of a gin-style API to zip.
migrate-from-gin example — mechanical port of a gin-style API to zip.
module-routes command
module-routes example — mount HIP-0105 extension modules as routes.
module-routes example — mount HIP-0105 extension modules as routes.
plugin/billing command
The plugin.
The plugin.
plugin/host command
The host.
The host.
sse-streaming command
sse-streaming example — Server-Sent Events via c.SendStreamWriter.
sse-streaming example — Server-Sent Events via c.SendStreamWriter.
subsystem-mount command
subsystem-mount example — HIP-0106 Mount(app, deps) idiom.
subsystem-mount example — HIP-0106 Mount(app, deps) idiom.
websocket command
websocket example — chat-style echo server.
websocket example — chat-style echo server.
zap-typed command
zap-typed example — typed handler with auto-generated OpenAPI spec.
zap-typed example — typed handler with auto-generated OpenAPI spec.
internal
jsonenc
Package jsonenc is zip's single JSON entry point.
Package jsonenc is zip's single JSON entry point.
runtime
Package runtime defines the minimal Loader / Module interface zip consumes from any HIP-0105 extension runtime implementation (hanzoai/base/plugins/extruntime is the reference).
Package runtime defines the minimal Loader / Module interface zip consumes from any HIP-0105 extension runtime implementation (hanzoai/base/plugins/extruntime is the reference).
testplugin command
Command testplugin is a minimal zip plugin used by the reload tests.
Command testplugin is a minimal zip plugin used by the reload tests.
Package middleware ships zip's canonical generic middleware stack.
Package middleware ships zip's canonical generic middleware stack.
esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes.
esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes.
Package wsx provides Fiber-v3-compatible WebSocket support via fasthttp/websocket.
Package wsx provides Fiber-v3-compatible WebSocket support via fasthttp/websocket.
Package zaprpc is an OPTIONAL named-service RPC dispatch helper for zip apps that want a Cap'n-Proto/gRPC-style service registry (name → method → handler) rather than plain REST routes.
Package zaprpc is an OPTIONAL named-service RPC dispatch helper for zip apps that want a Cap'n-Proto/gRPC-style service registry (name → method → handler) rather than plain REST routes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL