Help us shape the future of Go! Take the 2026 Go Developer Survey

vial

package module
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 24 Imported by: 0

README

Vial

CI

Vial is a Flask-inspired framework for Go's standard net/http package. It adds error-returning handlers, route groups, application lifecycle, background tasks, test helpers, and a rebuild loop without replacing Go's HTTP types.

Use Vial when an application needs more structure than raw net/http but must still work with standard handlers, middleware, contexts, response writers, and httptest. The runtime uses only the standard library.

Capabilities

  • Error-returning handlers: func(*vial.Context) error
  • Standard Go middleware composition
  • Named, method-aware routes and path parameters through http.ServeMux
  • Route groups with scoped middleware
  • Named application modules with isolated route registrars
  • Collision-safe typed context values and trusted-proxy client IPs
  • Supervised startup, shutdown, background tasks, liveness, and readiness
  • RFC 7240 asynchronous operations with polling and cancellation
  • Server-Sent Events through standard HTTP streaming primitives
  • WebSocket integration through standard handlers and request cancellation
  • gRPC integration through standard handlers and native HTTP/2 protocols
  • JSON, text, redirects, and empty responses
  • Cached path, query, header, cookie, form, multipart, and JSON binding
  • Centralized HTTP errors and transport-neutral application faults
  • Request IDs, structured logging, panic recovery, and configurable CORS
  • Graceful HTTP shutdown
  • Raw http.Handler mounting
  • Standard httptest compatibility
  • vial dev automatic build-and-restart loop
  • vial load bounded HTTP load checks and CI thresholds
  • Last-known-good process remains online after compilation failures
  • No runtime dependencies outside the Go standard library

Examples

  1. examples/hello is the smallest runnable app and introduces the development loop.
  2. examples/json-api demonstrates JSON binding and errors. The module, task, upload, SSE, config, secure-cookie, CSRF, and template examples each cover one concern.
  3. examples/async demonstrates submission, Prefer: wait, polling, cancellation, ownership, idempotency, readiness, and metrics with the bounded in-memory executor.
  4. examples/websocket uses coder/websocket through a standard handler with Vial middleware, limits, and graceful shutdown.
  5. examples/grpc shares one h2c listener with HTTP routes and demonstrates standard interceptors, TLS, streaming, and graceful shutdown.
  6. vial-gateway and vialboard are complete applications.

Project status

Vial is pre-1.0. The API freeze is complete, but the project still needs more production evidence and a release-candidate cycle. Vial began as a learning project and accepts AI-assisted contributions. Those changes go through the same tests, review, security reporting, and compatibility policy as any other contribution. Read the release notes before using a pre-1.0 version in production.

Run it

The module lives at github.com/jrgf/go-vial.

git clone https://github.com/jrgf/go-vial.git
cd go-vial

make install
vial version

go test ./...
vial dev ./examples/hello

make install places vial in Go's binary directory (GOBIN, or $GOPATH/bin by default), which must be on PATH.

Open:

http://localhost:8080
http://localhost:8080/users/42

Change the response in examples/hello/main.go and save. The development runner will build a candidate binary, gracefully stop the previous application only after compilation succeeds, and start the replacement.

A failed build behaves like this:

[vial] source change detected; rebuilding
[vial] building ./examples/hello
examples/hello/main.go:20:2: undefined: broken
[vial] build failed; last successful application remains running

Minimal application

package main

import (
    "context"
    "log"
    "net/http"

    "github.com/jrgf/go-vial"
    "github.com/jrgf/go-vial/middleware"
)

func main() {
    app := vial.New(
        vial.WithDisallowUnknownJSONFields(true),
    )

    app.Use(
        middleware.RequestID(),
        middleware.Logger(),
        middleware.Recover(),
    )

    app.Get("/", func(c *vial.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "Hello from vial",
        })
    })

    app.Get("/users/{id}", func(c *vial.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "id": c.Param("id"),
        })
    })

    if err := app.Run(context.Background(), ":8080"); err != nil {
        log.Fatal(err)
    }
}

App implements http.Handler, so it also works without the framework server helper:

server := &http.Server{
    Addr:    ":8080",
    Handler: app,
}

Route groups

api := app.Group("/api")
api.Use(authenticationMiddleware)

api.Get("/users/{id}", getUser)
api.Post("/users", createUser)

Application middleware wraps all requests, including 404 and 405 responses. Group middleware wraps only endpoints registered through that group.

Modules

A route group organizes HTTP endpoints. A module organizes one named business capability, such as users, billing, inventory, or administration.

type UserModule struct {
    service *UserService
}

func NewModule(service *UserService) *UserModule { return &UserModule{service: service} }

func (module *UserModule) Name() string { return "users" }

func (module *UserModule) Register(registrar *vial.Registrar) error {
    users := registrar.Group("/users")
    users.Get("/", module.list, vial.RouteName("users.list"))
    users.Post("/", module.create, vial.RouteName("users.create"))
    users.Get("/{id}", module.get, vial.RouteName("users.get"))
    return nil
}

Register modules before building or serving the application:

if err := app.Register(
    users.NewModule(userService),
    orders.NewModule(orderService),
); err != nil {
    return err
}

Registration is atomic. Each module gets an isolated registrar for routes, middleware, lifecycle hooks, supervised tasks, and health endpoints. See the runnable examples/modules.

Modules contain application functionality. Extensions provide technical infrastructure, such as sessions, databases, telemetry, authentication, task supervision, or a gRPC server. Vial does not require an extension interface; ordinary constructors and application options remain sufficient.

JSON binding

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

app.Post("/users", func(c *vial.Context) error {
    var request CreateUserRequest
    if err := c.BindJSON(&request); err != nil {
        return err
    }

    return c.JSON(http.StatusCreated, request)
})

