qilat

package module
v0.2.1 Latest Latest
Warning

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

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

README

qilat

Test Go Reference Go Report Card

High-performance HTTP library for Go with net/http-style APIs and native HTTP/1.1, HTTP/2, and HTTP/3 support, for servers and clients.

[!NOTE] qilat is pre-1.0. The API may still change between minor versions; every change is listed in the changelog. See API stability.

Why qilat?

fasthttp is fast thanks to zero-allocation parsing and aggressive pooling, but it only speaks HTTP/1.1, cancels requests only on shutdown, and uses its own API that does not fit the net/http ecosystem. net/http supports HTTP/2 and has a familiar API, but allocates more per request and has no HTTP/3.

qilat takes net/http's API shape and fasthttp's performance model, and adds HTTP/3:

net/http fasthttp qilat
HTTP/1.1 ✅ ✅ ✅
HTTP/2 ✅ ❌ ✅ native
HTTP/3 (QUIC) ❌ ❌ ✅ native
net/http-style Handler / ResponseWriter ✅ ❌ ✅
ServeMux with Go 1.22 patterns (GET /users/{id}) ✅ ❌ ✅ allocation-free
Per-request context.Context cancellation ✅ ❌ (shutdown only) ✅
Zero-allocation hot path ❌ ✅ ✅
Streaming request/response bodies ✅ partial ✅
Adapters to and from net/http — partial ✅
FileServer with Range, conditional requests, and sendfile ✅ ✅ ✅
Response compression (gzip, pluggable zstd/br) ❌ ✅ ✅ allocation-free
HTTP client ✅ H1, H2 ✅ H1 ✅ H1, H2, H3, allocation-free

Performance claims are backed by reproducible benchmarks.

Contents

Installation

go get github.com/erizeta/qilat

qilat needs Go 1.26 or newer. CI tests the two latest Go releases on Linux and macOS.

Quick start

package main

import (
	"log"

	"github.com/erizeta/qilat"
)

func main() {
	h := qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString("Hello over " + r.Proto() + "!\n")
	})
	log.Fatal((&qilat.Server{Handler: h}).ListenAndServe(":8080"))
}
curl localhost:8080   # Hello over HTTP/1.1!

ListenAndServeTLS serves HTTP/1.1 and HTTP/2 over TCP and HTTP/3 over UDP, on the same port and with the same handlers. For local development, qilat.GenerateDevCert makes a self-signed certificate:

package main

import (
	"crypto/tls"
	"log"

	"github.com/erizeta/qilat"
)

func main() {
	cert, err := qilat.GenerateDevCert() // self-signed, for local development only
	if err != nil {
		log.Fatal(err)
	}
	srv := &qilat.Server{
		Handler: qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
			_, _ = w.WriteString("hello over " + r.Proto() + "\n")
		}),
		TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
	}
	// TCP: HTTP/1.1 and HTTP/2. UDP: HTTP/3. Both on port 8443.
	log.Fatal(srv.ListenAndServeTLS(":8443", "", ""))
}
curl -k --http1.1 https://localhost:8443      # hello over HTTP/1.1
curl -k https://localhost:8443                # hello over HTTP/2.0, with alt-svc: h3=":8443"; ma=86400
curl -k --http3-only https://localhost:8443   # hello over HTTP/3.0 (needs a curl built with HTTP/3)

In production, pass your certificate files instead: srv.ListenAndServeTLS(":443", "cert.pem", "key.pem"). examples/ has runnable programs, such as examples/hello (go run ./examples/hello) and examples/h3-dev-cert.

Handling requests

Headers, query parameters, and the body work as in net/http, with []byte variants that do not allocate:

package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/erizeta/qilat"
)

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("POST /greet", func(w qilat.ResponseWriter, r *qilat.Request) {
		var in struct {
			Name string `json:"name"`
		}
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			qilat.Error(w, "invalid JSON", http.StatusBadRequest)
			return
		}
		lang := r.URI().Query().Get("lang")  // a string copy
		agent := r.Header.Peek("User-Agent") // bytes valid until the handler returns
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]string{
			"message": "hello " + in.Name,
			"lang":    lang,
			"agent":   string(agent),
		})
	})
	log.Fatal((&qilat.Server{Handler: mux}).ListenAndServe(":8080"))
}
curl -d '{"name":"qilat"}' 'localhost:8080/greet?lang=en'
# {"agent":"curl/8.7.1","lang":"en","message":"hello qilat"}
  • r.Method(), r.Path(), r.Host(), r.Proto(), r.RemoteAddr(), r.TLS(), and r.ContentLength() describe the request; r.URI() gives the decoded path, raw query, and parsed Args.
  • r.Header.Get and Args.Get return strings; Peek returns bytes without allocating, and PeekAll every value of a repeated field. Names are case-insensitive.
  • r.Body is an io.Reader that is never nil. Bodies over MaxRequestBodySize (4 MiB by default) get 413, or qilat.ErrBodyTooLarge from Read when the length was not declared.
  • The keep-alive hot path of the HTTP/1.1 server allocates nothing per request.

Header, URI, and Args are usable on their own, for example to parse a URL-encoded form body with Args.Parse. examples/core shows them: go run ./examples/core.

Streaming and cancellation

ResponseWriter is an io.Writer with Flush, so a handler streams by writing and flushing as it goes. r.Context() is canceled when the client disconnects (an HTTP/1.1 connection closes, an HTTP/2 or HTTP/3 stream is reset) and when the handler returns, so long-running work can stop early:

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/erizeta/qilat"
)

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /events", func(w qilat.ResponseWriter, r *qilat.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		tick := time.NewTicker(time.Second)
		defer tick.Stop()
		for i := 1; ; i++ {
			select {
			case <-r.Context().Done(): // the client went away
				return
			case <-tick.C:
				_, _ = fmt.Fprintf(w, "data: tick %d\n\n", i)
				if err := w.Flush(); err != nil {
					return
				}
			}
		}
	})
	log.Fatal((&qilat.Server{Handler: mux}).ListenAndServe(":8080"))
}
curl -N localhost:8080/events   # data: tick 1, data: tick 2, ... one per second

Pass r.Context() to database calls and outgoing requests so they stop with the request. Unlike the request itself, the context is never reused, so it is safe to hand to goroutines.

Graceful shutdown

Shutdown stops accepting connections, closes idle ones, sends GOAWAY on HTTP/2 and HTTP/3, and waits for running requests. If its context ends first, the contexts of running requests are canceled. Close stops everything at once.

package main

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

	"github.com/erizeta/qilat"
)

func main() {
	srv := &qilat.Server{Handler: qilat.HandlerFunc(func(w qilat.ResponseWriter, _ *qilat.Request) {
		_, _ = w.WriteString("ok\n")
	})}

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	drained := make(chan struct{})
	go func() {
		defer close(drained)
		<-ctx.Done()
		timeout, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		if err := srv.Shutdown(timeout); err != nil {
			log.Print("shutdown: ", err)
		}
	}()

	if err := srv.ListenAndServe(":8080"); !errors.Is(err, qilat.ErrServerClosed) {
		log.Fatal(err)
	}
	<-drained // ListenAndServe returns as soon as Shutdown starts; wait for running requests
}

Middleware

Native middleware is a func(qilat.Handler) qilat.Handler, and qilat.Chain(a, b)(h) is a(b(h)), so middleware runs in the order listed:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/erizeta/qilat"
)

func logging(next qilat.Handler) qilat.Handler {
	return qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %v", r.Method(), r.Path(), time.Since(start))
	})
}

func requireToken(next qilat.Handler) qilat.Handler {
	return qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
		if string(r.Header.Peek("Authorization")) != "Bearer secret" {
			qilat.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /", func(w qilat.ResponseWriter, _ *qilat.Request) {
		_, _ = w.WriteString("welcome\n")
	})
	h := qilat.Chain(logging, requireToken, qilat.Compress(0))(mux)
	log.Fatal((&qilat.Server{Handler: h}).ListenAndServe(":8080"))
}

net/http middleware works too, through qilat.FromStdMiddleware.

Handler panics are recovered and logged, and the response is aborted, as in net/http; panic with qilat.ErrAbortHandler to abort without logging.

Routing

qilat.ServeMux has the same pattern syntax, precedence, and redirect behavior as net/http.ServeMux (Go 1.22+), and matching a request does not allocate:

package main

