qinetic

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 27 Imported by: 0

README

qinetic

qinetic is a Go web framework with a Fiber-style API, built on the qilat HTTP engine instead of fasthttp.

From qilat it inherits:

  • HTTP/1.1, HTTP/2, and HTTP/3 served by one app;
  • a request context that is canceled when the client disconnects;
  • an allocation-free hot path and a net/http adapter.

qinetic adds routing with :param and * patterns, groups, a middleware chain with c.Next(), and ready-made middleware.

Status

qinetic is pre-1.0: until v1.0.0, a minor version may change the API, with what to change listed in the changelog. It pins qilat v0.2.1. Coming from Fiber? See docs/migrating-from-fiber.md.

Installation

Requires Go 1.26 or newer.

go get github.com/erizeta/qinetic

Quick start

package main

import (
	"log"

	"github.com/erizeta/qinetic"
)

func main() {
	app := qinetic.New()
	app.Get("/", func(c *qinetic.Ctx) error {
		return c.SendString("Hello, " + c.Protocol())
	})
	log.Fatal(app.Listen(":3000"))
}

Example

A small REST API.

package main

import (
	"log"
	"strconv"
	"sync"

	"github.com/erizeta/qinetic"
)

type user struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type store struct {
	mu    sync.Mutex
	next  int
	users map[int]user
}

func newApp() *qinetic.App {
	s := &store{users: map[int]user{}}
	app := qinetic.New()

	api := app.Group("/api")
	api.Get("/users/:id", func(c *qinetic.Ctx) error {
		id, err := c.ParamsInt("id")
		if err != nil {
			return qinetic.ErrBadRequest
		}
		s.mu.Lock()
		u, ok := s.users[id]
		s.mu.Unlock()
		if !ok {
			return qinetic.ErrNotFound
		}
		return c.JSON(u)
	})
	api.Post("/users", func(c *qinetic.Ctx) error {
		var u user
		if err := c.Bind().JSON(&u); err != nil {
			return err
		}
		if u.Name == "" {
			return qinetic.NewError(422, "name is required")
		}
		s.mu.Lock()
		s.next++
		u.ID = s.next
		s.users[u.ID] = u
		s.mu.Unlock()
		c.Set("Location", "/api/users/"+strconv.Itoa(u.ID))
		return c.Status(201).JSON(u)
	})
	api.Delete("/users/:id", func(c *qinetic.Ctx) error {
		id, err := c.ParamsInt("id")
		if err != nil {
			return qinetic.ErrBadRequest
		}
		s.mu.Lock()
		delete(s.users, id)
		s.mu.Unlock()
		return c.SendStatus(204)
	})
	return app
}

func main() {
	log.Fatal(newApp().Listen(":3000"))
}

Run it with go run ./examples/rest, or test it without a network through app.Test, as examples/rest/main_test.go does.