The default body limit is 16 MiB. Applications can override it and reject unknown JSON fields:

app := vial.New(
	vial.WithMaxBodySize(10 << 20),
    vial.WithDisallowUnknownJSONFields(true),
)

Context.Bind combines fields tagged with path, query, header, cookie, and form with JSON or form bodies. Uploaded files use Context.FormFile; see the runnable examples/upload.

HTML templates

Keep parsing in html/template; render buffers the result before committing the response:

parsed, err := template.ParseFS(webFS, "templates/*.html")
if err != nil {
    return err
}
views := render.New(parsed)

app.Get("/", func(c *vial.Context) error {
    return views.HTML(c, http.StatusOK, "home", page)
})

The runnable examples/web adds a validated, CSRF-protected form and uses embed.FS with the native http.FileServerFS for static assets:

VIAL_ALLOW_INSECURE_COOKIE=1 vial dev ./examples/web

The insecure-cookie flag is only for local HTTP.

Vial leaves session policy opt-in. The isolated examples/securecookie module demonstrates signed sessions, key rotation, and one-time flash messages with securecookie:

umask 077
openssl rand -hex 32 > /tmp/vial-session-keys
SESSION_KEYS_FILE=/tmp/vial-session-keys VIAL_ALLOW_INSECURE_COOKIE=1 vial dev ./examples/securecookie

The example reloads this file every minute and expires sessions after five minutes. Rotate with NEW_KEY,OLD_KEY, newest first; remove the old key after five minutes.

VIAL_ALLOW_INSECURE_COOKIE=1 is only for local HTTP. Cookie contents are authenticated but not encrypted, so never store secrets in them. SameSite is defense in depth, not a replacement for CSRF protection.

Testing

testkit runs the full application lifecycle and cleans up automatically:

server := testkit.Start(t, app)
response := server.JSON(http.MethodPost, "/users", request)
response.RequireStatus(http.StatusCreated)

var created User
response.Decode(&created)

Raw net/http requests and httptest remain supported.

Async operations

Register a bounded executor once; Vial starts it before serving HTTP and drains accepted work during graceful shutdown:

executor := async.NewMemoryExecutor(
    async.WithWorkers(8),
    async.WithQueueSize(256),
    async.WithTaskTimeout(5*time.Minute),
)
executor.Handle("reports.generate", func(ctx context.Context, job async.Job) (any, error) {
    var request GenerateReportRequest
    if err := job.Decode(&request); err != nil {
        return nil, err
    }
    return reportService.Generate(ctx, request)
})
app.Async(executor)

app.Post("/reports", func(c *vial.Context) error {
    preference, err := vial.ParsePrefer(c.Header("Prefer"))
    if err != nil {
        return err
    }
    if !preference.RespondAsync {
        return vial.BadRequest("respond_async_required", "Use Prefer: respond-async")
    }
    operation, err := c.Async().Submit(c.Request().Context(), vial.SubmitRequest{
        Name:             "reports.generate",
        Payload:          GenerateReportRequest{ReportID: "report_123"},
        IdempotencyKey:   c.Header("Idempotency-Key"),
        IdempotencyScope: currentUserID(c),
        Metadata:         map[string]string{"user_id": currentUserID(c)},
    })
    if err != nil {
        return err
    }

    operation, completed, err := c.Await(operation, 3*time.Second)
    if err != nil {
        return err
    }
    if completed {
        if operation.Error != nil {
            return operation.Error
        }
        if operation.Status == vial.OperationCancelled {
            return vial.Conflict("operation_cancelled", "The operation was cancelled")
        }
        return c.JSON(http.StatusCreated, operation.Result)
    }
    return c.Accepted(operation)
})

authorize := func(c *vial.Context, operation *vial.Operation) error {
    if operation.Metadata["user_id"] != currentUserID(c) {
        return vial.NotFound("operation_not_found", "Operation not found")
    }
    return nil
}
app.Get("/operations/{id}", vial.OperationStatusHandler(executor, authorize))
app.Delete("/operations/{id}", vial.OperationCancelHandler(executor, authorize))
app.Get("/metrics/async", vial.AsyncMetricsHandler(executor))
app.Readiness("/ready", executor.Ready)

Accepted returns 202 Accepted with Location, Retry-After, and a status URL. A full queue returns 503 Service Unavailable; the same operation name, authenticated-owner scope, and idempotency key returns the existing operation. Prefer: respond-async, wait=3 waits up to the smaller client preference and server maximum before falling back to 202. Return resource URLs instead of embedding large results.

The memory executor is not durable. Every pending, running, completed, and idempotency record is lost when the process exits. Use it only when losing work is acceptable.

For durable work, use the PostgreSQL adapter with any database/sql PostgreSQL driver already selected by the application:

executor := asyncpostgres.New(db,
    asyncpostgres.WithWorkers(16),
    asyncpostgres.WithLeaseDuration(30*time.Second),
)
executor.Handle("reports.generate", generateReport)
app.Async(executor)

operation, err := executor.Submit(ctx, vial.SubmitRequest{
    Name:             "reports.generate",
    Payload:          request,
    IdempotencyKey:   idempotencyKey,
    IdempotencyScope: authenticatedUserID,
    Retry: vial.RetryPolicy{
        MaxAttempts:    5,
        InitialBackoff: time.Second,
        MaxBackoff:     time.Minute,
    },
})

The adapter creates its schema idempotently by default. It persists operations and results, leases rows with FOR UPDATE SKIP LOCKED, renews visibility while handlers run, recovers expired leases after crashes, supports multiple replicas, and leaves exhausted deliveries as inspectable failed operations. Disable automatic schema creation with asyncpostgres.WithAutoMigrate(false) after applying contrib/asyncpostgres/schema.sql through the application's migration system.