import (
	"log"

	"github.com/erizeta/qilat"
)

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /users/{id}", func(w qilat.ResponseWriter, r *qilat.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.WriteString(`{"id":"` + r.PathValue("id") + `"}` + "\n")
	})
	mux.HandleFunc("GET /files/{path...}", func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString("file: " + r.PathValue("path") + "\n")
	})
	log.Fatal((&qilat.Server{Handler: mux}).ListenAndServe(":8080"))
}
curl localhost:8080/users/42           # {"id":"42"}
curl localhost:8080/files/docs/a.txt   # file: docs/a.txt
curl -i -X POST localhost:8080/users/42  # 405 Method Not Allowed, Allow: GET, HEAD
  • Patterns: [METHOD ][HOST]/path, with {name} for one segment, {name...} for the rest of the path, {$} for an exact match, and a trailing / for a subtree. GET also matches HEAD.
  • The most specific pattern wins, and registering two patterns that conflict panics with a description of both.
  • Unclean paths (/a/../b, //) and subtree roots without the trailing slash are redirected with 307, unmatched paths get 404, and paths that match only another method get 405 with an Allow header.
  • r.PathValue(name) returns a decoded copy that is safe to keep. r.PeekPathValue(name) returns the same bytes without copying, valid until the handler returns. r.Pattern() returns the matched pattern.

examples/routing is a small key-value API: go run ./examples/routing.

One deliberate difference from net/http: escapes such as %2F and %2E are never treated as / or ., even in paths that also contain characters net/http re-escapes (where it would decode them before routing).

HTTP/2

HTTP/2 needs no extra code. ListenAndServeTLS offers h2 through ALPN and falls back to HTTP/1.1. Set H2C: true to also accept cleartext HTTP/2 (prior knowledge) on a plain listener:

package main

import (
	"log"

	"github.com/erizeta/qilat"
)

func main() {
	h := qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString("hello over " + r.Proto() + "\n")
		// Trailers are sent after the body, as gRPC does with grpc-status.
		w.(qilat.TrailerSetter).Trailer().Set("X-Checksum", "abc123")
	})
	srv := &qilat.Server{Handler: h, H2C: true}
	log.Fatal(srv.ListenAndServe(":8080"))
}
curl --http2-prior-knowledge localhost:8080   # hello over HTTP/2.0
curl localhost:8080                           # hello over HTTP/1.1 (same port)
  • Passes the h2spec conformance suite (147/147 in strict mode over TLS), run in CI.
  • Flow control follows how fast handlers read request bodies, so a slow handler slows its client down instead of buffering without bound.
  • Responses are scheduled by RFC 9218 priority (priority header and PRIORITY_UPDATE).
  • Protections against Rapid Reset (CVE-2023-44487), CONTINUATION floods, and control-frame floods are always on.
  • Server.HTTP2 tunes limits; zero values are safe defaults. DisableHTTP2 serves HTTP/1.1 only.

Trailers work on HTTP/1.1 too, in a chunked body. They are dropped only when the handler sets Content-Length itself, or for HTTP/1.0 clients. examples/http2 serves HTTPS and h2c side by side.

HTTP/3

ListenAndServeTLS serves HTTP/3 over UDP on the same port as TCP (see the quick start), and adds an Alt-Svc header to HTTP/1.1 and HTTP/2 responses so browsers and curl switch to HTTP/3.

  • If either the TCP or the UDP listener fails, ListenAndServeTLS closes the other and returns the error.
  • ServeQUIC(packetConn) serves HTTP/3 on a UDP socket of your own; DisableHTTP3 keeps ListenAndServeTLS on TCP only.
  • Server.HTTP3 tunes stream and flow-control limits and the Alt-Svc max age, or turns Alt-Svc off.
  • 0-RTT is off by default. With HTTP3.Allow0RTT, returning clients save a round trip, and early requests other than GET, HEAD, and OPTIONS get 425 Too Early, since 0-RTT data can be replayed.
  • QPACK uses only the static table, so it needs no extra streams and decodes requests without allocating.

QUIC runs over UDP, so firewalls and load balancers need to let it through. Deploying HTTP/3 covers ports, load balancers, TLS, Linux UDP buffer sizes, and 0-RTT.

net/http handlers and middleware

qilat.FromStd runs any http.Handler on qilat, over all three protocols, and qilat.ToStd mounts a qilat handler on a net/http server. qilat.FromStdMiddleware adapts func(http.Handler) http.Handler middleware, so packages such as chi/middleware and rs/cors work unchanged:

package main

import (
	"log"

	"github.com/erizeta/qilat"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/rs/cors"
)

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /hello/{name}", func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString("hello " + r.PathValue("name") + ", request " + middleware.GetReqID(r.Context()) + "\n")
	})
	// Compose the net/http middleware first, then adapt the chain once.
	std := chi.Chain(
		middleware.RequestID,
		middleware.Heartbeat("/ping"),
		middleware.Recoverer,
		cors.New(cors.Options{AllowedOrigins: []string{"https://example.com"}}).Handler,
	)
	h := qilat.FromStdMiddleware(std.Handler)(mux)
	log.Fatal((&qilat.Server{Handler: h}).ListenAndServe(":8080"))
}
go get github.com/go-chi/chi/v5 github.com/rs/cors
curl -i -H 'Origin: https://example.com' localhost:8080/hello/qilat   # Access-Control-Allow-Origin: https://example.com

examples/std-middleware is a separate module (so qilat does not depend on chi or cors) whose test runs this chain over HTTP/1.1, HTTP/2, and HTTP/3.

  • The *http.Request given to a net/http handler has the URL, canonical header map, Host, RemoteAddr, TLS, request context, and the ServeMux pattern and path values. Like in net/http it may be kept after the handler returns; its Body may not.
  • The http.ResponseWriter behaves like net/http's: the header is captured at WriteHeader, a missing Content-Type is sniffed, trailers work through the Trailer header or http.TrailerPrefix, and http.ErrAbortHandler aborts quietly. It implements http.Flusher, http.ResponseController's FlushError, and http.Hijacker on HTTP/1.x only.
  • Adapters cost allocations that native handlers avoid: about 0.5 µs and 10 allocations for FromStd, and twice that for FromStdMiddleware, which converts the request both ways.

Moving an existing application over gradually is covered in migrating from net/http.

Static files

qilat.FileServer serves an fs.FS with the behavior of http.FileServerFS, and is tested against it response for response:

package main

import (
	"log"
	"os"

	"github.com/erizeta/qilat"
)

func main() {
	mux := qilat.NewServeMux()
	mux.Handle("GET /static/", qilat.StripPrefix("/static", qilat.FileServer(os.DirFS("public"))))
	log.Fatal((&qilat.Server{Handler: qilat.Compress(0)(mux)}).ListenAndServe(":8080"))
}
curl -i localhost:8080/static/index.html            # 301 to ./, then serves index.html
curl -H 'Range: bytes=0-99' localhost:8080/static/app.js   # 206 Partial Content
  • Range requests (including multipart/byteranges), If-Modified-Since, If-None-Match, If-Match, If-Unmodified-Since, If-Range, Content-Type from the extension or by sniffing, index.html, and directory listings. Paths are cleaned, so requests cannot leave the file system.
  • On HTTP/1.1 without TLS, an *os.File is sent with sendfile; io.Copy to a qilat ResponseWriter gets the same path.
  • qilat.ServeFile and qilat.ServeContent work like their net/http namesakes; qilat.Error, qilat.NotFound, and qilat.StripPrefix too. Unlike net/http, the path left by StripPrefix always starts with /.

examples/static serves embedded files next to a JSON API: go run ./examples/static.

Compression

qilat.Compress(level) gzips responses for clients that accept it. qilat.CompressWith sets the minimum size and adds codings such as zstd or Brotli from any library with a Reset-able writer:

package main

import (
	"io"
	"log"

	"github.com/erizeta/qilat"
	"github.com/klauspost/compress/zstd"
)

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /", func(w qilat.ResponseWriter, _ *qilat.Request) {
		w.Header().Set("Content-Type", "text/plain")
		for range 100 {
			_, _ = w.WriteString("compressible text\n")
		}
	})
	zstdEncoder := qilat.Encoder{Name: "zstd", New: func(w io.Writer) qilat.CompressWriter {
		enc, _ := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedFastest))
		return enc
	}}
	compress := qilat.CompressWith(qilat.CompressConfig{Encoders: []qilat.Encoder{zstdEncoder}})
	log.Fatal((&qilat.Server{Handler: compress(mux)}).ListenAndServe(":8080"))
}
curl -sI -H 'Accept-Encoding: zstd, gzip' localhost:8080   # Content-Encoding: zstd
curl -sI -H 'Accept-Encoding: gzip' localhost:8080         # Content-Encoding: gzip
  • The coding is chosen from Accept-Encoding q-values; on a tie, extra encoders win over gzip in the order given.
  • Skipped: bodies under 1 KiB (MinSize), responses that already have Content-Encoding, Range requests and 206, text/event-stream (so server-sent events still stream), Cache-Control: no-transform, and types that do not compress, such as images and archives.
  • Compressible responses get Vary: Accept-Encoding, and a strong ETag becomes weak. Flush flushes the compressor too, so compressed streams arrive as they are written.
  • Writers are pooled: compressing allocates nothing per request.

HTTP client

qilat.Client sends requests over HTTP/1.1, HTTP/2, and HTTP/3. Like fasthttp, requests and responses come from pools, so a request does not allocate; like net/http, cancellation and timeouts follow the request context.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/erizeta/qilat"
)