Routing
Pattern Matches c.Params
/users /users, and /users/ unless StrictRouting
/users/:id /users/42, not /users or /users/42/x id
/users/:id? /users and /users/42 (last segment only) id, maybe empty
/files/* /files, /files/a/b * (or *1), maybe empty
/files/+ /files/a/b, not /files + (or +1)
/a/*/b/* /a/x/y/b/z; inner wildcards are greedy *1, *2
  • Handlers run in registration order. Use(prefix, ...) matches at segment boundaries (/api matches /api/x, not /apiv2), method routes match the whole path, and c.Next() moves to the next match.
  • Get also registers HEAD. When nothing matches, the response is 404, or 405 with Allow when the path matches routes of other methods.
  • Routing uses the raw path, so %2F stays inside one segment; c.Params returns it decoded. Paths with empty or dot segments, encoded or not, are not routed: only middleware registered on / runs, then 404.
  • Matching is case-insensitive unless Config.CaseSensitive is set. Parameters and wildcards are whole segments; patterns Fiber accepts within a segment, such as /:from-:to, panic at registration.
The Ctx
  • Request: Method, Path, OriginalURL, Host, Hostname, IP, IPs, Secure, Protocol, Params, ParamsInt, Query, QueryInt, QueryBool, Queries, Get, Cookies, Body, BodyReader, FormValue, FormFile, MultipartForm, Bind(), Locals, Route, Context, Request.
  • Response: Status, Set, Append, Vary, Type, Send, SendString, SendStatus, JSON, XML, Redirect, Cookie, ClearCookie, Attachment, Download, SendFile, SendStream, Writer, Flush, Trailer.
  • For middleware: Next, StatusCode, ResponseSize, ResponseHeader, ResponseBody, Committed, HandleError.

The response is buffered and sent with a Content-Length when the handler chain returns, so an ErrorHandler can still replace it. SendStream, Writer, Flush, and SendFile send it as they go.

Middleware

Middleware lives under middleware/, and each package has New(config ...Config) qinetic.Handler with a Next function to skip it:

Package Does
recovery Turns panics into a 500 through the ErrorHandler; qilat.ErrAbortHandler still aborts
logger One slog record per request: method, raw path (no query), status, latency, bytes, protocol, IP, request ID
requestid Keeps a valid incoming X-Request-ID (at most 128 safe characters) or generates one; requestid.FromContext(c)
cors Exact and https://*.example.com origins, preflight, credentials (refuses * with credentials at startup), always Vary: Origin
compress gzip or other codings through qilat.CompressWith, skipping small bodies, images, and Range requests
static Files from a directory or fs.FS, with Range, 304, and index files; listings only with Browse; .. refused
limiter Fixed or sliding window per key (IP by default), X-RateLimit-* headers, 429 with Retry-After
csrf Refuses cross-origin unsafe requests (Sec-Fetch-Site, Origin) and requires a stored token for every one
cache Stores GET responses and serves them to GET and HEAD, one copy per value of each Vary header
package main

import (
	"log"

	"github.com/erizeta/qinetic"
	"github.com/erizeta/qinetic/middleware/compress"
	"github.com/erizeta/qinetic/middleware/cors"
	"github.com/erizeta/qinetic/middleware/logger"
	"github.com/erizeta/qinetic/middleware/recovery"
	"github.com/erizeta/qinetic/middleware/requestid"
	"github.com/erizeta/qinetic/middleware/static"
)

func main() {
	app := qinetic.New()
	// requestid first so the logger sees the ID; logger before recovery so panics are logged as 500s.
	app.Use(requestid.New(), logger.New(), recovery.New(), compress.New())

	api := app.Group("/api", cors.New(cors.Config{
		AllowOrigins:     []string{"https://app.example.com", "https://*.preview.example.com"},
		AllowCredentials: true,
	}))
	api.Get("/hello", func(c *qinetic.Ctx) error {
		return c.JSON(map[string]string{"request_id": requestid.FromContext(c)})
	})

	app.Use("/assets", static.New("./public", static.Config{MaxAge: 3600}))
	log.Fatal(app.Listen(":3000"))
}

Order matters: put requestid before logger, and logger before recovery. compress is qilat middleware run through qinetic.FromQilatMiddleware: the handlers after it, the ErrorHandler, and the buffered response all run inside it, so its output covers error pages too, and middleware registered before it see the response as already sent: status and size are still available from c.StatusCode() and c.ResponseSize(), and a late write returns qinetic.ErrResponseSent instead of corrupting the compressed body. Register middleware that must change the body, such as a cache, after compress. FromStdMiddleware, FromQilat, and FromStd bring in net/http and qilat middleware and handlers the same way.

Compressing a page that holds a secret, such as a CSRF token, together with input the client controls lets an attacker who can watch response sizes guess the secret (BREACH). Skip compress for such pages with its Next option, or keep reflected input out of them.

Rate limiting, CSRF, and storage
package main

import (
	"html/template"
	"log"
	"time"

	"github.com/erizeta/qinetic"
	"github.com/erizeta/qinetic/middleware/csrf"
	"github.com/erizeta/qinetic/middleware/limiter"
)

var page = template.Must(template.New("form").Parse(`<form method="post" action="/comments">
<input type="hidden" name="_csrf" value="{{.}}">
<textarea name="body"></textarea>
<button>Send</button>
</form>`))

func newApp() *qinetic.App {
	app := qinetic.New()
	// 20 requests per minute per IP, counted over a sliding window.
	app.Use(limiter.New(limiter.Config{Max: 20, Expiration: time.Minute, Algorithm: limiter.SlidingWindow}))
	app.Use(csrf.New())

	app.Get("/", func(c *qinetic.Ctx) error {
		c.Type("html")
		return page.Execute(c.Writer(), csrf.TokenFromContext(c))
	})
	app.Post("/comments", func(c *qinetic.Ctx) error {
		// Only reached with the token from the form and a same-origin request.
		return c.SendString("saved: " + c.FormValue("body"))
	})
	return app
}

func main() {
	log.Fatal(newApp().Listen(":3000"))
}

csrf checks every POST, PUT, PATCH, and DELETE twice. First the origin: a browser-marked cross-site request is refused, as is one whose Origin differs from the request's own scheme and host and is not in TrustedOrigins; over HTTPS, a request without Origin needs a matching Referer. Then the token sent in the X-Csrf-Token header or _csrf form field must match the csrf_ cookie and still be stored. The cookie is HttpOnly by default, so pages get the token from csrf.TokenFromContext(c); call csrf.DeleteToken(c) on login and logout. Rejected requests do not use up the rate limit.

limiter and csrf keep state in a qinetic.Storage, in memory by default and per middleware. The memory store starts no goroutine: expired entries go on access, and it holds at most 100,000 entries. To share limits and tokens between instances, pass a Storage backed by Redis or similar; if it also implements qinetic.Incrementer, limits are counted atomically across them.

Caching

cache.New() stores 200 responses to GET for a minute, keyed by host, path, and query, and marks responses X-Cache: hit or miss. Requests with Authorization, Cookie, or Cache-Control: no-store bypass it, since with cookie sessions their responses are often personal; set CacheCookieRequests only with a KeyGenerator that separates users. Nothing is stored for responses with Set-Cookie, Cache-Control private, no-store, or no-cache, or a streamed body.

Register it after compress, as in app.Use(compress.New(), cache.New()). The cache then runs inside compress: it stores the body before compression, and compress compresses each hit for the client's Accept-Encoding. Registered before compress, it would see responses already sent and store nothing. Headers set by middleware registered before the cache, such as cors and requestid, are not stored, since they run again on every hit.

A response with Vary is stored once per combination of the request headers it names, so a response for one Origin (which cors always varies on) or Accept-Language is never served to another. A header that is absent differs from one that is empty. Vary: * is never stored, and at most MaxVariants copies (16) are kept per key, so random header values cannot fill the storage. The default storage is in memory and holds at most 64 MiB; with an external Storage, entries are versioned, and an unreadable one is a miss. If the storage fails, requests are served uncached and the error is logged.

Strings and lifetimes
  • Params, Query, Get, Cookies, and FormValue return copies, safe to keep after the handler returns. Unlike Fiber, there is no unsafe string mode.
  • PeekParam, PeekQuery, and PeekHeader return []byte without copying; they are valid only until the handler chain returns, like everything from qilat's Request.
  • c.Body() returns ([]byte, error): the body is streamed, so reading can fail. Past Config.BodyLimit (4 MiB by default) it fails with ErrRequestEntityTooLarge.
  • c.Context() is canceled when the client disconnects and may be kept; c itself may not.
Behind a proxy

Set Config.TrustedProxies to your proxies' addresses. Only requests from them have their X-Forwarded-Proto and X-Forwarded-Host believed by c.Secure() and c.Host(), and their Config.ProxyHeader (for example X-Forwarded-For) by c.IP(). c.IP() returns the rightmost address that is not a trusted proxy and c.Host() the last forwarded host, so a client cannot choose either by sending the headers itself. Fiber takes the first forwarded host instead.

HTTP/3 and graceful shutdown

ListenTLS serves HTTP/1.1 and HTTP/2 over TCP and HTTP/3 over UDP on the same port, advertising HTTP/3 to TCP clients with Alt-Svc. ListenTLSWithCertificate takes a certificate in memory, such as a development one:

package main

import (
	"context"
	"errors"
	"log"
	"os"
	"os/signal"
	"time"

	"github.com/erizeta/qilat"
	"github.com/erizeta/qinetic"
)

func main() {
	cert, err := qilat.GenerateDevCert()
	if err != nil {
		log.Fatal(err)
	}
	app := qinetic.New()
	app.Get("/", func(c *qinetic.Ctx) error {
		return c.SendString("Hello over " + c.Protocol() + "\n")
	})
	app.OnShutdown(func(_ context.Context) error {
		log.Println("server stopped")
		return nil
	})

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()
	go func() {
		<-ctx.Done()
		if err := app.ShutdownWithTimeout(10 * time.Second); err != nil {
			log.Println(err)
		}
	}()
	// HTTP/1.1 and HTTP/2 on TCP, HTTP/3 on UDP, all on port 8443. After Shutdown, Listen returns once
	// in-flight requests and OnShutdown hooks are done.
	if err := app.ListenTLSWithCertificate(":8443", cert); !errors.Is(err, qilat.ErrServerClosed) {
		log.Fatal(err)
	}
}

Try it with curl -k --http1.1 https://localhost:8443/, curl -k https://localhost:8443/ (HTTP/2), and, with a curl built with HTTP/3, curl -k --http3-only https://localhost:8443/.

After Shutdown, Listen returns qilat.ErrServerClosed only once in-flight requests have finished and the OnShutdown hooks have run, so returning from main right after it cuts nothing short.

Other ways to serve: Listen (HTTP/1.1, plus cleartext HTTP/2 with Config.H2C), Listener(net.Listener), ListenerTLS(net.Listener) and ListenQUIC(net.PacketConn) with certificates from Config.TLSConfig, or app.Handler() with your own qilat.Server. Listen calls running at the same time share one server, so ListenerTLS and ListenQUIC together serve HTTP/1.1, HTTP/2, and HTTP/3 and stop with one Shutdown.

Server limits

The Listen methods build a qilat.Server from Config. Zero values keep qilat's defaults, so slow clients are cut off by the header and stall timeouts even though a request or response as a whole has no time limit:

Setting Default Set with
Request header 10 s (qilat.DefaultReadHeaderTimeout) Config.Server
Request body or response stalled 60 s without progress (qilat.DefaultStallTimeout) Config.Server
Keep-alive idle 120 s (qilat.DefaultIdleTimeout) Config.IdleTimeout
Whole request / whole response no limit Config.ReadTimeout / Config.WriteTimeout
Request body size 4 MiB Config.BodyLimit
Header size 64 KiB (qilat.DefaultMaxHeaderBytes) Config.Server

Config.Server receives the qilat.Server before it starts, for anything else: TLS settings, connection limits, HTTP/2 and HTTP/3 tuning.

Errors

A handler error goes to Config.ErrorHandler. The default sends an *Error's code and message (ErrNotFound, NewError(422, "name is required")) and turns any other error into a plain 500, logging the details instead of sending them. Bind errors are 400s that never include decoder output.

Performance

benchmarks/ compares plain qilat (its ServeMux and qilat.Chain), qinetic, and Fiber v3.6.1 on the same scenarios and the same keep-alive HTTP/1.1 client, with 8 connections per GOMAXPROCS. In memory, the listener skips the kernel, so the numbers show server CPU per request; over loopback, the socket path dominates. Median of 5 runs on an Apple M5 (10 cores), Go 1.27.3, in ns/op:

Scenario In memory: qilat qinetic Fiber Loopback: qilat qinetic Fiber
Plaintext 523 510 360 5779 5881 6572
JSON 479 565 408 5878 5876 6355
Param route 441 520 366 5901 5895 6432
Last of 100 routes 457 519 375 5908 5854 6538
5 middleware 437 533 373 5906 5891 6551

fasthttp spends less CPU per request than qilat, so Fiber leads in memory; over a real socket, in this setup, qilat and qinetic are ahead and qinetic costs no measurable time over qilat. Every framework allocates about 10 times per request here, nearly all of it in the benchmark client. On its own side, a static, parameter, or wildcard hello route allocates nothing beyond qilat (TestHelloAllocs), and route matching allocates nothing (BenchmarkRouterMatch).

Reproduce with:

cd benchmarks && go test -run '^$' -bench BenchmarkCompare -benchmem -count 5 .

make bench runs these and the router and baseline benchmarks.

Differences from Fiber

Fiber qinetic
fasthttp, HTTP/1.1 only qilat: HTTP/1.1, HTTP/2, and HTTP/3 from one App
c.Context() is not canceled when the client disconnects It is, so work stops when nobody waits for it
Strings point into reused buffers unless Immutable Params, Query, Get, and the rest return copies; Peek* variants return []byte
c.Body() returns []byte c.Body() returns ([]byte, error), since the body is streamed
A non-*Error error sends err.Error() to the client It sends a generic 500 and logs the details
fiber.Ctx is an interface *qinetic.Ctx is a struct
Paths are normalized, and %2F may split segments Routing uses the raw path; unclean paths get 404

docs/migrating-from-fiber.md covers routing syntax, middleware, serving, and the other changes in detail.

Contributing

See CONTRIBUTING.md. Report security issues through SECURITY.md.

License

MIT

Documentation

Overview

Package qinetic is a Fiber-style web framework built on the qilat HTTP engine, serving HTTP/1.1, HTTP/2, and HTTP/3 from one App.

Index

Constants

View Source
const (
	CookieSameSiteLax    = "Lax"
	CookieSameSiteStrict = "Strict"
	CookieSameSiteNone   = "None"
)

SameSite values for Cookie.SameSite.

View Source
const DefaultBodyLimit = 4 << 20

DefaultBodyLimit is the request body size c.Body reads when Config.BodyLimit is zero.

Variables

View Source
var (
	ErrBadRequest            = NewError(http.StatusBadRequest)
	ErrUnauthorized          = NewError(http.StatusUnauthorized)
	ErrForbidden             = NewError(http.StatusForbidden)
	ErrNotFound              = NewError(http.StatusNotFound)
	ErrMethodNotAllowed      = NewError(http.StatusMethodNotAllowed)
	ErrNotAcceptable         = NewError(http.StatusNotAcceptable)
	ErrRequestTimeout        = NewError(http.StatusRequestTimeout)
	ErrConflict              = NewError(http.StatusConflict)
	ErrRequestEntityTooLarge = NewError(http.StatusRequestEntityTooLarge)
	ErrUnsupportedMediaType  = NewError(http.StatusUnsupportedMediaType)
	ErrUnprocessableEntity   = NewError(http.StatusUnprocessableEntity)
	ErrTooManyRequests       = NewError(http.StatusTooManyRequests)
	ErrInternalServerError   = NewError(http.StatusInternalServerError)
	ErrNotImplemented        = NewError(http.StatusNotImplemented)
	ErrServiceUnavailable    = NewError(http.StatusServiceUnavailable)
)

Errors for common client and server failures, sent as is by DefaultErrorHandler.

View Source
var ErrCertificateAfterStart = errors.New("qinetic: ListenTLSWithCertificate called while the app is already serving")

ErrCertificateAfterStart is returned by ListenTLSWithCertificate when another Listen call already started the App's server, whose TLS configuration can no longer change. Use Config.TLSConfig for both instead.

View Source
var ErrResponseSent = errors.New("qinetic: response already sent by a qilat handler")

ErrResponseSent is returned when writing a body after a qilat handler or middleware, run through FromQilat or FromQilatMiddleware, has finished the response: the bytes would land after a body framed or encoded by that handler, corrupting it, so they are not sent.

Functions

func DefaultErrorHandler

func DefaultErrorHandler(c *Ctx, err error) error

DefaultErrorHandler sends an *Error's code and message. Any other error becomes a 500 with a generic message, so internal details never reach the client; the error itself goes to Config.Logger.

Types

type App

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

App is a qinetic application. It is a qilat.Handler; routes must be registered before it serves its first request, after which registering panics.

func New

func New(cfg ...Config) *App

New returns an App configured by cfg, if given.

func (*App) Add

func (app *App) Add(methods []string, path string, handler Handler, handlers ...Handler) Router

Add registers a route for each of methods; see Group.Add.

func (*App) All

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

All registers a route for every standard method.

func (*App) Connect

func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router

Connect registers a CONNECT route.

func (*App) Delete

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

Delete registers a DELETE route.

func (*App) Get

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

Get registers a GET route, and the same handlers for HEAD.

func (*App) Group

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

Group returns a Router for routes under prefix; see Group.Group.

func (*App) Handler

func (app *App) Handler() qilat.Handler

Handler returns app as a qilat.Handler, for a custom qilat.Server or qilat middleware.

func (*App) Head

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

Head registers a HEAD route.

func (*App) Listen

func (app *App) Listen(addr string) error

Listen serves HTTP/1.1 on the TCP address addr, and cleartext HTTP/2 when Config.H2C is set. It blocks until the server stops; after Shutdown it returns qilat.ErrServerClosed once Shutdown has finished.

The Listen methods may run concurrently on one App, for example Listener for TCP and ListenQUIC for UDP: they share one qilat.Server and one Shutdown.

func (*App) ListenQUIC

func (app *App) ListenQUIC(pc net.PacketConn) error

ListenQUIC serves HTTP/3 on pc with the certificates of Config.TLSConfig. Responses from a ListenerTLS on the same port advertise it with Alt-Svc.

func (*App) ListenTLS

func (app *App) ListenTLS(addr, certFile, keyFile string) error

ListenTLS serves HTTPS on addr: HTTP/2 and HTTP/1.1 over TCP and, unless Config.DisableHTTP3 is set, HTTP/3 over UDP on the same port, advertised to TCP clients with Alt-Svc. The certificate files may be empty when Config.TLSConfig holds certificates.

func (*App) ListenTLSWithCertificate

func (app *App) ListenTLSWithCertificate(addr string, cert tls.Certificate) error

ListenTLSWithCertificate is like ListenTLS with a certificate already loaded, such as one from qilat.GenerateDevCert, added to Config.TLSConfig. It must start the App's server: while another Listen call is running it returns ErrCertificateAfterStart.

func (*App) Listener

func (app *App) Listener(ln net.Listener) error

Listener serves HTTP/1.1, and cleartext HTTP/2 with Config.H2C, on ln. A listener from tls.NewListener offers HTTP/2 only if its config lists "h2" in NextProtos; ListenerTLS does that for you.

func (*App) ListenerTLS

func (app *App) ListenerTLS(ln net.Listener) error

ListenerTLS serves HTTPS, HTTP/2 and HTTP/1.1, on ln with the certificates of Config.TLSConfig. Run ListenQUIC alongside it on a UDP socket to add HTTP/3.

func (*App) OnShutdown

func (app *App) OnShutdown(fn func(ctx context.Context) error)

OnShutdown registers fn to run, in registration order, after Shutdown has stopped the server. It receives Shutdown's context.

func (*App) Options

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

Options registers an OPTIONS route.

func (*App) Patch

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

Patch registers a PATCH route.

func (*App) Post

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

Post registers a POST route.

func (*App) Put

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

Put registers a PUT route.

func (*App) Route

func (app *App) Route(prefix string, fn func(Router)) Router

Route calls fn with a Router for routes under prefix; see Group.Route.

func (*App) ServeHTTP

func (app *App) ServeHTTP(w qilat.ResponseWriter, r *qilat.Request)

ServeHTTP runs the handler chain for r. A panic propagates to qilat and its Ctx is not reused.

func (*App) Shutdown

func (app *App) Shutdown(ctx context.Context) error

Shutdown stops accepting connections, waits for in-flight requests until ctx is done, then runs the OnShutdown hooks. Running Listen calls return qilat.ErrServerClosed once Shutdown has finished, and later ones at once. The hooks run once; the errors of the server and every hook are joined.

func (*App) ShutdownWithTimeout

func (app *App) ShutdownWithTimeout(d time.Duration) error

ShutdownWithTimeout is Shutdown with a context that expires after d.

func (*App) Test

func (app *App) Test(req *http.Request) (*http.Response, error)

Test serves req in memory and returns the response, without a network or a qilat.Server. Server settings such as its body size limit and DebugPoison therefore do not apply; Config.DebugPoison does. A handler panic propagates to the caller.

func (*App) Trace

func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router

Trace registers a TRACE route.

func (*App) Use

func (app *App) Use(args ...any) Router

Use registers middleware; see Group.Use.

type Bind

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

Bind decodes request data into a struct, returned by c.Bind. Malformed input fails with a 400 *Error whose message names at most the offending field; the decoder's own error is kept only for logs.

func (*Bind) Body

func (b *Bind) Body(v any) error

Body decodes the body according to its Content-Type: JSON, XML, or a form. Other types fail with ErrUnsupportedMediaType.

func (*Bind) Form

func (b *Bind) Form(v any) error

Form decodes an application/x-www-form-urlencoded or multipart/form-data body into the fields of the struct v points to, named by `form` tags or else by field name, matched case-insensitively as in Fiber. Supported field types are string, bool, integers, floats, and slices of them.

func (*Bind) JSON

func (b *Bind) JSON(v any) error

JSON decodes the body as JSON with Config.JSONDecoder.

func (*Bind) Query

func (b *Bind) Query(v any) error

Query decodes the query string into the struct v points to, like Form but with `query` tags.

func (*Bind) XML

func (b *Bind) XML(v any) error

XML decodes the body as XML.

type Config

type Config struct {
	// ErrorHandler handles errors returned by handlers. If nil, DefaultErrorHandler is used.
	ErrorHandler ErrorHandler

	// Logger receives errors that are not sent to the client. If nil, slog.Default is used.
	Logger *slog.Logger

	// CaseSensitive makes "/Users" and "/users" different routes.
	CaseSensitive bool

	// StrictRouting makes "/users" and "/users/" different routes.
	StrictRouting bool

	// DebugPoison makes any use of a Ctx after its handler returned panic, instead of reusing it, and sets
	// qilat.Server.DebugPoison for the Listen methods. It helps find handlers that keep c in a goroutine;
	// it disables pooling.
	DebugPoison bool

	// BodyLimit is the largest request body, in bytes, that c.Body and form parsing read; larger bodies
	// fail with ErrRequestEntityTooLarge. Zero means DefaultBodyLimit and a negative value means no limit.
	BodyLimit int64

	// ProxyHeader names the header holding the client IP set by a reverse proxy, such as X-Forwarded-For.
	// It is used by c.IP only when the connection comes from one of TrustedProxies.
	ProxyHeader string

	// TrustedProxies lists the IPs or CIDR prefixes of reverse proxies whose ProxyHeader and
	// X-Forwarded-Proto are believed. New panics on an invalid entry.
	TrustedProxies []string

	// JSONEncoder and JSONDecoder replace encoding/json for c.JSON and c.Bind().JSON.
	JSONEncoder func(v any) ([]byte, error)
	JSONDecoder func(data []byte, v any) error

	// ReadTimeout and WriteTimeout bound reading a whole request and writing a response. Zero means no
	// limit, as in qilat; slow clients are still cut off by qilat's header and stall timeouts.
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
	// IdleTimeout bounds the wait for the next keep-alive request. Zero means qilat.DefaultIdleTimeout.
	IdleTimeout time.Duration
	// TLSConfig holds the certificates and settings for ListenTLS, ListenerTLS, and ListenQUIC. It is cloned,
	// never modified.
	TLSConfig *tls.Config
	// H2C makes Listen and Listener serve cleartext HTTP/2 to clients with prior knowledge.
	H2C bool
	// DisableHTTP3 makes ListenTLS serve TCP only.
	DisableHTTP3 bool
	// Server, if set, adjusts the qilat.Server built by the Listen methods before it starts, for options
	// Config does not cover, such as TLSConfig, MaxConcurrentConns, or HTTP2 and HTTP3 settings.
	Server func(srv *qilat.Server)
}

Config configures an App. The zero value is ready to use.

type Cookie struct {
	Name        string
	Value       string
	Path        string
	Domain      string
	MaxAge      int // seconds; zero omits Max-Age, negative deletes the cookie now
	Expires     time.Time
	Secure      bool
	HTTPOnly    bool
	SameSite    string // CookieSameSiteLax, CookieSameSiteStrict, CookieSameSiteNone, or "" to omit
	Partitioned bool
}

Cookie is a response cookie, serialized with net/http.

type Ctx

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

Ctx carries one request through its handlers. Like the qilat request it wraps, a Ctx and every []byte returned from it are valid only until the handler chain returns; strings are copies and c.Context() may be kept.

The response body is buffered and written once the chain and any ErrorHandler are done, so an ErrorHandler can still replace it. Headers go straight to the qilat ResponseWriter.

func (*Ctx) Append

func (c *Ctx) Append(field string, values ...string)

Append adds values to the comma-separated response header field, skipping values already listed.

func (*Ctx) Attachment

func (c *Ctx) Attachment(filename ...string)

Attachment sets Content-Disposition to attachment and, given a file name, sets Content-Type from its extension and offers its base name to the client.

func (*Ctx) Bind

func (c *Ctx) Bind() *Bind

Bind returns the binder for the request.

func (*Ctx) Body

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

Body reads the whole request body, once; later calls return the same bytes. It fails with ErrRequestEntityTooLarge past Config.BodyLimit and with a 400 *Error when reading fails. The result is valid only until the handler chain returns.

func (*Ctx) BodyReader

func (c *Ctx) BodyReader() io.Reader

BodyReader returns the request body as a stream, limited like Body. After Body it reads the bytes Body returned. Do not mix it with Body otherwise.

func (*Ctx) ClearCookie

func (c *Ctx) ClearCookie(names ...string)

ClearCookie expires the named cookies in the client, or every cookie of the request when no name is given. Cookies set with a Path or Domain are cleared only by a Cookie call with the same Path and Domain.

func (*Ctx) Committed

func (c *Ctx) Committed() bool

Committed reports whether the response has been sent, at least in part, so it can no longer be replaced.

func (*Ctx) Context

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

Context returns the request context. It is canceled when the client disconnects or the request finishes, and unlike the Ctx it may be kept after the handler returns.

func (*Ctx) Cookie

func (c *Ctx) Cookie(ck *Cookie)

Cookie adds a Set-Cookie header. A cookie net/http considers invalid, such as one whose name or value has characters that could inject attributes, is not sent; the reason goes to Config.Logger.

func (*Ctx) Cookies

func (c *Ctx) Cookies(key string, def ...string) string

Cookies returns the value of the request cookie key, or def[0] (or "") when it is absent or empty.

func (*Ctx) Download

func (c *Ctx) Download(file string, filename ...string) error

Download sends file as an attachment named filename, or file's base name.

func (*Ctx) Flush

func (c *Ctx) Flush() error

Flush commits the response and sends what has been written so far to the client.

func (*Ctx) FormFile

func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error)

FormFile returns the first file uploaded in the multipart form field key.

func (*Ctx) FormValue

func (c *Ctx) FormValue(key string, def ...string) string

FormValue returns a copy of the first value of the form field key from an application/x-www-form-urlencoded or multipart/form-data body, or def[0] (or "") when it is absent, empty, or the body cannot be read. The query string is not consulted; use Query for it.

func (*Ctx) Get

func (c *Ctx) Get(key string, def ...string) string

Get returns a copy of the request header key, or def[0] (or "") when it is absent or empty.

func (*Ctx) GetRespHeader

func (c *Ctx) GetRespHeader(key string, def ...string) string

GetRespHeader returns a copy of the response header key, or def[0] (or "") when it is absent or empty.

func (*Ctx) HandleError

func (c *Ctx) HandleError(err error)

HandleError runs the App's error handling for err now, as it would after the chain: the body is discarded and Config.ErrorHandler writes the response. Middleware such as a logger use it to see the final status; the handler should then return nil.

func (*Ctx) Host

func (c *Ctx) Host() string

Host returns the host the client asked for, including any port: the Host header (:authority on HTTP/2 and HTTP/3), or, when the connection comes from one of Config.TrustedProxies, the last X-Forwarded-Host value, which the nearest proxy set. Earlier values could have come from the client.

func (*Ctx) Hostname

func (c *Ctx) Hostname() string

Hostname returns Host without its port, and without brackets for an IPv6 address.

func (*Ctx) IP

func (c *Ctx) IP() string

IP returns the client IP. It is the connection's remote address unless that address is one of Config.TrustedProxies: then it is the rightmost address in Config.ProxyHeader that is not a trusted proxy, so a client cannot spoof it by sending the header itself.

func (*Ctx) IPs

func (c *Ctx) IPs() []string

IPs returns the comma-separated entries of the proxy header (Config.ProxyHeader, or X-Forwarded-For), as sent. They come from the client or proxies and must not be trusted.

func (*Ctx) JSON

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

JSON encodes v with Config.JSONEncoder (encoding/json by default) as the body, with Content-Type application/json.

func (*Ctx) Locals

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

Locals stores value under key for later handlers of the same request, or returns the stored value when value is omitted.

func (*Ctx) Method

func (c *Ctx) Method() string

Method returns the request method.

func (*Ctx) MultipartForm

func (c *Ctx) MultipartForm() (*multipart.Form, error)

MultipartForm parses a multipart/form-data body. It is read in memory, bounded by Config.BodyLimit.

func (*Ctx) Next

func (c *Ctx) Next() error

Next runs the next handler of the current route, or else the first handler of the next route that matches. When no route is left it returns ErrNotFound, or ErrMethodNotAllowed with an Allow header when the path matches routes of other methods.

func (*Ctx) OriginalURL

func (c *Ctx) OriginalURL() string

OriginalURL returns a copy of the request-target as received, with its query.

func (*Ctx) Params

func (c *Ctx) Params(key string, def ...string) string

Params returns a decoded copy of the route parameter key, or def[0] (or "") when it is absent or empty. Wildcards are named "*" (or "*1"), "*2", "+", and so on.

func (*Ctx) ParamsInt

func (c *Ctx) ParamsInt(key string) (int, error)

ParamsInt parses the route parameter key as an int.

func (*Ctx) Path

func (c *Ctx) Path() string

Path returns a copy of the percent-decoded request path. Routing uses the raw path, where an encoded slash (%2F) does not separate segments; here it is decoded to '/'.

func (*Ctx) PeekHeader

func (c *Ctx) PeekHeader(key string) []byte

PeekHeader is like Get but returns the value without copying it, or nil when it is absent. The result is valid only until the handler chain returns.

func (*Ctx) PeekParam

func (c *Ctx) PeekParam(key string) []byte

PeekParam is like Params but returns the decoded value without copying it, or nil when it is absent. The result is valid only until the handler chain returns.

func (*Ctx) PeekQuery

func (c *Ctx) PeekQuery(key string) []byte

PeekQuery is like Query but returns the value without copying it, or nil when it is absent. The result is valid only until the handler chain returns.

func (*Ctx) Protocol

func (c *Ctx) Protocol() string

Protocol returns the request protocol: "HTTP/1.0", "HTTP/1.1", "HTTP/2.0", or "HTTP/3.0".

func (*Ctx) Queries

func (c *Ctx) Queries() map[string]string

Queries returns copies of all query arguments, keeping the first value of repeated keys.

func (*Ctx) Query

func (c *Ctx) Query(key string, def ...string) string

Query returns a copy of the first query argument key, or def[0] (or "") when it is absent or empty.

func (*Ctx) QueryBool

func (c *Ctx) QueryBool(key string, def ...bool) bool

QueryBool parses the query argument key with strconv.ParseBool, returning def[0] (or false) when it is absent or invalid.

func (*Ctx) QueryInt

func (c *Ctx) QueryInt(key string, def ...int) int

QueryInt parses the query argument key as an int, returning def[0] (or 0) when it is absent or invalid.

func (*Ctx) Redirect

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

Redirect responds with a redirect to location, 303 See Other unless code is given. A location with control characters, such as CR or LF from an unchecked request value, is refused with an error.

func (*Ctx) Request

func (c *Ctx) Request() *qilat.Request

Request returns the underlying qilat request, valid until the handler chain returns.

func (*Ctx) ResponseBody

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

ResponseBody returns the buffered response body, or nil once the response is committed, when it has been sent. It is valid only until the body changes or the handler chain returns.

func (*Ctx) ResponseHeader

func (c *Ctx) ResponseHeader() *qilat.Header

ResponseHeader returns the response header, for middleware that inspect or copy it. It is valid only until the handler chain returns.

func (*Ctx) ResponseSize

func (c *Ctx) ResponseSize() int64

ResponseSize returns the size of the response body: what is buffered, plus what has already been sent once the response is committed. Bytes compressed by middleware count before compression.

func (*Ctx) Route

func (c *Ctx) Route() Route

Route returns a copy of the route being run, or the zero Route before any route matched. Changing it does not change the route; its Params slice is shared and must not be modified.

func (*Ctx) Secure

func (c *Ctx) Secure() bool

Secure reports whether the request came over TLS, directly or, from a trusted proxy, as declared by X-Forwarded-Proto: https.

func (*Ctx) Send

func (c *Ctx) Send(body []byte) error

Send replaces the response body with body. It sets no Content-Type.

func (*Ctx) SendFile

func (c *Ctx) SendFile(file string) error

SendFile sends the named file with qilat.ServeFile, which handles Range and conditional requests and sets Content-Type. A name with a ".." element, as a decoded parameter like "a%2F..%2Fb" can produce, fails with ErrBadRequest; a missing file or directory with ErrNotFound. On success the response is committed and the file server picks the status, ignoring c.Status.

func (*Ctx) SendStatus

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

SendStatus sets the status code and, if the body is empty, sends the status text.

func (*Ctx) SendStream

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

SendStream commits the response and copies r to it, closing r if it is an io.Closer. With size, it is sent as the Content-Length. The body set before is discarded.

func (*Ctx) SendString

func (c *Ctx) SendString(s string) error

SendString replaces the response body with s, with Content-Type text/plain; charset=utf-8 unless one is set.

func (*Ctx) Set

func (c *Ctx) Set(key, val string)

Set sets the response header key to val. qilat replaces CR, LF, and NUL in val, so a value taken from the request cannot inject header fields, and panics on an invalid key.

func (*Ctx) Status

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

Status sets the response status code. It has no effect once the response is committed.

func (*Ctx) StatusCode

func (c *Ctx) StatusCode() int

StatusCode returns the response status: the one set so far, or 200.

func (*Ctx) Trailer

func (c *Ctx) Trailer() *qilat.Header

Trailer returns the header whose fields are sent as trailers after the body. Using it drops the Content-Length the response would otherwise get, since HTTP/1.1 needs a chunked body for trailers.

func (*Ctx) Type

func (c *Ctx) Type(ext string, charset ...string) *Ctx

Type sets Content-Type from a file extension such as "json" or ".html", or from a full media type. A charset, if given, replaces the type's own. Unknown extensions give application/octet-stream.

func (*Ctx) Vary

func (c *Ctx) Vary(fields ...string)

Vary adds fields to the Vary response header.

func (*Ctx) Writer

func (c *Ctx) Writer() io.Writer

Writer commits the response and returns a writer for the rest of the body, sent as it is written. The response can no longer be replaced by an ErrorHandler.

func (*Ctx) XML

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

XML encodes v with encoding/xml as the body, with Content-Type application/xml.

type Error

type Error struct {
	Code    int
	Message string
	// contains filtered or unexported fields
}

Error is an error with an HTTP status code. DefaultErrorHandler sends its Code and Message to the client.

func NewError

func NewError(code int, message ...string) *Error

NewError returns an Error with the given code and message, or the code's status text when message is empty.

func (*Error) Error

func (e *Error) Error() string

Error returns the message sent to the client.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the error that caused e, if any.

type ErrorHandler

type ErrorHandler = func(c *Ctx, err error) error

ErrorHandler handles an error returned by a handler. It may write a new response; the body written before the error has already been discarded.

type Group

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

Group is a set of routes under a common prefix, created by Router.Group.

func (*Group) Add

func (g *Group) Add(methods []string, path string, handler Handler, handlers ...Handler) Router

Add registers a route for each of methods. A route matches the whole path; its handlers run in order when each calls c.Next.

func (*Group) All

func (g *Group) All(path string, handler Handler, handlers ...Handler) Router

All registers a route for every standard method.

func (*Group) Connect

func (g *Group) Connect(path string, handler Handler, handlers ...Handler) Router

Connect registers a CONNECT route.

func (*Group) Delete

func (g *Group) Delete(path string, handler Handler, handlers ...Handler) Router

Delete registers a DELETE route.

func (*Group) Get

func (g *Group) Get(path string, handler Handler, handlers ...Handler) Router

Get registers a GET route, and the same handlers for HEAD.

func (*Group) Group

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

Group returns a Router for routes under prefix. handlers, if any, run as middleware for that prefix.

func (*Group) Head

func (g *Group) Head(path string, handler Handler, handlers ...Handler) Router

Head registers a HEAD route.

func (*Group) Options

func (g *Group) Options(path string, handler Handler, handlers ...Handler) Router

Options registers an OPTIONS route.

func (*Group) Patch

func (g *Group) Patch(path string, handler Handler, handlers ...Handler) Router

Patch registers a PATCH route.

func (*Group) Post

func (g *Group) Post(path string, handler Handler, handlers ...Handler) Router

Post registers a POST route.

func (*Group) Put

func (g *Group) Put(path string, handler Handler, handlers ...Handler) Router

Put registers a PUT route.

func (*Group) Route

func (g *Group) Route(prefix string, fn func(Router)) Router

Route calls fn with a Router for routes under prefix and returns that Router.

func (*Group) Trace

func (g *Group) Trace(path string, handler Handler, handlers ...Handler) Router

Trace registers a TRACE route.

func (*Group) Use

func (g *Group) Use(args ...any) Router

Use registers middleware that runs for every method on paths under a prefix, "/" by default. Arguments are an optional prefix (string or []string) followed by one or more Handlers. Without strict routing, Use("/api") matches /api and /api/x but not /apiv2.

type Handler

type Handler = func(c *Ctx) error

Handler handles a request. A returned error goes to the App's ErrorHandler.

func FromQilat

func FromQilat(h qilat.Handler) Handler

FromQilat runs a qilat handler, such as qilat.FileServer, as the response. The handler writes to the client directly, so the response is committed and an ErrorHandler cannot replace it; its status and size are still recorded for c.StatusCode and c.ResponseSize.

func FromQilatMiddleware

func FromQilatMiddleware(mw func(qilat.Handler) qilat.Handler) Handler

FromQilatMiddleware runs qilat middleware, such as qilat.CompressWith, around the rest of the chain. Inside it the Ctx writes to the middleware's writer and reads the request the middleware passes on, which may differ, as with qilat.StripPrefix. Since qilat finishes that writer when its handler returns, the rest of the chain, any ErrorHandler, and the buffered response all complete inside the middleware: the response is then committed, and handlers registered before the adapter see it as sent. When the middleware answers without calling next, the rest of the chain does not run.

func FromStd

func FromStd(h http.Handler) Handler

FromStd runs a net/http handler as the response, through qilat.FromStd. See FromQilat.

func FromStdMiddleware

func FromStdMiddleware(mw func(http.Handler) http.Handler) Handler

FromStdMiddleware runs net/http middleware around the rest of the chain, through qilat.FromStdMiddleware. See FromQilatMiddleware.

type Incrementer

type Incrementer interface {
	// Incr adds delta, which may be negative, to the integer at key and returns the new value and when the
	// key expires. A missing or expired key starts at zero and gets ttl; an existing key keeps its expiry.
	// Get on the key returns the value in decimal.
	Incr(ctx context.Context, key string, delta int64, ttl time.Duration) (int64, time.Time, error)
}

Incrementer is implemented by a Storage with atomic counters, such as Redis INCRBY. Middleware use it when available, so counts are exact even across instances sharing the storage.

type Route

type Route struct {
	// Method is the request method, or "USE" for middleware.
	Method string
	// Path is the pattern as registered, including group prefixes.
	Path string
	// Params are the parameter names in order; wildcards are named "*1", "+1", and so on.
	Params []string
}

Route describes a registered route, as returned by c.Route.

type Router

type Router interface {
	Use(args ...any) Router
	Get(path string, handler Handler, handlers ...Handler) Router
	Head(path string, handler Handler, handlers ...Handler) Router
	Post(path string, handler Handler, handlers ...Handler) Router
	Put(path string, handler Handler, handlers ...Handler) Router
	Patch(path string, handler Handler, handlers ...Handler) Router
	Delete(path string, handler Handler, handlers ...Handler) Router
	Options(path string, handler Handler, handlers ...Handler) Router
	Connect(path string, handler Handler, handlers ...Handler) Router
	Trace(path string, handler Handler, handlers ...Handler) Router
	All(path string, handler Handler, handlers ...Handler) Router
	Add(methods []string, path string, handler Handler, handlers ...Handler) Router
	Group(prefix string, handlers ...Handler) Router
	Route(prefix string, fn func(Router)) Router
}

Router registers routes. *App and *Group implement it.

type Storage

type Storage interface {
	// Get returns the value of key, or nil and no error when the key is missing or expired.
	Get(ctx context.Context, key string) ([]byte, error)
	// Set stores val under key. A ttl of zero or less means the entry does not expire.
	Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
	// Delete removes key. Deleting a missing key is not an error.
	Delete(ctx context.Context, key string) error
	// Close releases the storage's resources.
	Close() error
}

Storage is a key-value store with expiry, used by middleware such as limiter, csrf, and cache. Implementations must be safe for concurrent use. The default, used when a middleware is given none, keeps entries in the process's memory, so state is not shared between instances; plug in an external store, such as Redis, to share it.

Directories

Path Synopsis
internal
leaktest
Package leaktest checks that tests leave no goroutines behind, without a dependency outside qilat's.
Package leaktest checks that tests leave no goroutines behind, without a dependency outside qilat's.
memory
Package memory is the in-process qinetic.Storage used by middleware when none is configured.
Package memory is the in-process qinetic.Storage used by middleware when none is configured.
middleware
cache
Package cache stores responses and serves them again, keeping a separate copy per value of each header the response varies on, so a response for one Origin or language is never served to another.
Package cache stores responses and serves them again, keeping a separate copy per value of each header the response varies on, so a response for one Origin or language is never served to another.
compress
Package compress compresses response bodies with qilat.CompressWith.
Package compress compresses response bodies with qilat.CompressWith.
cors
Package cors implements Cross-Origin Resource Sharing: it answers preflight requests and adds Access-Control-* headers for allowed origins.
Package cors implements Cross-Origin Resource Sharing: it answers preflight requests and adds Access-Control-* headers for allowed origins.
csrf
Package csrf protects state-changing requests from cross-site request forgery with two checks: the request's origin, from Sec-Fetch-Site or Origin, and a token stored server side that the client must send back in both a cookie and a header or form field.
Package csrf protects state-changing requests from cross-site request forgery with two checks: the request's origin, from Sec-Fetch-Site or Origin, and a token stored server side that the client must send back in both a cookie and a header or form field.
limiter
Package limiter limits how many requests a client makes per time window.
Package limiter limits how many requests a client makes per time window.
logger
Package logger writes one slog record per request.
Package logger writes one slog record per request.
recovery
Package recovery turns panics in later handlers into a 500 handled by the App's ErrorHandler.
Package recovery turns panics in later handlers into a 500 handled by the App's ErrorHandler.
requestid
Package requestid gives each request an ID, kept from the client when valid, and returns it in a header.
Package requestid gives each request an ID, kept from the client when valid, and returns it in a header.
static
Package static serves files from a directory or an fs.FS.
Package static serves files from a directory or an fs.FS.

Jump to

Keyboard shortcuts

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