Background tasks

Keep shared queues application-owned; use contexts only for cancellation:

type Event struct {
    Name string
}

events := make(chan Event, 128)

app.Post("/events", func(c *vial.Context) error {
    select {
    case events <- Event{Name: "user.created"}:
        return c.NoContent(http.StatusAccepted)
    case <-c.Request().Context().Done():
        return c.Request().Context().Err()
    default:
        return vial.NewHTTPError(503, "queue_full", "Event queue is full")
    }
})

app.Go("event-consumer", func(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case event := <-events:
            if err := handleEvent(ctx, event); err != nil {
                return err
            }
        }
    }
})

This queue is in-memory and loses pending events when the process stops. Use a database outbox or external broker when events must be durable. See the runnable examples/events.

Configuration

Use config.Load with an initialized struct. Existing values are defaults, JSON files are applied in order, and environment variables win:

type AppConfig struct {
    Environment string `json:"environment" env:"VIAL_ENV"`
    HTTP        config.HTTP `json:"http"`
}

func (configuration *AppConfig) Validate() error {
    if configuration.Environment == "" {
        return errors.New("environment is required")
    }
    return nil
}

configuration := AppConfig{Environment: "development"}

if err := config.Load(
    &configuration,
    config.OptionalFile("config.json"),
); err != nil {
    log.Fatal(err)
}

if err := app.Run(context.Background(), configuration.HTTP.Address()); err != nil {
    log.Fatal(err)
}

An optional config.json can override the defaults:

{
  "http": {
    "port": 9000
  }
}

Environment variables take final precedence:

VIAL_HTTP_PORT=9090 go run .

Use config.File when a file is required. Configuration types may implement config.Validator; validation runs after all sources are loaded.

config.HTTP.Address() defaults to 127.0.0.1:8080. Production deployments that must accept external traffic can set VIAL_HTTP_HOST=0.0.0.0 explicitly. See the runnable examples/config application.

Errors

Handlers return an error. Business failures use transport-neutral faults:

return fault.New(
    fault.NotFound,
    "user_not_found",
    "The requested user was not found",
)

Use HTTPError only when an HTTP-specific status or response header is needed.

Default response:

{
  "error": {
    "code": "user_not_found",
    "message": "The requested user was not found"
  }
}

Unexpected errors are rendered as a generic 500 response. Their internal details remain in server logs. Unmatched routes and unsupported methods use the same error handler and return not_found and method_not_allowed codes; 405 responses include Allow.

Mount standard handlers

app.HandleHTTP("GET /health", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
    w.WriteHeader(http.StatusNoContent)
}))

Mount a raw handler for existing middleware, metrics endpoints, profilers, and other net/http integrations.

Development runner

Build the CLI:

go install ./cmd/vial

Usage:

vial dev [flags] [package] [-- application arguments]

Examples:

vial dev ./examples/hello
vial dev --verbose ./cmd/server
vial dev --debounce 500ms ./cmd/server
vial dev --exclude generated ./cmd/server
vial dev ./cmd/server -- --config ./config/dev.json

Flags:

Flag Default Meaning
--root current directory Directory to watch and use as the build working directory
--debounce 250ms Quiet period before rebuilding
--restart-timeout 3s Time allowed for graceful child shutdown
--exclude none Additional ignored name or path; repeatable
--verbose false Print each relevant changed path

The MVP watcher recursively scans:

  • *.go
  • go.mod
  • go.sum
  • go.work
  • go.work.sum

It ignores:

  • .git
  • .vial
  • vendor
  • node_modules
  • tmp
  • dist

Generated binaries are written under .vial/bin and are ignored by the watcher.

Inspect routes without starting the server:

vial routes ./examples/hello
vial routes --json ./examples/hello

This command supports applications that call App.Run; applications using a custom http.Server can call App.Routes directly.

Validate configuration and application build setup without starting the server:

vial doctor ./examples/config

Run a bounded load check against a deployed endpoint:

vial load --workers 2000 --duration 30s --timeout 5s http://localhost:8080/
vial load --max-error-rate 1 --max-p95 250ms http://localhost:8080/
CLI contract
  • vial, help, --help, -h, and subcommand help exit with status 0.
  • Unknown commands, invalid arguments, runtime failures, and failed load thresholds exit with status 1.
  • vial routes --json writes an indented JSON array of vial.Route values to standard output.
  • vial version --verbose writes stable version=, commit=, and go= lines to standard output.
  • vial load writes its final summary to standard output and progress to standard error, keeping redirected summaries clean.

Human-readable help, tables, progress text, and error wording may improve within 1.x. Exit behavior and the machine-readable formats above are compatibility contracts.

Replacement sequence
source change
    ↓
debounce
    ↓
build unique candidate binary
    ├── failure → retain current process
    └── success
            ↓
       interrupt current process
            ↓
       wait up to restart timeout
            ↓
       start candidate

Builds never run concurrently. Changes detected during a build remain queued for the next pass.

Repository layout

.
├── app.go                 # application, registration, and net/http adapter
├── context.go             # request and response helpers
├── binding.go             # query, form, multipart, and JSON body limits
├── errors.go              # HTTP error model and renderer
├── server.go              # server lifecycle and graceful shutdown
├── middleware/            # request ID, logging, recovery, CORS, and CSRF
├── internal/dev/          # watcher, builder, runner, and process control
├── cmd/vial/              # development and load-check CLI
└── examples/              # runnable applications

Verification

go test ./...
go test -race ./...
go vet ./...

The codebase is also compile-checked for Windows and macOS in CI.