func main() {
	client := &qilat.Client{
		Transport: &qilat.Transport{EnableHTTP3: true},
		Timeout:   10 * time.Second,
	}
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	if err := req.SetRequestURI("https://www.cloudflare.com/"); err != nil {
		log.Fatal(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	for range 2 {
		if err := client.Do(req, resp); err != nil {
			log.Fatal(err)
		}
		body, err := resp.ReadBody()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(resp.Proto(), resp.StatusCode(), len(body), "bytes")
	}
	// HTTP/2.0 200 ... bytes
	// HTTP/3.0 200 ... bytes  (the first response advertised HTTP/3 with Alt-Svc)
}

examples/client does the same for any URL: go run ./examples/client https://example.com/.

  • Transport keeps HTTP/1.1 connections per host (MaxConnsPerHost, MaxIdleConnsPerHost, IdleConnTimeout) and uses HTTP/2 when the server offers it through ALPN, multiplexing requests on one connection; a burst of first requests to a new HTTPS host waits for that one connection instead of each opening its own. H2C: true sends plain http:// requests over HTTP/2 with prior knowledge, for servers such as a qilat Server with H2C.
  • DialContext replaces the dialer for every protocol: it is called with "tcp", and with "udp" for HTTP/3.
  • With EnableHTTP3, HTTPS requests switch to HTTP/3 once the host advertises it with Alt-Svc (cached for its ma). If QUIC cannot connect within HTTP3DialTimeout, for example because UDP is blocked, the request falls back to TCP and the host stays on TCP for five minutes.
  • resp.Body streams the body; resp.ReadBody() reads all of it into a reused buffer, up to MaxResponseBodySize (64 MiB by default). Release the response when done, read or not: on HTTP/1.1, the rest of an unread body is drained in the background so the connection can be reused, if it arrives within 50 ms and 256 KiB, as in net/http since Go 1.27. Response headers are limited by MaxResponseHeaderBytes (64 KiB by default) on every protocol.
  • client.Do(req.WithContext(ctx), resp) or Client.Timeout stops the request even in the middle of the body: the HTTP/1.1 connection is closed, and the HTTP/2 or HTTP/3 stream is reset.
  • Redirects are followed up to 10 times; CheckRedirect can stop earlier. Credentials (Authorization, Cookie) are dropped when a redirect leaves the host or goes from HTTPS to HTTP. 307 and 308 resend the body when it is an io.Seeker.
  • Idempotent requests without a body are retried once when the server closed the connection before acting on them, such as a stale keep-alive connection or an HTTP/2 stream refused by GOAWAY.
  • Proxy sends HTTPS through an HTTP CONNECT tunnel and plain HTTP in absolute form, with Proxy-Authorization from the proxy URL; qilat.ProxyFromEnvironment reads HTTP_PROXY and friends. Only http:// proxies are supported.

Request lifetime

Like fasthttp, qilat reuses requests between clients. A *Request, its Header, its Body, the ResponseWriter, and every []byte they return are valid only until ServeHTTP returns. Copy what you need, or call r.Clone(ctx) to keep the whole request. r.Context() is the exception: it is never reused.

Set Server.DebugPoison in tests to make any use after return panic. docs/lifetime.md has the full rules, with examples of correct and incorrect use, and the matching rules for client requests and responses.

Configuration

Unlike net/http, a zero value means a safe limit, not "unlimited". Set a field to a negative value to disable it.

Server field Zero value means
ReadHeaderTimeout 10 s, also used for the TLS and QUIC handshakes and for each HTTP/3 request stream
IdleTimeout 120 s between keep-alive requests, and the QUIC idle timeout
MaxHeaderBytes 64 KiB of request line and header (larger headers get 431)
MaxRequestBodySize 4 MiB (larger bodies get 413, or qilat.ErrBodyTooLarge while reading)
ReadStallTimeout 60 s waiting for more request body from a client that stopped sending
WriteStallTimeout 60 s waiting for a client that stopped accepting the response
ReadTimeout no limit, so slow uploads work
WriteTimeout no limit, so streaming responses work
MaxConcurrentConns no limit
HTTP2.MaxConcurrentStreams, HTTP3.MaxConcurrentStreams 250 concurrent requests per connection

The stall timeouts apply only while a handler waits for the client: a read waiting for body bytes, or a write waiting for the client to take data. A slow upload or download that keeps moving, a long poll, and a server-sent event stream are not affected. Socket buffers and flow control make a reader's progress visible in steps of up to a few megabytes, so keep the timeouts at several seconds or more. When one expires, the read or write fails with an error wrapping os.ErrDeadlineExceeded, the request context is canceled, and the connection (HTTP/1.1) or stream (HTTP/2, HTTP/3) is closed.

ReadTimeout bounds reading the whole request, body included, and WriteTimeout writing the whole response. Both are off by default because a fixed limit breaks large uploads and long streams; set them when neither is expected. When both a stall timeout and a total timeout are set, the sooner deadline applies.

Other settings:

  • TLSConfig is cloned, never modified. H2C, DisableHTTP2, and DisableHTTP3 choose protocols.
  • HTTP2 and HTTP3 tune per-protocol limits: receive buffers, frame size, and Alt-Svc.
  • BaseContext sets the parent context of requests; Logger takes a *slog.Logger for handler panics and connection errors.
  • ReduceMemoryUsage returns read buffers to a pool while connections are idle, for servers with many idle connections.

Performance

Measured on an Apple M5 (10 cores), macOS 26.4, Go 1.27.3, medians of 5 runs. Full results, raw output, and how to reproduce them are in benchmarks/RESULTS.md.

Benchmark net/http fasthttp qilat
HTTP/1.1 server, 13-byte response, in-memory 964 ns, 22 allocs 247 ns, 0 allocs 235 ns, 0 allocs
HTTP/1.1 server, 20 request headers, in-memory 1.87 µs, 64 allocs 357 ns, 0 allocs 414 ns, 0 allocs
HTTP/1.1 server, 16 pipelined requests per write 2.04 µs 379 ns 343 ns
HTTP/2 server, h2load, 8 connections × 100 streams 362k req/s — 895k req/s
HTTP/1.1 client, 20 response headers 7.26 µs, 78 allocs 5.70 µs, 0 allocs 5.74 µs, 0 allocs
Benchmark quic-go/http3 qilat
HTTP/3 server, 13-byte response, quic-go client 16.0 µs, 199 allocs 11.3 µs, 131 allocs
HTTP/3 server, 20 request headers 25.0 µs, 341 allocs 14.4 µs, 189 allocs

The server numbers measure server cost: HTTP/1.1 uses an in-memory listener and a minimal client, so neither the kernel nor the client hides the difference. Over real TCP on this machine, all three HTTP/1.1 servers are within 10% of each other because the kernel dominates. Reproduce with:

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

Security

  • The HTTP/1.1 parser follows RFC 9112 strictly and rejects request smuggling patterns (Content-Length with Transfer-Encoding, duplicate lengths, obsolete line folding, bare LF) with the right status code.
  • Header values are sanitized against injection; invalid header names panic instead of reaching the wire.
  • HTTP/2 and HTTP/3 limit streams, header sizes, and control frames per connection by default.
  • Parsers for HTTP/1.1 requests and responses, HTTP/2 frames and HPACK, HTTP/3 frames, control streams, and QPACK, URIs, query strings, and headers are fuzzed nightly in CI.
  • FileServer cleans paths, so requests cannot leave the file system it serves.

Please report vulnerabilities privately, as described in SECURITY.md, not in public issues.

API stability

qilat follows Semantic Versioning. Before v1.0.0, minor versions (v0.x.0) may contain breaking changes, each listed in the changelog with what to change; patch versions only fix bugs. Packages under internal/ are not part of the API. The minimum Go version is raised only when a dependency or standard library feature needs it, and that is also noted in the changelog.

Known gaps, planned after v0.1.0: cookie and form helpers (r.Cookie, r.FormValue) and multipart forms, which work today through FromStd; proxies other than http:// in the client.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md and the Code of Conduct first.

License

MIT. Parts derived from the Go standard library carry its license in LICENSE-GO.

Documentation

Overview

Package qilat is a high-performance HTTP library with net/http-style APIs and native HTTP/1.1, HTTP/2, and HTTP/3 support.

Index

Examples

Constants

View Source
const (
	DefaultHTTP2MaxConcurrentStreams          = 250
	DefaultHTTP2MaxReadFrameSize              = 16 << 10
	DefaultHTTP2MaxReceiveBufferPerConnection = 1 << 20
	DefaultHTTP2MaxReceiveBufferPerStream     = 1 << 20
	DefaultHTTP2MaxDecoderHeaderTableSize     = 4 << 10
)

Default HTTP/2 limits used when the corresponding HTTP2Config field is zero.

View Source
const (
	DefaultHTTP3MaxConcurrentStreams          = 250
	DefaultHTTP3MaxReceiveBufferPerConnection = 4 << 20
	DefaultHTTP3MaxReceiveBufferPerStream     = 1 << 20
	DefaultAltSvcMaxAge                       = 24 * time.Hour
)

Default HTTP/3 settings used when the corresponding HTTP3Config field is zero.

View Source
const (
	DefaultReadHeaderTimeout  = 10 * time.Second
	DefaultIdleTimeout        = 120 * time.Second
	DefaultStallTimeout       = 60 * time.Second
	DefaultMaxHeaderBytes     = 64 << 10
	DefaultMaxRequestBodySize = 4 << 20
)

Default limits used when the corresponding Server field is zero.

View Source
const (
	DefaultMaxIdleConnsPerHost    = 100
	DefaultIdleConnTimeout        = 90 * time.Second
	DefaultDialTimeout            = 30 * time.Second
	DefaultTLSHandshakeTimeout    = 10 * time.Second
	DefaultMaxResponseHeaderBytes = 64 << 10
	DefaultHTTP3DialTimeout       = time.Second
)

Defaults used when the corresponding Transport field is zero.

View Source
const DefaultCompressMinSize = 1024

DefaultCompressMinSize is the smallest response body Compress compresses when MinSize is zero.

View Source
const DefaultMaxResponseBodySize = 64 << 20

DefaultMaxResponseBodySize is the limit of Response.ReadBody when Transport.MaxResponseBodySize is zero.

Variables

View Source
var (
	// ErrUseLastResponse can be returned by Client.CheckRedirect to stop at the redirect response.
	ErrUseLastResponse = errors.New("qilat: use last response")
	// ErrTooManyRedirects is returned by Client.Do after 10 redirects when CheckRedirect is nil.
	ErrTooManyRedirects = errors.New("qilat: stopped after 10 redirects")
	// ErrResponseBodyTooLarge is returned by Response.ReadBody when the body exceeds Transport.MaxResponseBodySize.
	ErrResponseBodyTooLarge = errors.New("qilat: response body too large")
)
View Source
var (
	// ErrNotSupported is returned when the protocol does not support a feature, such as Hijack on HTTP/2.
	ErrNotSupported = errors.New("qilat: feature not supported")
	// ErrHijacked is returned by ResponseWriter methods after the connection has been hijacked.
	ErrHijacked = errors.New("qilat: connection has been hijacked")
	// ErrBodyNotAllowed is returned by Write when the status code does not allow a body.
	ErrBodyNotAllowed = errors.New("qilat: request method or response status code does not allow body")
	// ErrContentLength is returned when the body length differs from a declared Content-Length.
	ErrContentLength = errors.New("qilat: body length does not match declared Content-Length")
)
View Source
var DefaultClient = &Client{}

DefaultClient is the Client used by Get.

View Source
var DefaultTransport = &Transport{}

DefaultTransport is the Transport used by a Client whose Transport is nil.

View Source
var ErrAbortHandler = errors.New("qilat: abort Handler")

ErrAbortHandler is a sentinel panic value to abort a handler. The connection or stream is closed and, unlike other panics, nothing is logged.

View Source
var ErrBodyTooLarge = exchange.ErrBodyTooLarge

ErrBodyTooLarge is returned when reading a request body that exceeds Server.MaxRequestBodySize.

View Source
var ErrInvalidURI = errors.New("qilat: invalid request URI")

ErrInvalidURI is returned when a request-target is malformed.

View Source
var ErrServerClosed = errors.New("qilat: Server closed")

ErrServerClosed is returned by Serve and ListenAndServe after Shutdown or Close.

Functions

func Chain

func Chain(mw ...func(Handler) Handler) func(Handler) Handler

Chain composes middleware so that Chain(a, b)(h) is a(b(h)): a sees the request first.

Example
package main

import (
	"fmt"
	"net"
	"strings"

	"github.com/erizeta/qilat"
)

// serve runs h on a loopback port and returns its base URL and a function that stops it.
func serve(h qilat.Handler) (string, func()) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	srv := &qilat.Server{Handler: h}
	done := make(chan struct{})
	go func() {
		defer close(done)
		_ = srv.Serve(ln)
	}()
	return "http://" + ln.Addr().String(), func() {
		_ = srv.Close()
		<-done
	}
}

// get fetches url with a client of its own and returns the status and body.
func get(url string) (int, string) {
	tr := &qilat.Transport{}
	defer tr.CloseIdleConnections()
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	if err := req.SetRequestURI(url); err != nil {
		panic(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	if err := (&qilat.Client{Transport: tr}).Do(req, resp); err != nil {
		panic(err)
	}
	body, err := resp.ReadBody()
	if err != nil {
		panic(err)
	}
	return resp.StatusCode(), string(body)
}

func main() {
	tag := func(name string) func(qilat.Handler) qilat.Handler {
		return func(next qilat.Handler) qilat.Handler {
			return qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
				w.Header().Add("X-Order", name)
				next.ServeHTTP(w, r)
			})
		}
	}
	h := qilat.Chain(tag("first"), tag("second"))(qilat.HandlerFunc(func(w qilat.ResponseWriter, _ *qilat.Request) {
		var order []string
		for _, v := range w.Header().PeekAll("X-Order") {
			order = append(order, string(v))
		}
		_, _ = w.WriteString(strings.Join(order, " then "))
	}))
	url, stop := serve(h)
	defer stop()

	fmt.Println(get(url))
}
Output:
200 first then second

func Compress

func Compress(level int) func(Handler) Handler

Compress returns middleware that compresses response bodies with gzip at level. See CompressWith.

func CompressWith

func CompressWith(cfg CompressConfig) func(Handler) Handler

CompressWith returns middleware that compresses response bodies with the coding the client prefers in Accept-Encoding. It skips small and already encoded bodies, Range requests, partial content, server-sent events, Cache-Control: no-transform, and types that do not compress, such as images. Compressible responses get Vary: Accept-Encoding, and a strong ETag becomes weak. It panics on an invalid Level.

func Error

func Error(w ResponseWriter, msg string, code int)

Error replies with the plain-text message msg and status code, like net/http.Error.

func FromStdMiddleware

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

FromStdMiddleware adapts net/http middleware, such as CORS or request logging, to qilat handlers. Each request pays for both FromStd and ToStd.

func GenerateDevCert

func GenerateDevCert(hosts ...string) (tls.Certificate, error)

GenerateDevCert returns a self-signed certificate for local development, valid for 30 days for the given host names and IP addresses ("localhost", "127.0.0.1", and "::1" if none are given). Browsers and clients will not trust it without an exception; never use it in production.

func Get

func Get(url string, resp *Response) error

Get fetches url with DefaultClient into resp.

func NotFound

func NotFound(w ResponseWriter, _ *Request)

NotFound replies with a 404 Not Found error.

func ProxyFromEnvironment

func ProxyFromEnvironment(req *Request) (*url.URL, error)

ProxyFromEnvironment returns the proxy for req from the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables, like net/http.ProxyFromEnvironment. Set it as Transport.Proxy. The variables are read once.

func ReleaseRequest

func ReleaseRequest(r *Request)

ReleaseRequest returns r to the pool. r must not be used afterwards.

func ReleaseResponse

func ReleaseResponse(resp *Response)

ReleaseResponse closes resp's body and returns resp to the pool. resp must not be used afterwards.

func ServeContent

func ServeContent(w ResponseWriter, r *Request, name string, modtime time.Time, content io.ReadSeeker)

ServeContent replies with content like net/http.ServeContent: it handles Range and conditional requests, sets Content-Type from name's extension or by sniffing, and sets Last-Modified unless modtime is zero. An ETag set on w before the call is used for If-Match, If-None-Match, and If-Range.

func ServeFile

func ServeFile(w ResponseWriter, r *Request, name string)

ServeFile replies with the named file or directory from the operating system, like net/http.ServeFile. Requests whose path contains a ".." element are rejected, in case name was built from the path.

func ToStd

func ToStd(h Handler) http.Handler

ToStd adapts a Handler to a net/http.Handler, so qilat handlers can run on a net/http server. Path values set by net/http.ServeMux are copied to the request.

Types

type Args

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

Args holds decoded query or form arguments in order. Keys are case-sensitive. Returned byte slices are valid until the next Parse or until the handler returns.

Example
package main

import (
	"fmt"

	"github.com/erizeta/qilat"
)

func main() {
	var a qilat.Args
	a.Parse([]byte("tag=go&tag=http%2F3&q=fast+server"))
	fmt.Println(a.Get("q"))
	for _, v := range a.PeekAll("tag") {
		fmt.Println(string(v))
	}
}
Output:
fast server
go
http/3

func (*Args) All

func (a *Args) All() iter.Seq2[[]byte, []byte]

All returns an iterator over every key and value in order.

func (*Args) Get

func (a *Args) Get(key string) string

Get returns a copy of the first value for key, or "" if there is none. It allocates.

func (*Args) Has

func (a *Args) Has(key string) bool

Has reports whether key is present.

func (*Args) Len

func (a *Args) Len() int

Len returns the number of arguments, counting repeated keys separately.

func (*Args) Parse

func (a *Args) Parse(query []byte)

Parse replaces the arguments with the decoded pairs of an application/x-www-form-urlencoded string. Like net/url.ParseQuery, pairs with invalid escapes or a ';' are skipped and the rest are kept.

func (*Args) Peek

func (a *Args) Peek(key string) []byte

Peek returns the first value for key, or nil if there is none.

func (*Args) PeekAll

func (a *Args) PeekAll(key string) [][]byte

PeekAll returns every value for key in order. It allocates the returned slice.

func (*Args) Reset

func (a *Args) Reset()

Reset removes all arguments while keeping allocated memory for reuse.

type Client

type Client struct {
	// Transport sends requests; nil means DefaultTransport.
	Transport RoundTripper
	// Timeout limits the whole request, including reading the response body; zero means no limit.
	// The request context, set with Request.WithContext, can end it sooner.
	Timeout time.Duration
	// CheckRedirect decides whether to follow a redirect response; redirects counts those already followed.
	// It sees the request just sent and its redirect response. Returning ErrUseLastResponse makes Do return
	// that response; any other error is returned by Do. Nil means follow up to 10 redirects.
	CheckRedirect func(req *Request, resp *Response, redirects int) error
}

Client sends HTTP requests over HTTP/1.1, HTTP/2, and HTTP/3. Requests and responses are pooled: get them with AcquireRequest and AcquireResponse and give them back with ReleaseRequest and ReleaseResponse. A Client is safe for concurrent use.

func (*Client) Do

func (c *Client) Do(req *Request, resp *Response) error

Do sends req and fills resp, following redirects. The response body streams from resp.Body until resp is released or closed. After a followed redirect, req describes the last request sent.

Example
package main

import (
	"fmt"
	"net"
	"time"

	"github.com/erizeta/qilat"
)

// serve runs h on a loopback port and returns its base URL and a function that stops it.
func serve(h qilat.Handler) (string, func()) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	srv := &qilat.Server{Handler: h}
	done := make(chan struct{})
	go func() {
		defer close(done)
		_ = srv.Serve(ln)
	}()
	return "http://" + ln.Addr().String(), func() {
		_ = srv.Close()
		<-done
	}
}

