Minimal app-facing HTTP abstractions, middleware, adapters, and route indexing for GoForj.
web is built on top of Echo, which is a fantastic HTTP framework with a fast router, strong middleware story, and a mature ecosystem. GoForj wraps it so applications can code against a smaller app-facing contract while still getting a high-quality underlying engine, reusable middleware packages, testing helpers, route indexing, and framework-owned integration points like Prometheus and generated wiring.
go get github.com/goforj/webpackage main
import (
"fmt"
"log"
"net/http"
"github.com/goforj/web"
"github.com/goforj/web/adapter/echoweb"
"github.com/goforj/web/webmiddleware"
)
func main() {
// Pick an adapter that satisfies the app-facing web.Router contract.
adapter := echoweb.New()
router := adapter.Router()
// Attach common app middleware once near the top of the stack.
router.Use(
webmiddleware.Recover(),
webmiddleware.RequestID(),
)
// Register a basic health route for uptime checks and local smoke tests.
router.GET("/healthz", func(c web.Context) error {
return c.Text(http.StatusOK, "ok")
})
// Register an actual application route that returns JSON.
router.GET("/users/:id", func(c web.Context) error {
return c.JSON(http.StatusOK, map[string]any{
"id": c.Param("id"),
"name": fmt.Sprintf("user-%s", c.Param("id")),
})
})
// Boot the HTTP server with the adapter as the final handler.
log.Fatal(http.ListenAndServe(":8080", adapter))
}routes := []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusOK)
}),
web.NewRoute(http.MethodGet, "/users", func(c web.Context) error {
return c.JSON(http.StatusOK, []map[string]any{{"id": 1}})
}),
}
group := web.NewRouteGroup("/api", routes)
adapter := echoweb.New()
_ = web.RegisterRoutes(adapter.Router(), []web.RouteGroup{group})requestID := webmiddleware.RequestID()
recoverer := webmiddleware.Recover()
limiter := webmiddleware.RateLimiter(
webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second)),
)
handler := web.Handler(func(c web.Context) error {
return c.Text(http.StatusOK, "ok")
})
wrapped := requestID(recoverer(limiter(handler)))
_ = wrappedreq := httptest.NewRequest(http.MethodGet, "/healthz", nil)
ctx := webtest.NewContext(req, nil, "/healthz", nil)
handler := webmiddleware.RequestID()(func(c web.Context) error {
return c.Text(http.StatusOK, "ok")
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
fmt.Println(ctx.Response().Header().Get("X-Request-ID") != "")
fmt.Println(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String())
// 200
// true
// okadapter := echoweb.New()
metrics, _ := webprometheus.New(webprometheus.Config{Namespace: "app"})
adapter.Router().Use(metrics.Middleware())
adapter.Router().GET("/users", func(c web.Context) error {
return c.NoContent(http.StatusOK)
})
adapter.Router().GET("/metrics", metrics.Handler())manifest, err := webindex.Run(context.Background(), webindex.IndexOptions{
Root: ".",
OutPath: "webindex.json",
})
fmt.Println(err == nil, manifest.Version != "")
// true trueweb: app-facing interfaces, route registration, route reporting helpersadapter/echoweb: Echo-backed adapter and server bootstrapwebmiddleware: grouped HTTP middleware for auth, routing, payloads, rate limiting, and morewebprometheus: Prometheus middleware and scrape handlerwebindex: route manifest and OpenAPI index generationwebtest: lightweight handler testing context
Generated from public API comments and examples.
Echo returns the underlying Echo engine.
adapter := echoweb.New()
fmt.Println(adapter.Echo() != nil)
// trueRouter returns the app-facing router contract.
adapter := echoweb.New()
fmt.Println(adapter.Router() != nil)
// trueServeHTTP exposes the adapter as a standard http.Handler.
adapter := echoweb.New()
adapter.Router().GET("/healthz", func(c web.Context) error { return c.NoContent(http.StatusOK) })
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
adapter.ServeHTTP(rr, req)
fmt.Println(rr.Code)
// 204New creates a new Echo-backed web adapter.
adapter := echoweb.New()
fmt.Println(adapter.Router() != nil, adapter.Echo() != nil)
// true trueNewServer creates an Echo-backed server from web route groups and mounts.
server, err := echoweb.NewServer(echoweb.ServerConfig{
RouteGroups: []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return c.NoContent(http.StatusOK) }),
}),
},
})
fmt.Println(err == nil, server.Router() != nil)
// true trueRouter exposes the app-facing router contract.
server, _ := echoweb.NewServer(echoweb.ServerConfig{})
fmt.Println(server.Router() != nil)
// trueServe starts the server and gracefully shuts it down when ctx is cancelled.
server, _ := echoweb.NewServer(echoweb.ServerConfig{Addr: "127.0.0.1:0"})
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println(server.Serve(ctx) == nil)
// trueServeHTTP exposes the server as an http.Handler for tests and local probing.
server, _ := echoweb.NewServer(echoweb.ServerConfig{
RouteGroups: []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return c.NoContent(http.StatusOK) }),
}),
},
})
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/healthz", nil)
server.ServeHTTP(rr, req)
fmt.Println(rr.Code)
// 204UnwrapContext returns the underlying Echo context when the web.Context came from this adapter.
adapter := echoweb.New()
adapter.Router().GET("/healthz", func(c web.Context) error {
_, ok := echoweb.UnwrapContext(c)
fmt.Println(ok)
return c.NoContent(http.StatusOK)
})
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
adapter.ServeHTTP(rr, req)
// trueUnwrapWebSocketConn returns the underlying gorilla websocket connection.
_, ok := echoweb.UnwrapWebSocketConn(nil)
fmt.Println(ok)
// falseWrap exposes an existing Echo engine through the web.Router contract.
adapter := echoweb.Wrap(nil)
fmt.Println(adapter.Echo() != nil)
// trueRun indexes API metadata from source and writes artifacts.
manifest, err := webindex.Run(context.Background(), webindex.IndexOptions{
Root: ".",
OutPath: "webindex.json",
})
fmt.Println(err == nil, manifest.Version != "")
// true trueBasicAuth returns basic auth middleware.
mw := webmiddleware.BasicAuth(func(user, pass string, c web.Context) (bool, error) {
return user == "demo" && pass == "secret", nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "basic ZGVtbzpzZWNyZXQ=")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204BasicAuthWithConfig returns basic auth middleware with config.
mw := webmiddleware.BasicAuthWithConfig(webmiddleware.BasicAuthConfig{
Realm: "Example",
Validator: func(user, pass string, c web.Context) (bool, error) { return true, nil },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("WWW-Authenticate"))
// 401 basic realm=\"Example\"CSRF enables token-based CSRF protection.
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := webmiddleware.CSRF()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Set-Cookie") != "")
// trueCSRFWithConfig enables token-based CSRF protection with config.
mw := webmiddleware.CSRFWithConfig(webmiddleware.CSRFConfig{CookieName: "_csrf"})
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(strings.Contains(ctx.Response().Header().Get("Set-Cookie"), "_csrf="))
// trueCreateExtractors creates extractors from a lookup definition.
extractors, err := webmiddleware.CreateExtractors("header:X-API-Key,query:token")
fmt.Println(err == nil, len(extractors))
// true 2KeyAuth returns key auth middleware.
mw := webmiddleware.KeyAuth(func(key string, c web.Context) (bool, error) {
return key == "demo-key", nil
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer demo-key")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204KeyAuthWithConfig returns key auth middleware with config.
mw := webmiddleware.KeyAuthWithConfig(webmiddleware.KeyAuthConfig{
Validator: func(key string, c web.Context) (bool, error) { return true, nil },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 400Compress is an alias for Gzip to match the checklist naming.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Compress()(func(c web.Context) error {
return c.Text(http.StatusOK, "hello")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding"))
// gzipDecompress decompresses gzip-encoded request bodies.
var body string
compressed := &bytes.Buffer{}
gz := gzip.NewWriter(compressed)
_, _ = gz.Write([]byte("hello"))
_ = gz.Close()
req := httptest.NewRequest(http.MethodPost, "/", compressed)
req.Header.Set("Content-Encoding", webmiddleware.GZIPEncoding)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Decompress()(func(c web.Context) error {
data, _ := io.ReadAll(c.Request().Body)
body = string(data)
return c.NoContent(http.StatusNoContent)
})
_ = handler(ctx)
fmt.Println(body, ctx.Request().Header.Get("Content-Encoding"))
// helloDecompressWithConfig decompresses gzip-encoded request bodies with config.
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("plain"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.DecompressWithConfig(webmiddleware.DecompressConfig{})(func(c web.Context) error {
data, _ := io.ReadAll(c.Request().Body)
fmt.Println(string(data))
return nil
})
_ = handler(ctx)
// plainGzip compresses responses with gzip.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.Gzip()(func(c web.Context) error {
return c.Text(http.StatusOK, "hello")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding"))
// gzipGzipWithConfig compresses responses with gzip and config.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.GzipWithConfig(webmiddleware.GzipConfig{MinLength: 256})(func(c web.Context) error {
return c.Text(http.StatusOK, "short")
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Content-Encoding") == "")
// trueMethodFromForm gets an override method from a form field.
getter := webmiddleware.MethodFromForm("_method")
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("_method=DELETE"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx := webtest.NewContext(req, nil, "/", nil)
fmt.Println(getter(ctx))
// DELETEMethodFromHeader gets an override method from a request header.
getter := webmiddleware.MethodFromHeader("X-HTTP-Method-Override")
ctx := webtest.NewContext(nil, nil, "/", nil)
ctx.Request().Header.Set("X-HTTP-Method-Override", "PATCH")
fmt.Println(getter(ctx))
// PATCHMethodFromQuery gets an override method from a query parameter.
getter := webmiddleware.MethodFromQuery("_method")
req := httptest.NewRequest(http.MethodPost, "/?_method=PUT", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
fmt.Println(getter(ctx))
// PUTMethodOverride returns method override middleware.
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set("X-HTTP-Method-Override", http.MethodPatch)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.MethodOverride()(func(c web.Context) error {
fmt.Println(c.Method())
return nil
})
_ = handler(ctx)
// PATCHMethodOverrideWithConfig returns method override middleware with config.
req := httptest.NewRequest(http.MethodPost, "/?_method=DELETE", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.MethodOverrideWithConfig(webmiddleware.MethodOverrideConfig{
Getter: webmiddleware.MethodFromQuery("_method"),
})(func(c web.Context) error {
fmt.Println(c.Method())
return nil
})
_ = handler(ctx)
// DELETEAddTrailingSlash adds a trailing slash to the request path.
req := httptest.NewRequest(http.MethodGet, "/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
handler := webmiddleware.AddTrailingSlash()(func(c web.Context) error {
fmt.Println(c.Request().URL.Path)
return nil
})
_ = handler(ctx)
// /docs/AddTrailingSlashWithConfig returns trailing-slash middleware with config.
req := httptest.NewRequest(http.MethodGet, "/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
handler := webmiddleware.AddTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{RedirectCode: 308})(func(c web.Context) error {
return c.NoContent(204)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 308 /docs/RemoveTrailingSlash removes the trailing slash from the request path.
req := httptest.NewRequest(http.MethodGet, "/docs/", nil)
ctx := webtest.NewContext(req, nil, "/docs/", nil)
handler := webmiddleware.RemoveTrailingSlash()(func(c web.Context) error {
fmt.Println(c.Request().URL.Path)
return nil
})
_ = handler(ctx)
// /docsRemoveTrailingSlashWithConfig returns remove-trailing-slash middleware with config.
req := httptest.NewRequest(http.MethodGet, "/docs/", nil)
ctx := webtest.NewContext(req, nil, "/docs/", nil)
handler := webmiddleware.RemoveTrailingSlashWithConfig(webmiddleware.TrailingSlashConfig{RedirectCode: 308})(func(c web.Context) error {
return c.NoContent(204)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 308 /docsRewrite rewrites the request path using wildcard rules.
req := httptest.NewRequest(http.MethodGet, "/old/users", nil)
ctx := webtest.NewContext(req, nil, "/old/*", nil)
handler := webmiddleware.Rewrite(map[string]string{"/old/*": "/new/$1"})(func(c web.Context) error {
fmt.Println(c.Request().URL.Path)
return nil
})
_ = handler(ctx)
// /new/usersRewriteWithConfig rewrites the request path using wildcard and regex rules.
req := httptest.NewRequest(http.MethodGet, "/old/users", nil)
ctx := webtest.NewContext(req, nil, "/old/*", nil)
handler := webmiddleware.RewriteWithConfig(webmiddleware.RewriteConfig{
Rules: map[string]string{"/old/*": "/v2/$1"},
})(func(c web.Context) error {
fmt.Println(c.Request().URL.Path)
return nil
})
_ = handler(ctx)
// /v2/usersBodyDump captures request and response payloads.
var captured string
mw := webmiddleware.BodyDump(func(c web.Context, reqBody, resBody []byte) {
captured = fmt.Sprintf("%s -> %s", string(reqBody), string(resBody))
})
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("ping"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusOK, "pong") })
_ = handler(ctx)
fmt.Println(captured)
// ping -> pongBodyDumpWithConfig captures request and response payloads with config.
mw := webmiddleware.BodyDumpWithConfig(webmiddleware.BodyDumpConfig{
Handler: func(c web.Context, reqBody, resBody []byte) { fmt.Println(string(resBody)) },
})
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/", nil), nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusOK, "ok") })
_ = handler(ctx)
// okBodyLimit returns middleware that limits request body size.
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("hello"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.BodyLimit("2B")(func(c web.Context) error {
return c.NoContent(http.StatusOK)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 413BodyLimitWithConfig returns body limit middleware with config.
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("ok"))
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.BodyLimitWithConfig(webmiddleware.BodyLimitConfig{Limit: "2KB"})(func(c web.Context) error {
return c.NoContent(http.StatusNoContent)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204ErrorBodyDump captures response bodies for non-2xx and non-3xx responses.
var captured string
mw := webmiddleware.ErrorBodyDump(func(c web.Context, status int, body []byte) {
captured = fmt.Sprintf("%d:%s", status, string(body))
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusBadRequest, "nope") })
_ = handler(ctx)
fmt.Println(captured)
// 400:nopeErrorBodyDumpWithConfig captures response bodies for non-success responses with config.
mw := webmiddleware.ErrorBodyDumpWithConfig(webmiddleware.ErrorBodyDumpConfig{
Handler: func(c web.Context, status int, body []byte) { fmt.Println(status) },
})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.Text(http.StatusInternalServerError, "boom") })
_ = handler(ctx)
// 500NewRandomBalancer creates a random proxy balancer.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
fmt.Println(balancer.Next(nil).URL.Host)
// localhost:8080NewRoundRobinBalancer creates a round-robin proxy balancer.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRoundRobinBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
fmt.Println(balancer.Next(nil).URL.Host)
// localhost:8080Proxy creates a proxy middleware.
target, _ := url.Parse("http://localhost:8080")
balancer := webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}})
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = webmiddleware.Proxy(balancer)(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Get("target").(*webmiddleware.ProxyTarget).URL.Host)
// localhost:8080ProxyWithConfig creates a proxy middleware with config.
target, _ := url.Parse("http://localhost:8080")
mw := webmiddleware.ProxyWithConfig(webmiddleware.ProxyConfig{
Balancer: webmiddleware.NewRandomBalancer([]*webmiddleware.ProxyTarget{{URL: target}}),
})
req := httptest.NewRequest(http.MethodGet, "/old/path", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = mw(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Get("target").(*webmiddleware.ProxyTarget).URL.Host)
// localhost:8080NewRateLimiterMemoryStore creates an in-memory rate limiter store.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
allowed1, _ := store.Allow("192.0.2.1")
allowed2, _ := store.Allow("192.0.2.1")
fmt.Println(allowed1, allowed2)
// true falseNewRateLimiterMemoryStoreWithConfig creates an in-memory rate limiter store with config.
store := webmiddleware.NewRateLimiterMemoryStoreWithConfig(webmiddleware.RateLimiterMemoryStoreConfig{Rate: rate.Every(time.Second)})
allowed, _ := store.Allow("192.0.2.1")
fmt.Println(allowed)
// trueRateLimiter creates a rate limiting middleware.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
handler := webmiddleware.RateLimiter(store)(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
req1.RemoteAddr = "192.0.2.10:1234"
ctx1 := webtest.NewContext(req1, nil, "/", nil)
_ = handler(ctx1)
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
req2.RemoteAddr = "192.0.2.10:1234"
ctx2 := webtest.NewContext(req2, nil, "/", nil)
_ = handler(ctx2)
fmt.Println(ctx1.StatusCode(), ctx2.StatusCode())
// 204 429Allow checks whether the given identifier is allowed through.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
allowed, err := store.Allow("127.0.0.1")
fmt.Println(err == nil, allowed)
// true trueRateLimiterWithConfig creates a rate limiting middleware with config.
store := webmiddleware.NewRateLimiterMemoryStore(rate.Every(time.Second))
mw := webmiddleware.RateLimiterWithConfig(webmiddleware.RateLimiterConfig{Store: store})
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusAccepted) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 202HTTPSNonWWWRedirect redirects to https without www.
req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSNonWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// https://example.com/docsHTTPSNonWWWRedirectWithConfig returns HTTPS non-WWW redirect middleware with config.
req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSNonWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307HTTPSRedirect redirects http requests to https.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode(), ctx.Response().Header().Get("Location"))
// 301 https://example.com/docsHTTPSRedirectWithConfig returns HTTPS redirect middleware with config.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307HTTPSWWWRedirect redirects to https + www.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// https://www.example.com/docsHTTPSWWWRedirectWithConfig returns HTTPS+WWW redirect middleware with config.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.HTTPSWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307NonWWWRedirect redirects to the non-www host.
req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.NonWWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// http://example.com/docsNonWWWRedirectWithConfig returns non-WWW redirect middleware with config.
req := httptest.NewRequest(http.MethodGet, "http://www.example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.NonWWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307WWWRedirect redirects to the www host.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.WWWRedirect()(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.Response().Header().Get("Location"))
// http://www.example.com/docsWWWRedirectWithConfig returns WWW redirect middleware with config.
req := httptest.NewRequest(http.MethodGet, "http://example.com/docs", nil)
ctx := webtest.NewContext(req, nil, "/docs", nil)
_ = webmiddleware.WWWRedirectWithConfig(webmiddleware.RedirectConfig{Code: http.StatusTemporaryRedirect})(func(c web.Context) error { return nil })(ctx)
fmt.Println(ctx.StatusCode())
// 307Recover returns middleware that recovers panics from the handler chain.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Recover()(func(c web.Context) error {
panic("boom")
})
fmt.Println(handler(ctx) != nil)
// trueRecoverWithConfig returns recover middleware with config.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.RecoverWithConfig(webmiddleware.RecoverConfig{DisableErrorHandler: true})(func(c web.Context) error {
panic("boom")
})
fmt.Println(handler(ctx) != nil)
// trueContextTimeout sets a timeout on the request context.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.ContextTimeout(2 * time.Second)(func(c web.Context) error {
fmt.Println(c.Request().Context().Err() == nil)
return nil
})
_ = handler(ctx)
// trueContextTimeoutWithConfig sets a timeout on the request context with config.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.ContextTimeoutWithConfig(webmiddleware.ContextTimeoutConfig{Timeout: time.Second})(func(c web.Context) error {
fmt.Println(c.Request().Context().Err() == nil)
return nil
})
_ = handler(ctx)
// trueDefaultSkipper always runs the middleware.
fmt.Println(webmiddleware.DefaultSkipper(nil))
// falseRequestID returns middleware that sets a request id header and context value.
mw := webmiddleware.RequestID()
handler := mw(func(c web.Context) error {
_ = c.Get("request_id")
return c.NoContent(http.StatusOK)
})
ctx := webtest.NewContext(nil, nil, "/", nil)
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Request-ID") != "")
// true
// trueRequestIDWithConfig returns RequestID middleware with config.
mw := webmiddleware.RequestIDWithConfig(webmiddleware.RequestIDConfig{
Generator: func() string { return "fixed-id" },
})
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusOK) })
ctx := webtest.NewContext(nil, nil, "/", nil)
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Request-ID"))
// fixed-idRequestLoggerWithConfig returns request logger middleware with config.
var loggedURI string
mw := webmiddleware.RequestLoggerWithConfig(webmiddleware.RequestLoggerConfig{
LogValuesFunc: func(c web.Context, values webmiddleware.RequestLoggerValues) error {
loggedURI = values.URI
return nil
},
})
req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
ctx := webtest.NewContext(req, nil, "/users/:id", webtest.PathParams{"id": "42"})
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusAccepted) })
_ = handler(ctx)
fmt.Println(loggedURI, ctx.StatusCode())
// /users/42 202Timeout returns a response-timeout middleware.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Timeout()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 204TimeoutWithConfig returns a response-timeout middleware with config.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.TimeoutWithConfig(webmiddleware.TimeoutConfig{Timeout: time.Second})(func(c web.Context) error {
return c.NoContent(http.StatusAccepted)
})
_ = handler(ctx)
fmt.Println(ctx.StatusCode())
// 202CORS returns Cross-Origin Resource Sharing middleware.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://example.com")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := webmiddleware.CORS()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Access-Control-Allow-Origin"))
// *CORSWithConfig returns CORS middleware with config.
mw := webmiddleware.CORSWithConfig(webmiddleware.CORSConfig{AllowOrigins: []string{"https://example.com"}})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Origin", "https://example.com")
ctx := webtest.NewContext(req, nil, "/", nil)
handler := mw(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Access-Control-Allow-Origin"))
// https://example.comSecure sets security-oriented response headers.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.Secure()(func(c web.Context) error { return c.NoContent(http.StatusOK) })
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("X-Frame-Options"))
// SAMEORIGINSecureWithConfig sets security-oriented response headers with config.
ctx := webtest.NewContext(nil, nil, "/", nil)
handler := webmiddleware.SecureWithConfig(webmiddleware.SecureConfig{ReferrerPolicy: "same-origin"})(func(c web.Context) error {
return c.NoContent(http.StatusOK)
})
_ = handler(ctx)
fmt.Println(ctx.Response().Header().Get("Referrer-Policy"))
// same-originStatic serves static content from the provided root.
dir, _ := os.MkdirTemp("", "web-static-*")
defer os.RemoveAll(dir)
_ = os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello"), 0o644)
req := httptest.NewRequest(http.MethodGet, "/hello.txt", nil)
ctx := webtest.NewContext(req, nil, "/hello.txt", nil)
_ = webmiddleware.Static(dir)(func(c web.Context) error { return c.NoContent(http.StatusNotFound) })(ctx)
fmt.Println(strings.TrimSpace(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String()))
// helloStaticWithConfig serves static content using config.
dir, _ := os.MkdirTemp("", "web-static-*")
defer os.RemoveAll(dir)
_ = os.WriteFile(filepath.Join(dir, "index.html"), []byte("<h1>home</h1>"), 0o644)
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := webtest.NewContext(req, nil, "/", nil)
_ = webmiddleware.StaticWithConfig(webmiddleware.StaticConfig{Root: dir})(func(c web.Context) error { return c.NoContent(http.StatusNotFound) })(ctx)
fmt.Println(strings.TrimSpace(ctx.ResponseWriter().(*httptest.ResponseRecorder).Body.String()))
// <h1>home</h1>Default returns the package-level Prometheus metrics instance.
fmt.Println(webprometheus.Default() == webprometheus.Default())
// trueHandler returns the package-level Prometheus scrape handler.
registry := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "demo_total", Help: "demo counter"})
registry.MustRegister(counter)
counter.Inc()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: registry})
recorder := httptest.NewRecorder()
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/metrics", nil), recorder, "/metrics", nil)
_ = metrics.Handler()(ctx)
fmt.Println(strings.Contains(recorder.Body.String(), "demo_total"))
// trueHandler exposes the configured Prometheus metrics as a web.Handler.
registry := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{Name: "demo_total", Help: "demo counter"})
registry.MustRegister(counter)
counter.Inc()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: registry})
recorder := httptest.NewRecorder()
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/metrics", nil), recorder, "/metrics", nil)
_ = metrics.Handler()(ctx)
fmt.Println(strings.Contains(recorder.Body.String(), "demo_total"))
// trueMiddleware records Prometheus metrics for each request.
registry := prometheus.NewRegistry()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: registry, Gatherer: registry, Namespace: "example"})
handler := metrics.Middleware()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/healthz", nil), nil, "/healthz", nil)
_ = handler(ctx)
out := &bytes.Buffer{}
_ = webprometheus.WriteGatheredMetrics(out, registry)
fmt.Println(strings.Contains(out.String(), "example_requests_total"))
// trueMiddleware returns the package-level Prometheus middleware.
registry := prometheus.NewRegistry()
metrics, _ := webprometheus.New(webprometheus.Config{Registerer: registry, Gatherer: registry, Namespace: "example"})
handler := metrics.Middleware()(func(c web.Context) error { return c.NoContent(http.StatusNoContent) })
ctx := webtest.NewContext(httptest.NewRequest(http.MethodGet, "/healthz", nil), nil, "/healthz", nil)
_ = handler(ctx)
out := &bytes.Buffer{}
_ = webprometheus.WriteGatheredMetrics(out, registry)
fmt.Println(strings.Contains(out.String(), "example_requests_total"))
// trueMustNew creates a Metrics instance and panics on registration errors.
metrics := webprometheus.MustNew(webprometheus.Config{Registerer: prometheus.NewRegistry(), Gatherer: prometheus.NewRegistry()})
fmt.Println(metrics != nil)
// trueNew creates a Metrics instance backed by Prometheus collectors.
metrics, err := webprometheus.New(webprometheus.Config{Namespace: "app"})
_ = metrics
fmt.Println(err == nil)
// trueRunPushGatewayGatherer starts pushing collected metrics until the context finishes.
err := webprometheus.RunPushGatewayGatherer(context.Background(), webprometheus.PushGatewayConfig{})
fmt.Println(err != nil)
// trueWriteGatheredMetrics gathers collected metrics and writes them to the given writer.
var buf bytes.Buffer
err := webprometheus.WriteGatheredMetrics(&buf, prometheus.NewRegistry())
fmt.Println(err == nil)
// trueBuildRouteEntries builds a sorted slice of route entries from registered groups and extra entries.
entries := web.BuildRouteEntries([]web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
}),
})
fmt.Println(entries[0].Path, entries[0].Methods[0])
// /api/healthz GETRenderRouteTable renders a route table using simple ASCII borders and ANSI colors.
table := web.RenderRouteTable([]web.RouteEntry{{
Path: "/api/healthz",
Handler: "monitoring.Healthz",
Methods: []string{"GET"},
}})
fmt.Println(strings.Contains(table, "/api/healthz"))
// trueMountRouter applies mount-style router configuration in declaration order.
adapter := echoweb.New()
err := web.MountRouter(adapter.Router(), []web.RouterMount{
func(r web.Router) error {
r.GET("/healthz", func(c web.Context) error { return nil })
return nil
},
})
fmt.Println(err == nil)
// trueNewRoute creates a new route using the app-facing web handler contract directly.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusOK)
})
fmt.Println(route.Method(), route.Path())
// GET /healthzNewRouteGroup wraps routes and their accompanied web middleware.
group := web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
})
fmt.Println(group.RoutePrefix(), len(group.Routes()))
// /api 1NewWebSocketRoute creates a websocket route using the app-facing websocket handler contract.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error {
return nil
})
fmt.Println(route.IsWebSocket())
// trueRegisterRoutes registers route groups onto a router.
adapter := echoweb.New()
groups := []web.RouteGroup{
web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
}),
}
err := web.RegisterRoutes(adapter.Router(), groups)
fmt.Println(err == nil)
// trueHandler returns the route handler.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error {
return c.NoContent(http.StatusCreated)
})
ctx := webtest.NewContext(nil, nil, "/healthz", nil)
_ = route.Handler()(ctx)
fmt.Println(ctx.StatusCode())
// 201HandlerName returns the original handler name for route reporting.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil })
fmt.Println(route.HandlerName() != "")
// trueIsWebSocket reports whether this route upgrades to a websocket connection.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error { return nil })
fmt.Println(route.IsWebSocket())
// trueMethod returns the HTTP method.
route := web.NewRoute(http.MethodPost, "/users", func(c web.Context) error { return nil })
fmt.Println(route.Method())
// POSTMiddlewareNames returns original middleware names for route reporting.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth")
fmt.Println(route.MiddlewareNames()[0])
// authMiddlewares returns the route middleware slice.
route := web.NewRoute(
http.MethodGet,
"/healthz",
func(c web.Context) error { return nil },
func(next web.Handler) web.Handler { return next },
)
fmt.Println(len(route.Middlewares()))
// 1Path returns the path of the route.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil })
fmt.Println(route.Path())
// /healthzWebSocketHandler returns the websocket route handler.
route := web.NewWebSocketRoute("/ws", func(c web.Context, conn web.WebSocketConn) error {
c.Set("ready", true)
return nil
})
ctx := webtest.NewContext(nil, nil, "/ws", nil)
err := route.WebSocketHandler()(ctx, nil)
fmt.Println(err == nil, ctx.Get("ready"))
// true trueWithMiddlewareNames attaches reporting-only middleware names to the route.
route := web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }).WithMiddlewareNames("auth", "trace")
fmt.Println(len(route.MiddlewareNames()))
// 2MiddlewareNames returns original middleware names for route reporting.
group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth")
fmt.Println(group.MiddlewareNames()[0])
// authMiddlewares returns the middleware slice for the group.
group := web.NewRouteGroup("/api", nil, func(next web.Handler) web.Handler { return next })
fmt.Println(len(group.Middlewares()))
// 1RoutePrefix returns the group prefix.
group := web.NewRouteGroup("/api", nil)
fmt.Println(group.RoutePrefix())
// /apiRoutes returns the routes in the group.
group := web.NewRouteGroup("/api", []web.Route{
web.NewRoute(http.MethodGet, "/healthz", func(c web.Context) error { return nil }),
})
fmt.Println(len(group.Routes()))
// 1WithMiddlewareNames attaches reporting-only middleware names to the group.
group := web.NewRouteGroup("/api", nil).WithMiddlewareNames("auth", "trace")
fmt.Println(len(group.MiddlewareNames()))
// 2NewContext creates a new test context around the provided request/recorder pair.
req := httptest.NewRequest(http.MethodGet, "/users/42?expand=roles", nil)
ctx := webtest.NewContext(req, nil, "/users/:id", webtest.PathParams{"id": "42"})
fmt.Println(ctx.Param("id"), ctx.Query("expand"))
// 42 roles