Known limitations

  • Development change detection uses recursive polling rather than native filesystem events.
  • Only Go source and module/workspace files trigger rebuilds.
  • Windows child replacement uses direct termination; graceful console-event delivery is a later enhancement.
  • Route registration becomes immutable after the application is built or first served.

Go support

Vial requires Go 1.26.6 or newer and currently tests Go 1.26 and Go 1.27.

Contributing

See CONTRIBUTING.md for development setup, quality requirements, and pull request guidance. Report vulnerabilities privately through SECURITY.md.

License

MIT

Documentation

Overview

Package vial is a small web application framework built on net/http. It adds error-returning handlers, middleware, request helpers, and graceful shutdown.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAsyncUnavailable   = errors.New("async executor is unavailable")
	ErrAsyncQueueFull     = errors.New("async executor queue is full")
	ErrInvalidOperation   = errors.New("invalid async operation")
	ErrOperationNotFound  = errors.New("operation not found")
	ErrOperationFinished  = errors.New("operation is already finished")
	ErrRetriesUnsupported = errors.New("async executor does not support retries")
	ErrInvalidPreference  = errors.New("invalid Prefer header")
)

Functions

func PrefersRespondAsync added in v0.16.0

func PrefersRespondAsync(header string) bool

PrefersRespondAsync reports whether Prefer requests RFC 7240 respond-async.

func StatusCode

func StatusCode(err error) int

StatusCode returns the HTTP status represented by err, defaulting to 500.

Types

type App

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

App is the framework application and implements http.Handler.

func New

func New(options ...Option) *App

New creates an application with the supplied options.

func (*App) Async added in v0.16.0

func (app *App) Async(executor AsyncExecutor)

Async registers the application's executor. Lifecycle executors are started before HTTP serving and drained during application shutdown.

func (*App) AsyncExecutor added in v0.16.0

func (app *App) AsyncExecutor() AsyncExecutor

AsyncExecutor returns the registered executor, or nil when none is configured.

func (*App) Build

func (app *App) Build() error

Build validates and freezes application registrations.

func (*App) Delete

func (app *App) Delete(path string, handler Handler, options ...RouteOption)

Delete registers a DELETE route.

func (*App) Get

func (app *App) Get(path string, handler Handler, options ...RouteOption)

Get registers a GET route.

func (*App) Go added in v0.7.0

func (app *App) Go(name string, task Task, options ...TaskOption)

Go registers a named background task. Tasks are critical by default and begin running when the application starts.

func (*App) Group

func (app *App) Group(prefix string) *Group

Group creates a route group with a shared path prefix.

func (*App) Handle

func (app *App) Handle(method, path string, handler Handler, options ...RouteOption)

Handle registers a route for an HTTP method and path.

func (*App) HandleHTTP

func (app *App) HandleHTTP(pattern string, handler http.Handler, options ...RouteOption)

HandleHTTP mounts a standard-library handler using a native ServeMux pattern. Examples include "/metrics", "GET /health", and "api.example.com/".

func (*App) Health added in v0.15.0

func (app *App) Health(path string)

Health registers a dependency-free liveness endpoint.

func (*App) OnStart added in v0.4.0

func (app *App) OnStart(hooks ...LifecycleHook)

OnStart registers hooks that run in registration order before components start.

func (*App) OnStop added in v0.4.0

func (app *App) OnStop(hooks ...LifecycleHook)

OnStop registers hooks that run in reverse registration order after components stop.

func (*App) Options

func (app *App) Options(path string, handler Handler, options ...RouteOption)

Options registers an OPTIONS route.

func (*App) Patch

func (app *App) Patch(path string, handler Handler, options ...RouteOption)

Patch registers a PATCH route.

func (*App) Post

func (app *App) Post(path string, handler Handler, options ...RouteOption)

Post registers a POST route.

func (*App) Put

func (app *App) Put(path string, handler Handler, options ...RouteOption)

Put registers a PUT route.

func (*App) Readiness added in v0.15.0

func (app *App) Readiness(path string, checks ...HealthCheck)

Readiness registers an endpoint that is ready only while the application is running and every check succeeds within its configured timeout.

func (*App) Register added in v0.4.0

func (app *App) Register(modules ...Module) error

Register stages modules and commits their routes atomically.

func (*App) Routes added in v0.2.0

func (app *App) Routes() ([]Route, error)

Routes validates the application and returns registered route metadata in registration order. The returned slice does not share state with the app.

func (*App) Run

func (app *App) Run(contextValue context.Context, address string) error

Run listens on address and serves until the parent context or a supported OS shutdown signal is received.

func (*App) Serve

func (app *App) Serve(contextValue context.Context, listener net.Listener) (serveErr error)

Serve runs the application on an existing listener. This is useful for tests and for callers that need custom listener configuration.

func (*App) ServeHTTP

func (app *App) ServeHTTP(writer http.ResponseWriter, request *http.Request)

ServeHTTP implements http.Handler and builds the application on first use.

func (*App) SetErrorHandler

func (app *App) SetErrorHandler(handler ErrorHandler)

SetErrorHandler replaces the application's error renderer.

func (*App) Use

func (app *App) Use(middleware ...Middleware)

Use adds middleware to the application-wide chain.

type AsyncExecutor added in v0.16.0

type AsyncExecutor interface {
	Submit(context.Context, SubmitRequest) (*Operation, error)
	Get(context.Context, string) (*Operation, error)
	Cancel(context.Context, string) error
}

AsyncExecutor submits, retrieves, and cancels asynchronous operations.

type AsyncHandler added in v0.16.0

type AsyncHandler func(context.Context, AsyncJob) (any, error)

AsyncHandler processes one operation delivery.

type AsyncJob added in v0.16.0

type AsyncJob interface {
	ID() string
	Name() string
	Decode(any) error
	Metadata() map[string]string
	Progress(context.Context, int) error
}