func main() {
	url, stop := serve(qilat.HandlerFunc(func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString(r.Method() + " " + r.Header.Get("X-Request-Id"))
	}))
	defer stop()

	tr := &qilat.Transport{}
	defer tr.CloseIdleConnections()
	client := &qilat.Client{Transport: tr, Timeout: 5 * time.Second}

	// Requests and responses are pooled; release them when done.
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	req.SetMethod("DELETE")
	req.Header.Set("X-Request-Id", "abc")
	if err := req.SetRequestURI(url + "/items/1"); err != nil {
		panic(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	if err := client.Do(req, resp); err != nil {
		panic(err)
	}
	body, err := resp.ReadBody() // owned by resp: valid until ReleaseResponse
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.Proto(), resp.StatusCode(), string(body))
}
Output:
HTTP/1.1 200 DELETE abc

func (*Client) Get

func (c *Client) Get(url string, resp *Response) error

Get fetches url into resp.

type CompressConfig

type CompressConfig struct {
	// Level is the gzip level, from gzip.HuffmanOnly to gzip.BestCompression. Zero means gzip.DefaultCompression.
	Level int
	// MinSize is the smallest body to compress; zero means DefaultCompressMinSize. Smaller bodies are not worth it.
	MinSize int
	// Encoders adds content codings such as "zstd" or "br". When the client accepts several codings equally,
	// they are preferred in this order, then gzip.
	Encoders []Encoder
}