AsyncJob is immutable application-level input supplied to an operation handler.

type AsyncLifecycleExecutor added in v0.16.0

type AsyncLifecycleExecutor interface {
	AsyncExecutor
	Start(context.Context) error
	Done() <-chan error
	Shutdown(context.Context) error
}

AsyncLifecycleExecutor is an executor managed by the Vial application lifecycle. External durable executors may implement only AsyncExecutor.

type AsyncMetrics added in v0.16.0

type AsyncMetrics struct {
	SubmittedTotal        uint64
	CompletedTotal        uint64
	FailedTotal           uint64
	CancelledTotal        uint64
	RetriedTotal          uint64
	QueueDepth            int64
	Running               int64
	DurationSecondsTotal  float64
	DurationCount         uint64
	QueueWaitSecondsTotal float64
	QueueWaitCount        uint64
}

AsyncMetrics is a point-in-time executor metrics snapshot.

type AsyncMetricsProvider added in v0.16.0

type AsyncMetricsProvider interface {
	Metrics(context.Context) (AsyncMetrics, error)
}

AsyncMetricsProvider supplies executor metrics.

type AsyncPreference added in v0.16.0

type AsyncPreference struct {
	RespondAsync  bool
	Wait          time.Duration
	WaitSpecified bool
}

AsyncPreference is the RFC 7240 asynchronous preference requested by a client.

func ParsePrefer added in v0.16.0

func ParsePrefer(header string) (AsyncPreference, error)

ParsePrefer parses respond-async and wait=n while ignoring unknown preferences.

type AsyncWaiter added in v0.16.0

type AsyncWaiter interface {
	Wait(context.Context, string) (*Operation, error)
}

AsyncWaiter efficiently waits for an operation state change or completion.

type BindingError added in v0.4.0

type BindingError struct {
	Source string
	Field  string
	Cause  error
}

BindingError identifies the request field that could not be bound.

func (*BindingError) Error added in v0.4.0

func (err *BindingError) Error() string

Error describes the binding source, field, and cause.

func (*BindingError) Unwrap added in v0.4.0

func (err *BindingError) Unwrap() error

Unwrap returns the binding failure cause.

type Context

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

Context contains request-scoped framework state while retaining direct access to the standard net/http request and response writer.

func ContextFromRequest added in v0.15.0

func ContextFromRequest(request *http.Request) (*Context, bool)

ContextFromRequest returns the Vial context attached to a routed request. It lets standard net/http handlers access route metadata without exported context keys that could collide with application values.

func (*Context) Accepted added in v0.16.0

func (contextValue *Context) Accepted(operation *Operation) error

Accepted writes the conventional 202 response for an asynchronous operation.

func (*Context) AcceptedAt added in v0.16.0

func (contextValue *Context) AcceptedAt(operation *Operation, statusURL string) error

AcceptedAt writes a 202 response whose Location points at statusURL.

func (*Context) App

func (context *Context) App() *App

App returns the application serving the request.

func (*Context) Async added in v0.16.0

func (contextValue *Context) Async() AsyncExecutor

Async returns the application's registered executor.

func (*Context) Await added in v0.16.0

func (contextValue *Context) Await(operation *Operation, maximum time.Duration) (current *Operation, completed bool, err error)

Await waits up to the client preference and server maximum. completed is true only when the returned operation reached a terminal state.

func (*Context) Bind added in v0.4.0

func (context *Context) Bind(destination any) error

Bind combines tagged path, query, header, cookie, JSON, and form input.

func (*Context) BindCookie added in v0.4.0

func (context *Context) BindCookie(destination any) error

BindCookie binds request cookies to fields tagged with `cookie`.

func (*Context) BindForm added in v0.3.0

func (context *Context) BindForm(destination any) error

BindForm binds URL-encoded or multipart form fields to primitive struct fields tagged with `form`. Repeated values bind to slices.

func (*Context) BindHeader added in v0.4.0

func (context *Context) BindHeader(destination any) error

BindHeader binds request headers to fields tagged with `header`.

func (*Context) BindJSON

func (context *Context) BindJSON(destination any) error

BindJSON decodes a single JSON value into destination using the application's body limit and unknown-field policy.

func (*Context) BindPath added in v0.4.0

func (context *Context) BindPath(destination any) error

BindPath binds route parameters to fields tagged with `path`.

func (*Context) BindQuery added in v0.3.0

func (context *Context) BindQuery(destination any) error

BindQuery binds query parameters to primitive struct fields tagged with `query`. Repeated values bind to slices.

func (*Context) BytesWritten

func (context *Context) BytesWritten() int64

BytesWritten returns the response body bytes written so far.

func (*Context) ClientIP added in v0.15.0

func (context *Context) ClientIP() (netip.Addr, error)

ClientIP returns the direct peer unless it is an explicitly trusted proxy. Trusted proxy chains are evaluated from the application outward, stopping at the first untrusted address.

func (*Context) Committed

func (context *Context) Committed() bool

Committed reports whether the response headers were written.

func (*Context) Flush added in v0.15.0

func (context *Context) Flush() error

Flush sends buffered response data when the server supports streaming.

func (*Context) FormFile added in v0.3.0

func (context *Context) FormFile(name string) (multipart.File, *multipart.FileHeader, error)

FormFile returns the first uploaded file for name. The caller must close the file. Vial removes any temporary multipart files after the request finishes.

func (*Context) FormValue added in v0.10.0

func (context *Context) FormValue(name string) (string, error)

FormValue returns the first URL-encoded or multipart form value for name.

func (*Context) Get

func (context *Context) Get(key string) (any, bool)

Get returns a request-scoped value.

func (*Context) Header

func (context *Context) Header(name string) string

Header returns a request header value.

func (*Context) JSON

func (context *Context) JSON(status int, value any) error

JSON writes a JSON response with status.

func (*Context) Logger

func (context *Context) Logger() *slog.Logger

Logger returns the request logger.

func (*Context) NoContent

func (context *Context) NoContent(status int) error

NoContent writes a response status without a body.

func (*Context) Param

func (context *Context) Param(name string) string

Param returns a path parameter by name.

func (*Context) Query

func (context *Context) Query(name string) string

Query returns the first query value for name.

func (*Context) Redirect

func (context *Context) Redirect(status int, location string) error

Redirect writes an HTTP redirect response.

func (*Context) Request

func (context *Context) Request() *http.Request

Request returns the underlying HTTP request.

func (*Context) Response

func (context *Context) Response() http.ResponseWriter

Response returns the tracked HTTP response writer.

func (*Context) ResponseController added in v0.15.0

func (context *Context) ResponseController() *http.ResponseController

ResponseController exposes standard-library optional response operations.

func (*Context) Route

func (context *Context) Route() *Route

Route returns metadata for the matched route.

func (*Context) Set

func (context *Context) Set(key string, value any)

Set stores a request-scoped value.

func (*Context) SetLogger

func (context *Context) SetLogger(logger *slog.Logger)

SetLogger replaces the request logger when logger is non-nil.

func (*Context) SetWriteDeadline added in v0.15.0

func (context *Context) SetWriteDeadline(deadline time.Time) error

SetWriteDeadline sets the response write deadline when supported.

func (*Context) Status

func (context *Context) Status() int

Status returns the response status written so far.

func (*Context) Text

func (context *Context) Text(status int, value string) error

Text writes a plain-text response with status.

type ErrorHandler

type ErrorHandler func(*Context, error)

ErrorHandler renders errors returned by handlers or middleware.

type Group

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

Group applies a common path prefix and middleware chain to related routes.

func (*Group) Delete

func (group *Group) Delete(path string, handler Handler, options ...RouteOption)

Delete registers a DELETE route in the group.

func (*Group) Get

func (group *Group) Get(path string, handler Handler, options ...RouteOption)

Get registers a GET route in the group.

func (*Group) Group

func (group *Group) Group(prefix string) *Group

Group creates a nested group that inherits the current middleware chain.

func (*Group) Handle

func (group *Group) Handle(method, path string, handler Handler, options ...RouteOption)

Handle registers a route in the group.

func (*Group) HandleHTTP added in v0.15.0

func (group *Group) HandleHTTP(pattern string, handler http.Handler, options ...RouteOption)

HandleHTTP registers a standard library handler under the group's prefix.

func (*Group) Options

func (group *Group) Options(path string, handler Handler, options ...RouteOption)

Options registers an OPTIONS route in the group.

func (*Group) Patch

func (group *Group) Patch(path string, handler Handler, options ...RouteOption)

Patch registers a PATCH route in the group.

func (*Group) Post

func (group *Group) Post(path string, handler Handler, options ...RouteOption)

Post registers a POST route in the group.

func (*Group) Put

func (group *Group) Put(path string, handler Handler, options ...RouteOption)

Put registers a PUT route in the group.

func (*Group) Use

func (group *Group) Use(middleware ...Middleware)

Use adds middleware to routes subsequently registered through the group.

type HTTPError

type HTTPError struct {
	Status  int
	Code    string
	Message string
	Cause   error
	Headers http.Header
}

HTTPError is an application error with a stable HTTP representation.

func BadRequest

func BadRequest(code, message string) *HTTPError

BadRequest creates a 400 HTTP error.

func Conflict

func Conflict(code, message string) *HTTPError

Conflict creates a 409 HTTP error.

func Forbidden

func Forbidden(code, message string) *HTTPError

Forbidden creates a 403 HTTP error.

func InternalServerError

func InternalServerError(cause error) *HTTPError

InternalServerError creates a generic 500 HTTP error that retains cause.

func MethodNotAllowed added in v0.3.0

func MethodNotAllowed(code, message string) *HTTPError

MethodNotAllowed creates a 405 HTTP error.

func NewHTTPError

func NewHTTPError(status int, code, message string) *HTTPError

NewHTTPError creates an HTTP error with a stable status, code, and message.

func NotFound

func NotFound(code, message string) *HTTPError

NotFound creates a 404 HTTP error.

func RequestEntityTooLarge

func RequestEntityTooLarge(code, message string) *HTTPError

RequestEntityTooLarge creates a 413 HTTP error.

func Unauthorized

func Unauthorized(code, message string) *HTTPError

Unauthorized creates a 401 HTTP error.

func UnsupportedMediaType

func UnsupportedMediaType(code, message string) *HTTPError

UnsupportedMediaType creates a 415 HTTP error.

func WrapHTTPError

func WrapHTTPError(status int, code, message string, cause error) *HTTPError

WrapHTTPError creates an HTTP error that retains cause.

func (*HTTPError) Error

func (err *HTTPError) Error() string

Error returns the public error message and retained cause.

func (*HTTPError) Unwrap

func (err *HTTPError) Unwrap() error

Unwrap returns the retained cause.

type Handler

type Handler func(*Context) error

Handler is the framework's HTTP handler contract. Returning errors keeps transport error rendering centralized and makes middleware composition simple.

func AsyncMetricsHandler added in v0.16.0

func AsyncMetricsHandler(provider AsyncMetricsProvider) Handler

AsyncMetricsHandler exposes dependency-free Prometheus/OpenMetrics metrics.

func OperationCancelHandler added in v0.16.0

func OperationCancelHandler(executor AsyncExecutor, authorize OperationAuthorizer) Handler

OperationCancelHandler returns a cancellation handler for a route containing {id}.