CompressConfig configures CompressWith. The zero value compresses with gzip at the default level.

type CompressWriter

type CompressWriter interface {
	io.WriteCloser
	Flush() error
	Reset(w io.Writer)
}

CompressWriter is a reusable compressing writer, such as *gzip.Writer, *zstd.Encoder from github.com/klauspost/compress, or *brotli.Writer from github.com/andybalholm/brotli.

type Encoder

type Encoder struct {
	// Name is the content coding, as sent in Content-Encoding, such as "zstd".
	Name string
	// New returns a writer that compresses to w. Writers are pooled and reused with Reset.
	New func(w io.Writer) CompressWriter
}

Encoder is a content coding for CompressWith.

type HTTP2Config

type HTTP2Config struct {
	// MaxConcurrentStreams limits open streams, and running handlers, per connection.
	MaxConcurrentStreams int
	// MaxReadFrameSize is the largest frame the server accepts (16 KiB to 16 MiB).
	MaxReadFrameSize int
	// MaxReceiveBufferPerConnection and MaxReceiveBufferPerStream bound request body data buffered before
	// handlers read it, which is how much the client may send ahead (at least 64 KiB).
	MaxReceiveBufferPerConnection int
	MaxReceiveBufferPerStream     int
	// MaxDecoderHeaderTableSize is the HPACK dynamic table size the client may use for request headers.
	MaxDecoderHeaderTableSize int
}

HTTP2Config configures HTTP/2. Zero fields mean the defaults above; values outside the range HTTP/2 allows are clamped. Server.MaxHeaderBytes limits the decoded header list, and Server.MaxRequestBodySize the body.

Protections against known HTTP/2 attacks are always on: streams reset faster than their handlers finish (Rapid Reset, CVE-2023-44487), CONTINUATION floods, and floods of PING, SETTINGS, RST_STREAM, PRIORITY, or empty frames end the connection with GOAWAY ENHANCE_YOUR_CALM.

type HTTP3Config

type HTTP3Config struct {
	// MaxConcurrentStreams limits concurrent request streams, and running handlers, per connection.
	MaxConcurrentStreams int
	// MaxReceiveBufferPerConnection and MaxReceiveBufferPerStream bound the QUIC flow-control windows, which is
	// how much request data a client may send ahead of the handlers reading it.
	MaxReceiveBufferPerConnection int
	MaxReceiveBufferPerStream     int
	// Allow0RTT accepts requests in 0-RTT data, saving a round trip for returning clients. 0-RTT data can be
	// replayed, so requests other than GET, HEAD, and OPTIONS are answered 425 Too Early (RFC 8470).
	Allow0RTT bool
	// AltSvcMaxAge is how long clients may remember that HTTP/3 is available (the Alt-Svc "ma" parameter).
	AltSvcMaxAge time.Duration
	// DisableAltSvc stops adding the Alt-Svc header to HTTP/1.1 and HTTP/2 responses.
	DisableAltSvc bool
}

HTTP3Config configures HTTP/3. Zero fields mean the defaults above. Server.MaxHeaderBytes limits the field section, Server.MaxRequestBodySize the body, Server.IdleTimeout the QUIC idle timeout, and Server.ReadHeaderTimeout the QUIC handshake.

type Handler

type Handler interface {
	ServeHTTP(w ResponseWriter, r *Request)
}

Handler responds to an HTTP request. Like net/http.Handler, it must not use w or r after ServeHTTP returns; in qilat both are reused for later requests.

func FileServer

func FileServer(fsys fs.FS) Handler

FileServer returns a handler that serves files from fsys, like net/http.FileServerFS: Range requests, conditional requests, Content-Type detection, directory listings, and index.html. Paths cannot escape fsys. Files that implement io.Seeker support ranges; an *os.File is sent with sendfile on HTTP/1.1.

func FromStd

func FromStd(h http.Handler) Handler

FromStd adapts a net/http.Handler to a Handler. Each request allocates a net/http.Request with its URL and header map; like net/http, the request may be kept after the handler returns, but its Body may not. The ResponseWriter implements net/http.Flusher, FlushError, and on HTTP/1.x net/http.Hijacker.

Example
package main

import (
	"fmt"
	"net"
	"net/http"

	"github.com/erizeta/qilat"
)

// serve runs h on a loopback port and returns its base URL and a function that stops it.
func serve(h qilat.Handler) (string, func()) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	srv := &qilat.Server{Handler: h}
	done := make(chan struct{})
	go func() {
		defer close(done)
		_ = srv.Serve(ln)
	}()
	return "http://" + ln.Addr().String(), func() {
		_ = srv.Close()
		<-done
	}
}

// get fetches url with a client of its own and returns the status and body.
func get(url string) (int, string) {
	tr := &qilat.Transport{}
	defer tr.CloseIdleConnections()
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	if err := req.SetRequestURI(url); err != nil {
		panic(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	if err := (&qilat.Client{Transport: tr}).Do(req, resp); err != nil {
		panic(err)
	}
	body, err := resp.ReadBody()
	if err != nil {
		panic(err)
	}
	return resp.StatusCode(), string(body)
}

func main() {
	std := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = fmt.Fprintf(w, "net/http handler at %s", r.URL.Path)
	})
	url, stop := serve(qilat.FromStd(std))
	defer stop()

	fmt.Println(get(url + "/legacy"))
}
Output:
200 net/http handler at /legacy

func StripPrefix

func StripPrefix(prefix string, h Handler) Handler

StripPrefix serves requests whose path starts with prefix by removing it and calling h, like net/http.StripPrefix; other requests get 404. The remaining path always starts with a slash.

type HandlerFunc

type HandlerFunc func(w ResponseWriter, r *Request)

HandlerFunc adapts an ordinary function to a Handler.

func (HandlerFunc) ServeHTTP

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)

ServeHTTP calls f(w, r).

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

Header holds HTTP header fields in arrival order with lowercase names and case-insensitive lookups. Returned byte slices are valid only until the header changes or the handler returns.

Example
package main

import (
	"fmt"

	"github.com/erizeta/qilat"
)

func main() {
	var h qilat.Header
	h.Set("Content-Type", "application/json")
	h.Add("Vary", "Accept-Encoding")
	h.Add("vary", "Origin")

	fmt.Println(h.Get("CONTENT-TYPE"))
	fmt.Println(len(h.PeekAll("Vary")))
	for name, value := range h.All() {
		fmt.Printf("%s: %s\n", name, value)
	}
}
Output:
application/json
2
content-type: application/json
vary: Accept-Encoding
vary: Origin

func (*Header) Add

func (h *Header) Add(name, value string)

Add appends a field. Existing fields with the same name are kept. It panics and sanitizes like Header.Set.

func (*Header) AddBytes

func (h *Header) AddBytes(name string, value []byte)

AddBytes is like Header.Add but takes the value as bytes. value is copied.

func (*Header) All

func (h *Header) All() iter.Seq2[[]byte, []byte]

All returns an iterator over every field in order. Names are lowercase. The yielded slices follow the same rules as Header.Peek.

func (*Header) CopyTo

func (h *Header) CopyTo(dst *Header)

CopyTo replaces dst's fields with a deep copy of h's fields.

func (*Header) Del

func (h *Header) Del(name string)

Del removes every field with the given name.

func (*Header) Get

func (h *Header) Get(name string) string

Get returns a copy of the first value for name, or "" if there is none. Unlike Header.Peek the result stays valid after the handler returns, at the cost of an allocation.

func (*Header) Has

func (h *Header) Has(name string) bool

Has reports whether a field with the given name exists.

func (*Header) Len

func (h *Header) Len() int

Len returns the number of header fields, counting repeated names separately.

func (*Header) Peek

func (h *Header) Peek(name string) []byte

Peek returns the value of the first field with the given name, or nil if there is none. The result must not be modified and is valid only until the header changes or the handler returns.

func (*Header) PeekAll

func (h *Header) PeekAll(name string) [][]byte

PeekAll returns the values of every field with the given name, in order. It allocates the returned slice; the values follow the same rules as Header.Peek.

func (*Header) Reset

func (h *Header) Reset()

Reset removes all fields while keeping allocated memory for reuse.

func (*Header) Set

func (h *Header) Set(name, value string)

Set replaces every field named name with one field holding value. It panics if name is not an RFC 9110 token, and replaces CR, LF, and NUL in value with spaces to prevent header injection.

func (*Header) SetBytes

func (h *Header) SetBytes(name string, value []byte)

SetBytes is like Header.Set but takes the value as bytes. value is copied.

type Hijacker

type Hijacker interface {
	Hijack() (net.Conn, *bufio.ReadWriter, error)
}

Hijacker lets a handler take over the connection. Every qilat ResponseWriter implements it; Hijack returns ErrNotSupported when the protocol has no connection to hand over (HTTP/2, HTTP/3).

type Request

type Request struct {
	// Header holds the request header fields. The Host header is available from [Request.Host] instead.
	Header *Header
	// Body is the request body. It is never nil and returns io.EOF immediately when there is no body.
	Body io.Reader
	// contains filtered or unexported fields
}

Request is an HTTP request received by a server, or one sent by a Client. It is pooled: a server's Request and every byte slice it returns are valid only until ServeHTTP returns, and a client's until ReleaseRequest. Use Request.Clone to keep a request longer.

func AcquireRequest

func AcquireRequest() *Request

AcquireRequest returns an empty client request from a pool. Give it back with ReleaseRequest.

func (*Request) Clone

func (r *Request) Clone(ctx context.Context) *Request

Clone returns a deep copy of r with context ctx that stays valid after the handler returns. The body is not copied: the clone's Body is empty. It panics if ctx is nil.

func (*Request) ContentLength

func (r *Request) ContentLength() int64

ContentLength returns the declared body length, or -1 if it is unknown.

func (*Request) Context

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

Context returns the request context. It is canceled when the client disconnects or ServeHTTP returns. The first call allocates a small context that is never reused, so it is safe to pass to goroutines.

func (*Request) Host

func (r *Request) Host() []byte

Host returns the Host header (HTTP/1.1) or :authority pseudo-header (HTTP/2, HTTP/3).

func (*Request) Method

func (r *Request) Method() string

Method returns the request method, such as "GET".

func (*Request) Path

func (r *Request) Path() []byte

Path returns the percent-decoded request path. See URI.Path.

func (*Request) PathValue

func (r *Request) PathValue(name string) string

PathValue returns a copy of the value of the named wildcard in the ServeMux pattern that matched the request, or a value set by Request.SetPathValue. It returns "" if there is no such value. Like net/http.Request.PathValue, the value is percent-decoded.

func (*Request) Pattern

func (r *Request) Pattern() string

Pattern returns the ServeMux pattern that matched the request, or "" if none did.

func (*Request) PeekPathValue

func (r *Request) PeekPathValue(name string) []byte

PeekPathValue is like Request.PathValue but returns the value without copying it, or nil if there is none. The result must not be modified and is valid only until ServeHTTP returns.

func (*Request) Proto

func (r *Request) Proto() string

Proto returns the protocol version: "HTTP/1.0", "HTTP/1.1", "HTTP/2.0", or "HTTP/3.0".

func (*Request) RemoteAddr

func (r *Request) RemoteAddr() net.Addr

RemoteAddr returns the network address of the client.

func (*Request) SetBody

func (r *Request) SetBody(body io.Reader, contentLength int64)

SetBody sets the request body. contentLength is -1 when unknown, which sends a chunked body on HTTP/1.1. A body that implements io.Seeker can be sent again after a 307 or 308 redirect.

func (*Request) SetHost

func (r *Request) SetHost(host string) error

SetHost overrides the Host header that SetRequestURI set.

func (*Request) SetMethod

func (r *Request) SetMethod(method string)

SetMethod sets the request method. It panics if method is not an RFC 9110 token.

func (*Request) SetPathValue

func (r *Request) SetPathValue(name, value string)

SetPathValue sets the value returned by PathValue(name). value is copied and not unescaped.

func (*Request) SetRequestURI

func (r *Request) SetRequestURI(uri string) error

SetRequestURI sets the URL a client request is sent to, such as "https://example.com/path?q=1", and its Host header.

func (*Request) TLS

func (r *Request) TLS() *tls.ConnectionState

TLS returns the TLS connection state, or nil for unencrypted connections.

func (*Request) URI

func (r *Request) URI() *URI

URI returns the parsed request-target.

func (*Request) WithContext

func (r *Request) WithContext(ctx context.Context) *Request

WithContext returns a shallow copy of r with its context changed to ctx. The copy shares r's header, URI, and body, and has the same lifetime as r. It panics if ctx is nil.

type Response

type Response struct {
	// Header holds the response header fields.
	Header *Header
	// Body streams the response body. It is never nil. Read it to EOF, or release the response, so the
	// connection can be reused.
	Body io.Reader
	// contains filtered or unexported fields
}

Response is an HTTP response received by a Client. It is pooled: the Response and every byte slice it returns are valid until ReleaseResponse.

func AcquireResponse

func AcquireResponse() *Response

AcquireResponse returns an empty response from a pool. Give it back with ReleaseResponse.

func (*Response) Close

func (resp *Response) Close() error

Close abandons the unread body and frees its connection for reuse when possible. ReleaseResponse calls it.

func (*Response) ContentLength

func (resp *Response) ContentLength() int64

ContentLength returns the declared body length, or -1 if it is unknown.

func (*Response) Proto

func (resp *Response) Proto() string

Proto returns the protocol version: "HTTP/1.1", "HTTP/2.0", or "HTTP/3.0".

func (*Response) ReadBody

func (resp *Response) ReadBody() ([]byte, error)

ReadBody reads the whole body into a buffer owned by resp and returns it. It fails with ErrResponseBodyTooLarge past Transport.MaxResponseBodySize.

func (*Response) StatusCode

func (resp *Response) StatusCode() int

StatusCode returns the status code, such as 200.

func (*Response) Trailer

func (resp *Response) Trailer() *Header

Trailer returns the trailer fields, which are complete once Body has returned io.EOF.

type ResponseWriter

type ResponseWriter interface {
	// Header returns the response header. Content-Length is read when WriteHeader is called, and changes made
	// after the header is sent (on Flush, a large Write, or when ServeHTTP returns) have no effect.
	Header() *Header
	// WriteHeader sends the status code. Only the first call has an effect; it panics on codes outside 100–999.
	WriteHeader(statusCode int)
	// Write sends body bytes, calling WriteHeader(200) first if needed.
	Write(p []byte) (int, error)
	WriteString(s string) (int, error)
	// Flush sends the header and any buffered body bytes to the client.
	Flush() error
}

ResponseWriter is used by a Handler to construct a response. It must not be used after ServeHTTP returns.

type RoundTripper

type RoundTripper interface {
	RoundTrip(req *Request, resp *Response) error
}