func OperationStatusHandler added in v0.16.0

func OperationStatusHandler(executor AsyncExecutor, authorize OperationAuthorizer) Handler

OperationStatusHandler returns a polling handler for a route containing {id}.

type HealthCheck added in v0.15.0

type HealthCheck func(context.Context) error

HealthCheck reports whether one readiness dependency can currently serve.

type LifecycleHook added in v0.4.0

type LifecycleHook func(context.Context) error

LifecycleHook runs while the application starts or stops.

type Middleware

type Middleware func(Handler) Handler

Middleware wraps a Handler with cross-cutting behavior.

type Module added in v0.4.0

type Module interface {
	Name() string
	Register(*Registrar) error
}

Module registers one named application capability.

type Operation added in v0.16.0

type Operation struct {
	ID            string            `json:"id"`
	Name          string            `json:"name"`
	Status        OperationStatus   `json:"status"`
	Progress      int               `json:"progress"`
	CreatedAt     time.Time         `json:"created_at"`
	StartedAt     *time.Time        `json:"started_at"`
	FinishedAt    *time.Time        `json:"finished_at"`
	Result        any               `json:"result"`
	Error         *OperationError   `json:"error"`
	Metadata      map[string]string `json:"-"`
	Attempt       int               `json:"attempt"`
	MaxAttempts   int               `json:"max_attempts"`
	NextAttemptAt *time.Time        `json:"next_attempt_at"`
}

Operation describes submitted background work.

type OperationAuthorizer added in v0.16.0

type OperationAuthorizer func(*Context, *Operation) error

OperationAuthorizer applies application ownership rules before status or cancellation is exposed. Helpers require one to avoid globally readable IDs.

type OperationError added in v0.16.0

type OperationError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

OperationError is the safe, client-visible reason an operation failed.

func (*OperationError) Error added in v0.16.0

func (operationError *OperationError) Error() string

type OperationStatus added in v0.16.0

type OperationStatus string

OperationStatus is the current state of an asynchronous operation.

const (
	OperationPending   OperationStatus = "pending"
	OperationRunning   OperationStatus = "running"
	OperationRetrying  OperationStatus = "retrying"
	OperationSucceeded OperationStatus = "succeeded"
	OperationFailed    OperationStatus = "failed"
	OperationCancelled OperationStatus = "cancelled"
)

func (OperationStatus) Terminal added in v0.16.0

func (status OperationStatus) Terminal() bool

Terminal reports whether no further state transition is possible.

type Option

type Option func(*config)

Option configures an App.

func WithBaseContext added in v0.15.0

func WithBaseContext(baseContext func(net.Listener) context.Context) Option

WithBaseContext sets http.Server.BaseContext.

func WithConnContext added in v0.15.0

func WithConnContext(connContext func(context.Context, net.Conn) context.Context) Option

WithConnContext sets http.Server.ConnContext.

func WithDisallowUnknownJSONFields

func WithDisallowUnknownJSONFields(enabled bool) Option

WithDisallowUnknownJSONFields controls rejection of unknown JSON fields.

func WithHTTPProtocols added in v0.18.0

func WithHTTPProtocols(protocols *http.Protocols) Option

WithHTTPProtocols sets http.Server.Protocols.

func WithHealthCheckTimeout added in v0.15.0

func WithHealthCheckTimeout(timeout time.Duration) Option

WithHealthCheckTimeout sets the maximum duration of each readiness check.

func WithIdleTimeout

func WithIdleTimeout(timeout time.Duration) Option

WithIdleTimeout sets the HTTP server keep-alive idle timeout.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the application logger.

func WithMaxBodySize

func WithMaxBodySize(bytes int64) Option

WithMaxBodySize sets the request body limit when bytes is positive.

func WithMaxHeaderBytes added in v0.15.0

func WithMaxHeaderBytes(bytes int) Option

WithMaxHeaderBytes sets the maximum request-header size.

func WithReadHeaderTimeout

func WithReadHeaderTimeout(timeout time.Duration) Option

WithReadHeaderTimeout sets the HTTP server header-read timeout.

func WithReadTimeout

func WithReadTimeout(timeout time.Duration) Option

WithReadTimeout sets the HTTP server request-read timeout.

func WithServerErrorLog added in v0.15.0

func WithServerErrorLog(logger *log.Logger) Option

WithServerErrorLog sets http.Server.ErrorLog.

func WithShutdownTimeout

func WithShutdownTimeout(timeout time.Duration) Option

WithShutdownTimeout sets the graceful shutdown deadline when timeout is positive.

func WithTrustedProxies added in v0.15.0

func WithTrustedProxies(values ...string) Option

WithTrustedProxies trusts forwarding headers only when the direct peer matches one of the listed IPs or CIDRs. The default trusts none.

func WithWriteTimeout

func WithWriteTimeout(timeout time.Duration) Option

WithWriteTimeout sets the HTTP server response-write timeout.

type Registrar added in v0.4.0

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

Registrar exposes the route registration surface available to modules.

func (*Registrar) Go added in v0.15.0

func (registrar *Registrar) Go(name string, task Task, options ...TaskOption)

Go registers a supervised module background task.

func (*Registrar) Group added in v0.4.0

func (registrar *Registrar) Group(prefix string) *Group

Group creates a module route group.

func (*Registrar) Handle added in v0.4.0

func (registrar *Registrar) Handle(method, path string, handler Handler, options ...RouteOption)

Handle registers a module route.

func (*Registrar) HandleHTTP added in v0.4.0

func (registrar *Registrar) HandleHTTP(pattern string, handler http.Handler, options ...RouteOption)

HandleHTTP mounts a standard-library handler in the module.

func (*Registrar) Health added in v0.15.0