RoundTripper sends one request and receives its response, like net/http.RoundTripper. The response body is read through resp afterwards, and resp must be released or closed when done.

type ServeMux

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

ServeMux is an HTTP request multiplexer with the pattern syntax, precedence, and redirect behavior of net/http.ServeMux (Go 1.22 and later). Matching a request does not allocate.

Patterns have the form "[METHOD ][HOST]/[PATH]", for example "GET /users/{id}", "example.com/", or "/files/{path...}". A wildcard {name} matches one path segment and {name...} the rest of the path; their values are available from Request.PathValue. {$} matches only the end of the path, and a pattern ending in a slash matches every path below it. GET patterns also match HEAD requests.

When several patterns match, the most specific one wins; a pattern with a host beats one without. Registering a pattern that conflicts with an existing one (neither is more specific) panics.

Requests whose path is not clean (containing ".", "..", or repeated slashes) are redirected to the clean path, and a request for "/dir" is redirected to "/dir/" when only "/dir/" is registered. Both redirects use 307 Temporary Redirect, like net/http since Go 1.26. A path that matches no pattern gets 404, and one that matches only with another method gets 405 with an Allow header. CONNECT requests are matched without cleaning the path or stripping the port.

Example
package main

import (
	"fmt"
	"net"

	"github.com/erizeta/qilat"
)

// serve runs h on a loopback port and returns its base URL and a function that stops it.
func serve(h qilat.Handler) (string, func()) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	srv := &qilat.Server{Handler: h}
	done := make(chan struct{})
	go func() {
		defer close(done)
		_ = srv.Serve(ln)
	}()
	return "http://" + ln.Addr().String(), func() {
		_ = srv.Close()
		<-done
	}
}

// get fetches url with a client of its own and returns the status and body.
func get(url string) (int, string) {
	tr := &qilat.Transport{}
	defer tr.CloseIdleConnections()
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	if err := req.SetRequestURI(url); err != nil {
		panic(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	if err := (&qilat.Client{Transport: tr}).Do(req, resp); err != nil {
		panic(err)
	}
	body, err := resp.ReadBody()
	if err != nil {
		panic(err)
	}
	return resp.StatusCode(), string(body)
}

func main() {
	mux := qilat.NewServeMux()
	mux.HandleFunc("GET /users/{id}", func(w qilat.ResponseWriter, r *qilat.Request) {
		_, _ = w.WriteString("user " + r.PathValue("id"))
	})
	url, stop := serve(mux)
	defer stop()

	fmt.Println(get(url + "/users/42"))
	fmt.Println(get(url + "/nope"))
}
Output:
200 user 42
404 404 page not found

func NewServeMux

func NewServeMux() *ServeMux

NewServeMux allocates and returns a new ServeMux.

func (*ServeMux) Handle

func (mux *ServeMux) Handle(pattern string, handler Handler)

Handle registers handler for pattern. It panics if pattern is invalid or conflicts with a registered pattern.

func (*ServeMux) HandleFunc

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

HandleFunc registers handler for pattern. It panics like ServeMux.Handle.

func (*ServeMux) Handler

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)

Handler returns the handler to use for r and the pattern that matched it. It never returns nil: when nothing matches, it returns a handler that replies 404 or 405 and an empty pattern, and when the request must be redirected, a redirecting handler and the pattern that will match after the redirect. Handler does not set path values, so r.PathValue keeps returning what it did before.

func (*ServeMux) ServeHTTP

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

ServeHTTP dispatches the request to the handler whose pattern most closely matches it, setting Request.Pattern and the values returned by Request.PathValue.

type Server

type Server struct {
	// Handler serves every request. It must not be nil.
	Handler Handler
	// TLSConfig is used by ServeTLS and ListenAndServeTLS. It is cloned, never modified.
	TLSConfig *tls.Config

	// ReadHeaderTimeout bounds reading a request line and header. Zero means DefaultReadHeaderTimeout.
	ReadHeaderTimeout time.Duration
	// ReadTimeout bounds reading a whole request, body included. Zero means no limit, so slow uploads work.
	ReadTimeout time.Duration
	// WriteTimeout bounds writing a response, measured from the end of the request header. Zero means no limit.
	WriteTimeout time.Duration
	// ReadStallTimeout bounds how long a request body read waits for the client to send more bytes, so a slow
	// upload that keeps sending is not cut off. Zero means DefaultStallTimeout.
	ReadStallTimeout time.Duration
	// WriteStallTimeout bounds how long a response write waits for the client to accept more data, through the
	// socket or flow control. Zero means DefaultStallTimeout.
	WriteStallTimeout time.Duration
	// IdleTimeout bounds the wait for the next keep-alive request. Zero means DefaultIdleTimeout.
	IdleTimeout time.Duration
	// MaxHeaderBytes limits the request line and header. Zero means DefaultMaxHeaderBytes.
	MaxHeaderBytes int
	// MaxRequestBodySize limits the request body; larger bodies get 413. Zero means DefaultMaxRequestBodySize.
	MaxRequestBodySize int64
	// MaxConcurrentConns limits open connections; Accept waits while the limit is reached. Zero means no limit.
	MaxConcurrentConns int

	// HTTP2 configures HTTP/2, which TLS connections negotiate through ALPN unless DisableHTTP2 is set.
	HTTP2 HTTP2Config
	// H2C serves cleartext HTTP/2 to clients that start with the HTTP/2 preface (prior knowledge), on the
	// same listener as HTTP/1.1. The deprecated HTTP/1.1 Upgrade to h2c is not supported.
	H2C bool
	// DisableHTTP2 serves HTTP/1.1 only, even if TLSConfig.NextProtos lists "h2".
	DisableHTTP2 bool
	// HTTP3 configures HTTP/3, which ListenAndServeTLS and ServeQUIC serve over UDP.
	HTTP3 HTTP3Config
	// DisableHTTP3 makes ListenAndServeTLS serve TCP only.
	DisableHTTP3 bool

	// ReduceMemoryUsage returns read buffers to a pool while connections are idle, costing one extra read per request.
	ReduceMemoryUsage bool
	// DebugPoison makes handlers that use a request or ResponseWriter after ServeHTTP returns panic. It disables pooling.
	DebugPoison bool

	// BaseContext returns the base context for requests from ln, which is nil for HTTP/3. If BaseContext is nil,
	// context.Background is used.
	BaseContext func(ln net.Listener) context.Context
	// Logger receives handler panics and connection errors. If nil, slog.Default is used.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

Server is an HTTP server. Unlike net/http, zero limits mean safe defaults rather than no limit; set a negative value to disable a limit. A Server must not be copied after first use.

func (*Server) Close

func (s *Server) Close() error

Close immediately closes all listeners and connections, except hijacked ones, and cancels running requests.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(addr string) error

ListenAndServe listens on the TCP address addr and serves HTTP/1.1, and cleartext HTTP/2 if H2C is set. An empty addr means ":http".

func (*Server) ListenAndServeTLS

func (s *Server) ListenAndServeTLS(addr, certFile, keyFile string) error

ListenAndServeTLS listens on addr and serves HTTPS: HTTP/2 and HTTP/1.1 over TCP, chosen through ALPN, and HTTP/3 over UDP on the same port unless DisableHTTP3 is set. HTTP/1.1 and HTTP/2 responses then carry an Alt-Svc header so clients can switch to HTTP/3. If either listener fails, the other is closed and the error is returned. An empty addr means ":https". The certificate files may be empty if TLSConfig already holds certificates.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve accepts connections on ln and serves each in its own goroutine. It always closes ln and returns ErrServerClosed after Shutdown or Close.

func (*Server) ServeQUIC

func (s *Server) ServeQUIC(pc net.PacketConn) error

ServeQUIC serves HTTP/3 on the UDP connection pc, using TLSConfig, which must hold certificates. It always closes pc, after the connections it accepted have finished, and returns ErrServerClosed after Shutdown or Close. While it runs, HTTP/1.1 and HTTP/2 responses from this Server advertise it with Alt-Svc.

func (*Server) ServeTLS

func (s *Server) ServeTLS(ln net.Listener, certFile, keyFile string) error

ServeTLS serves HTTPS on ln. See Server.ListenAndServeTLS for the certificate arguments. It serves HTTP/1.1 and HTTP/2 only; use Server.ServeQUIC to add HTTP/3.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops accepting connections, closes idle ones, and waits for active requests to finish. If ctx ends first, the contexts of running requests are canceled and ctx's error is returned.

Example
package main

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/erizeta/qilat"
)

// get fetches url with a client of its own and returns the status and body.
func get(url string) (int, string) {
	tr := &qilat.Transport{}
	defer tr.CloseIdleConnections()
	req := qilat.AcquireRequest()
	defer qilat.ReleaseRequest(req)
	if err := req.SetRequestURI(url); err != nil {
		panic(err)
	}
	resp := qilat.AcquireResponse()
	defer qilat.ReleaseResponse(resp)
	if err := (&qilat.Client{Transport: tr}).Do(req, resp); err != nil {
		panic(err)
	}
	body, err := resp.ReadBody()
	if err != nil {
		panic(err)
	}
	return resp.StatusCode(), string(body)
}

func main() {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	srv := &qilat.Server{Handler: qilat.HandlerFunc(func(w qilat.ResponseWriter, _ *qilat.Request) {
		_, _ = w.WriteString("ok")
	})}
	served := make(chan error, 1)
	go func() { served <- srv.Serve(ln) }()
	fmt.Println(get("http://" + ln.Addr().String()))

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		panic(err)
	}
	fmt.Println(<-served)
}
Output:
200 ok
qilat: Server closed

type TrailerSetter

type TrailerSetter interface {
	Trailer() *Header
}

TrailerSetter lets a handler send trailer fields after the body. Every qilat ResponseWriter implements it. Fields set on the returned Header before ServeHTTP returns are sent as trailers. HTTP/2 always delivers them. HTTP/1.1 sends them in a chunked body, so they are dropped when the handler sets Content-Length itself or the client speaks HTTP/1.0.

type Transport

type Transport struct {
	// DialContext opens connections; nil means a net.Dialer. It is called with network "tcp", and with "udp"
	// for HTTP/3, where returning an error makes the request fall back to TCP.
	DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
	// DialTimeout limits connecting; zero means DefaultDialTimeout.
	DialTimeout time.Duration
	// TLSClientConfig configures TLS for HTTPS; nil means the defaults. It is cloned for each connection.
	TLSClientConfig *tls.Config
	// TLSHandshakeTimeout limits the TLS handshake; zero means DefaultTLSHandshakeTimeout.
	TLSHandshakeTimeout time.Duration
	// Proxy returns the proxy for a request, or nil for none; nil means no proxy. HTTPS requests go through
	// an HTTP CONNECT tunnel, plain HTTP requests are sent to the proxy in absolute form. See ProxyFromEnvironment.
	Proxy func(req *Request) (*url.URL, error)
	// MaxConnsPerHost limits HTTP/1.1 connections per host, counting those in use; zero means no limit.
	// Requests wait for a free connection.
	MaxConnsPerHost int
	// MaxIdleConnsPerHost limits idle HTTP/1.1 connections kept per host; zero means DefaultMaxIdleConnsPerHost.
	// A negative value disables keep-alive.
	MaxIdleConnsPerHost int
	// IdleConnTimeout closes connections idle for this long; zero means DefaultIdleConnTimeout.
	IdleConnTimeout time.Duration
	// ResponseHeaderTimeout limits waiting for the response header after the request is sent; zero means no limit.
	ResponseHeaderTimeout time.Duration
	// MaxResponseHeaderBytes limits the response header; zero means DefaultMaxResponseHeaderBytes.
	MaxResponseHeaderBytes int
	// MaxResponseBodySize limits Response.ReadBody; zero means DefaultMaxResponseBodySize, negative no limit.
	// Reading Response.Body directly is not limited.
	MaxResponseBodySize int64
	// DisableHTTP2 keeps HTTPS requests on HTTP/1.1.
	DisableHTTP2 bool
	// H2C sends plain http:// requests over HTTP/2 with prior knowledge (RFC 9113 section 3.3), for servers
	// known to accept it, such as a qilat Server with H2C set. It is not used through a proxy.
	H2C bool
	// EnableHTTP3 sends HTTPS requests over HTTP/3 to hosts that advertised it with Alt-Svc in an earlier
	// response. When QUIC cannot connect within HTTP3DialTimeout, as when UDP is blocked, the request falls
	// back to TCP and the host is reached over TCP for a while. HTTP/3 is not used through a proxy.
	EnableHTTP3 bool
	// HTTP3DialTimeout limits the QUIC handshake before falling back to TCP; zero means DefaultHTTP3DialTimeout.
	HTTP3DialTimeout time.Duration
	// contains filtered or unexported fields
}

Transport is a RoundTripper that keeps connections open for reuse. HTTPS requests use HTTP/2 when the server offers it through ALPN, multiplexed on one connection per host; other requests use a pool of HTTP/1.1 connections. A zero Transport is ready to use, and safe for concurrent use.

Like the Server, zero fields mean safe defaults rather than "unlimited".

func (*Transport) CloseIdleConnections

func (t *Transport) CloseIdleConnections()

CloseIdleConnections closes connections that are not carrying a request.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *Request, resp *Response) error

RoundTrip sends req on a pooled connection and reads the response header into resp.

type URI

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

URI is a parsed request-target (RFC 9112 section 3.2). Byte slices it returns are valid until the next Parse or until the handler returns. The zero value is an empty origin-form target.

Example
package main

import (
	"fmt"

	"github.com/erizeta/qilat"
)

func main() {
	var u qilat.URI
	if err := u.Parse([]byte("/users/j%C3%B6rg?tab=posts&page=2")); err != nil {
		panic(err)
	}
	fmt.Println(string(u.RawPath()))
	fmt.Println(string(u.Path()))
	fmt.Println(u.Query().Get("page"))
}
Output:
/users/j%C3%B6rg
/users/jörg
2

func (*URI) CopyTo

func (u *URI) CopyTo(dst *URI)

CopyTo makes dst an independent copy of u.

func (*URI) Host

func (u *URI) Host() []byte

Host returns the authority of an absolute- or authority-form target, or nil for other forms.

func (*URI) Parse

func (u *URI) Parse(target []byte) error

Parse copies target and parses it in origin, absolute, authority, or asterisk form. Path decoding and query parsing are deferred until URI.Path and URI.Query are called.

func (*URI) Path

func (u *URI) Path() []byte

Path returns the percent-decoded path. An encoded slash (%2F) decodes to '/'; use URI.RawPath when the distinction matters. '+' is not decoded in paths.

func (*URI) Query

func (u *URI) Query() *Args

Query returns the parsed query arguments.

func (*URI) Raw

func (u *URI) Raw() []byte

Raw returns the request-target exactly as parsed.

func (*URI) RawPath

func (u *URI) RawPath() []byte

RawPath returns the path still percent-encoded. It is empty for authority-form targets.

func (*URI) RawQuery

func (u *URI) RawQuery() []byte

RawQuery returns the query without the leading '?', still percent-encoded.

func (*URI) Scheme

func (u *URI) Scheme() []byte

Scheme returns the scheme of an absolute-form target, or nil for other forms.

func (*URI) String

func (u *URI) String() string

String returns the request-target as a string. It allocates.

Directories

Path Synopsis
examples
client command
Command client fetches a URL with qilat's Client and prints the protocol, status, and body size.
Command client fetches a URL with qilat's Client and prints the protocol, status, and body size.
core command
Command core shows the HTTP building blocks available in qilat today.
Command core shows the HTTP building blocks available in qilat today.
h3-dev-cert command
Command h3-dev-cert serves HTTP/1.1, HTTP/2, and HTTP/3 on one port with a generated development certificate.
Command h3-dev-cert serves HTTP/1.1, HTTP/2, and HTTP/3 on one port with a generated development certificate.
hello command
Command hello serves a minimal HTTP/1.1 application with qilat.
Command hello serves a minimal HTTP/1.1 application with qilat.
http2 command
Command http2 serves HTTP/2 over TLS (with HTTP/1.1 fallback) and cleartext HTTP/2 (h2c) with qilat.
Command http2 serves HTTP/2 over TLS (with HTTP/1.1 fallback) and cleartext HTTP/2 (h2c) with qilat.
routing command
Command routing serves a small in-memory key-value API with qilat's ServeMux.
Command routing serves a small in-memory key-value API with qilat's ServeMux.
static command
Command static serves the files embedded from ./public with gzip compression, next to a small JSON API.
Command static serves the files embedded from ./public with gzip compression, next to a small JSON API.
internal
bytesconv
Package bytesconv provides allocation-free conversions and ASCII helpers for HTTP parsing.
Package bytesconv provides allocation-free conversions and ASCII helpers for HTTP parsing.
exchange
Package exchange is the contract between protocol implementations and the qilat core.
Package exchange is the contract between protocol implementations and the qilat core.
h1
Package h1 implements the HTTP/1.1 server protocol (RFC 9112) on top of the exchange contract.
Package h1 implements the HTTP/1.1 server protocol (RFC 9112) on top of the exchange contract.
h2
Package h2 implements the server side of HTTP/2 (RFC 9113) on top of internal/exchange.
Package h2 implements the server side of HTTP/2 (RFC 9113) on top of internal/exchange.
h3
Package h3 implements the server side of HTTP/3 (RFC 9114) on raw QUIC streams from quic-go.
Package h3 implements the server side of HTTP/3 (RFC 9114) on raw QUIC streams from quic-go.
httpfields
Package httpfields holds the header field rules HTTP/2 and HTTP/3 share (RFC 9113 section 8, RFC 9114 section 4) on top of their different header compression.
Package httpfields holds the header field rules HTTP/2 and HTTP/3 share (RFC 9113 section 8, RFC 9114 section 4) on top of their different header compression.
stall
Package stall detects a client that stops making progress while a handler waits for it.
Package stall detects a client that stops making progress while a handler waits for it.

Jump to

Keyboard shortcuts

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