func (registrar *Registrar) Health(path string)

Health registers a module liveness endpoint.

func (*Registrar) OnStart added in v0.15.0

func (registrar *Registrar) OnStart(hooks ...LifecycleHook)

OnStart registers module startup hooks.

func (*Registrar) OnStop added in v0.15.0

func (registrar *Registrar) OnStop(hooks ...LifecycleHook)

OnStop registers module shutdown hooks.

func (*Registrar) Readiness added in v0.15.0

func (registrar *Registrar) Readiness(path string, checks ...HealthCheck)

Readiness registers a module readiness endpoint.

func (*Registrar) Use added in v0.4.0

func (registrar *Registrar) Use(middleware ...Middleware)

Use adds middleware to every route registered by the module.

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

ResponseWriter tracks response state while retaining access to the underlying standard-library writer through Unwrap.

func (*ResponseWriter) BytesWritten

func (writer *ResponseWriter) BytesWritten() int64

BytesWritten returns the response body bytes written so far.

func (*ResponseWriter) Committed

func (writer *ResponseWriter) Committed() bool

Committed reports whether response headers were written.

func (*ResponseWriter) Status

func (writer *ResponseWriter) Status() int

Status returns the written status or 200 before commitment.

func (*ResponseWriter) Unwrap

func (writer *ResponseWriter) Unwrap() http.ResponseWriter

Unwrap returns the underlying standard-library response writer.

func (*ResponseWriter) Write

func (writer *ResponseWriter) Write(data []byte) (int, error)

Write writes response data and records its size.

func (*ResponseWriter) WriteHeader

func (writer *ResponseWriter) WriteHeader(status int)

WriteHeader records and writes the first response status.

type RetryPolicy added in v0.16.0

type RetryPolicy struct {
	MaxAttempts    int
	InitialBackoff time.Duration
	MaxBackoff     time.Duration
}

RetryPolicy configures durable redelivery after transient worker failures. Its zero value permits one attempt and therefore disables retries.

type Route

type Route struct {
	Method          string
	Path            string
	Pattern         string
	Name            string
	Module          string
	MiddlewareCount int
	Parameters      []string
}

Route contains metadata for a registered endpoint.

type RouteOption added in v0.4.0

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

RouteOption configures route metadata.

func RouteMiddleware added in v0.15.0

func RouteMiddleware(middleware ...Middleware) RouteOption

RouteMiddleware applies middleware only to the configured route.

func RouteName added in v0.4.0

func RouteName(name string) RouteOption

RouteName assigns a globally unique, stable observability and diagnostics identifier. Route names are not used for URL generation.

type SubmitRequest added in v0.16.0

type SubmitRequest struct {
	Name             string
	Payload          any
	IdempotencyKey   string
	IdempotencyScope string
	Metadata         map[string]string
	Retry            RetryPolicy
}

SubmitRequest contains serializable, application-level data for an operation. IdempotencyScope must identify the authenticated owner when IdempotencyKey is set.

type Task added in v0.7.0

type Task func(context.Context) error

Task is application work that runs until its context is canceled or it fails.

type TaskOption added in v0.7.0

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

TaskOption configures a background task.

func NonCritical added in v0.7.0

func NonCritical() TaskOption

NonCritical keeps the application running when the task exits or fails.

type Validator added in v0.10.0

type Validator interface {
	Validate() error
}

Validator validates bound request data. Returning a *fault.Error preserves its field errors for response handling and rendering.

type ValueKey added in v0.15.0

type ValueKey[T any] struct {
	// contains filtered or unexported fields
}

ValueKey is a collision-safe request value key. Keep and reuse the pointer returned by NewValueKey; identity, not the display name, selects the value.

func NewValueKey added in v0.15.0

func NewValueKey[T any](name string) *ValueKey[T]

NewValueKey creates a typed request value key.

func (*ValueKey[T]) FromRequest added in v0.15.0

func (key *ValueKey[T]) FromRequest(request *http.Request) (T, bool)

FromRequest returns the same typed value through the underlying request.

func (*ValueKey[T]) Get added in v0.15.0

func (key *ValueKey[T]) Get(context *Context) (T, bool)

Get returns a typed value from a Vial context.

func (*ValueKey[T]) Name added in v0.15.0

func (key *ValueKey[T]) Name() string

Name returns the diagnostic name of the key. Names do not affect identity.

func (*ValueKey[T]) Set added in v0.15.0

func (key *ValueKey[T]) Set(context *Context, value T)

Set stores a value for the current request. Values are safe for concurrent access during the request but should not be retained after it completes.

Directories

Path Synopsis
Package async contains Vial's bounded in-memory executor.
Package async contains Vial's bounded in-memory executor.
cmd
vial command
Package config loads typed application configuration from explicit files and environment variables.
Package config loads typed application configuration from explicit files and environment variables.
contrib
asyncpostgres
Package asyncpostgres implements Vial's durable PostgreSQL executor.
Package asyncpostgres implements Vial's durable PostgreSQL executor.
examples
async command
config command
events command
hello command
json-api command
modules command
sse command
tasks command
upload command
web command
Package fault defines transport-neutral application errors.
Package fault defines transport-neutral application errors.
internal
dev
load
Package load runs bounded HTTP load checks for the vial command.
Package load runs bounded HTTP load checks for the vial command.
Package middleware contains the framework's optional default HTTP middleware.
Package middleware contains the framework's optional default HTTP middleware.
Package render writes html/template output through Vial handlers.
Package render writes html/template output through Vial handlers.
Package testkit runs Vial applications through their full lifecycle in HTTP tests.
Package testkit runs Vial applications through their full lifecycle in HTTP tests.
vialgrpc module
vialws module

Jump to

Keyboard shortcuts

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