Say it out loud: aichteeteapee.
Go on. Sound it out. Aitch-tee-tee-pee.
Yeah. That’s HTTP. I named my Go HTTP library by spelling “HTTP” phonetically and published it with a straight face. And the WebSocket package? It’s called dabluveees. Dub-ell-vee-ess. Double-v-s. WS. I did it twice.
But wait. The HTTP server package inside it? serbewr. Pronounced “server”. The proxy package? prawxxey. Pronounced “proxy”.
I did it four times.
No regrets.
Okay but what is it
aichteeteapee is a Go library that wraps net/http and gives you everything you’d otherwise spend the first three hours of every project setting up. You know the ritual. You’ve done it forty times. Make the mux. Configure the timeouts. Figure out where to put CORS again. Wire up graceful shutdown. Remember that one timeout you always forget. Add the panic recovery middleware. Copy the request ID snippet from that other project. Die a little inside.
That’s not hard. It’s just tedious as hell, and you do it for every single project, and you’re going to do it again next month for the next one.
The usage
package main
import (
"context"
"net/http"
"github.com/psyb0t/aichteeteapee"
"github.com/psyb0t/aichteeteapee/serbewr"
"github.com/psyb0t/aichteeteapee/serbewr/middleware"
)
func main() {
srv, _ := serbewr.New()
router := &serbewr.Router{
GlobalMiddlewares: []middleware.Middleware{
middleware.RequestID(),
middleware.Logger(),
middleware.Recovery(),
middleware.SecurityHeaders(),
middleware.CORS(),
},
Groups: []serbewr.GroupConfig{{
Path: "/",
Routes: []serbewr.RouteConfig{{
Method: http.MethodGet,
Path: "/hello",
Handler: func(w http.ResponseWriter, r *http.Request) {
aichteeteapee.WriteJSON(w, http.StatusOK, map[string]string{"msg": "hi"})
},
}},
}},
}
srv.Start(context.Background(), router)
}That’s it. You have request logging, panic recovery, CORS, HSTS/CSP/X-Frame-Options, request ID tracing, graceful shutdown, and sane timeouts. You wrote none of it. Import serbewr — pronounced “server”, obviously — and call New().
The root aichteeteapee package gives you the utilities: WriteJSON, GetClientIP, GetRequestID, every HTTP header name as a constant so you stop hardcoding strings, error codes for every HTTP status, pre-built error response structs. The stuff you reach for in every handler.
The error sentinel set keeps growing as I rip them out of every client package I’ve ever written. v1.8 and v1.9 added four more categories on top of the existing HTTP-status sentinels (ErrBadRequest, ErrUnauthorized, ErrNotFound, etc.):
- Response handling —
ErrInvalidResponse,ErrEmptyResponse,ErrUnexpectedResponseStatus. For when upstream gave you something that wasn’t an HTTP-status problem but still isn’t usable. - Anti-bot / scraping —
ErrBotDetected,ErrChallengeBlocked,ErrCaptchaRequired. For HTTP clients that have to navigate Cloudflare interstitials, Akamai bot manager, hCaptcha walls, and every other “are you a human” gate the modern web throws at automation. - API client —
ErrAPIError,ErrAPIKeyNotSet,ErrNilRequestBody. The generic boilerplate every typed API client ends up needing. - TLS & Security —
ErrTLSCertFileNotSpecified,ErrTLSKeyFileNotSpecified. So a server starting up without a cert path doesn’t have to invent its own error.
Same pattern as before — exported var, wrap with fmt.Errorf("...: %w", err), match downstream with errors.Is(). Stops every package in the dependency tree from inventing its own “invalid response” string and lets clients compare against a single shared sentinel. Already wired into the mt5-httpapi Go client, proxq, and a bunch of internal scrapers.
Security
Secure by default. CORS blocks unknown origins. WebSocket validates Origin against Host. File uploads sanitize filenames — no path traversal. Uploaded files get 0600 permissions and won’t overwrite existing files. Proxy responses are size-limited. Sensitive headers are filtered from echo responses.
For local dev when you want none of that:
aichteeteapee.FuckSecurity()CORS allows all origins. WebSocket accepts any origin. One call. When you’re done fucking around:
aichteeteapee.UnfuckSecurity()If you need per-component overrides without going full anarchy:
middleware.CORS(middleware.WithAllowAllOrigins())
wshub.UpgradeHandler(hub,
wshub.WithUpgradeHandlerCheckOrigin(
aichteeteapee.GetPermissiveWebSocketCheckOrigin,
),
)Middleware
The built-ins aren’t toy implementations. CORS handles preflight correctly. The security headers middleware lets you enable or disable individual headers — you don’t have to take all-or-nothing defaults if you have a custom CSP or handle XSS at the app level. Timeout middleware ships with presets (short: 5s, default: 30s, long: 5min). Content-type enforcement for APIs:
serbewr.GroupConfig{
Path: "/api",
Middlewares: []middleware.Middleware{
middleware.EnforceRequestContentTypeJSON(),
},
}Anything that posts non-JSON gets rejected before it ever hits your handler. The middleware chain builds the logger progressively — RequestID adds requestId to context, Logger adds method/path/IP, and downstream code calls slogging.GetLogger(ctx) and gets all fields automatically. No explicit logger passing anywhere.
prawxxey — the proxy package
serbewr/prawxxey. Spelled how it sounds. Pronounced like a normal person would say “proxy” if they were also insane.
Forward requests upstream with optional response caching, hop-by-hop header stripping, response size limits, and deterministic request fingerprinting. Cache key is derived from the request so identical requests hit cache instead of your upstream. Useful when you’re proxying external APIs with rate limits and don’t want every request burning quota. Responses include an X-Cache-Status header — HIT or MISS — so you can see exactly what’s being served from cache vs hitting upstream.
dabluveees — the WebSocket stuff
serbewr/dabluvee-es. Dub-ell-vee-ess. WS. You get it.
The wshub package inside it gives you a full multi-client WebSocket hub. Create a hub, register event handlers by type, it manages connection lifecycle, broadcasting, and per-connection metadata. You’re not reinventing the client map and the select loop for the eighth time.
chatHub := wshub.NewHub("chat")
chatHub.RegisterEventHandlers(map[dabluveees.EventType]wshub.EventHandler{
EventTypeChatMessage: func(hub wshub.Hub, client *wshub.Client, event *dabluveees.Event) error {
event.Metadata.Set("timestamp", time.Now().Unix())
hub.BroadcastToAll(event)
return nil
},
})
{Method: "GET", Path: "/ws/chat", Handler: wshub.UpgradeHandler(chatHub)}Then there’s wsunixbridge. It bridges WebSocket connections to Unix domain sockets — your WebSocket client connects, a pair of Unix sockets appear server-side, any process on the machine can now talk to that browser client by reading and writing those sockets. Shell scripts, Python scripts, legacy binaries that know nothing about WebSockets can suddenly speak to one.
echo "hello from shell" | socat - UNIX-CONNECT:./sockets/connection-id_inputecho — the Echo wrapper for OpenAPI-first APIs
There’s a subpackage simply called echo (yeah, I let one have a normal name) that wraps the Labstack Echo framework for the case where you’ve designed your API with an OpenAPI spec and want the server to actually enforce it. Hand it the YAML bytes of your spec at construction time and you get back a server that auto-serves the spec, mounts Swagger UI, validates every incoming request against the schema, and handles Bearer auth via the security definitions in the spec.
package main
import (
"context"
_ "embed"
"github.com/psyb0t/aichteeteapee/echo"
echomw "github.com/psyb0t/aichteeteapee/echo/middleware"
)
//go:embed openapi.yaml
var oasYAML []byte
func main() {
oas, _ := openapi3.NewLoader().LoadFromData(oasYAML)
auth := echomw.BearerAuth(func(ctx context.Context, token string) error {
if token != "supersecret" {
return errors.New("invalid token")
}
return nil
})
mws := echomw.CreateDefaultAPIMiddleware(oas, auth)
srv, _ := echo.New("/api/v1", oasYAML, mws)
// register your handlers on srv.RouterGroup ...
srv.Start(context.Background())
}That’s it. GET /api/v1/openapi.yaml serves the spec, GET /api/v1/swagger-ui/ renders the interactive docs, and every request is validated by oapi-codegen‘s middleware against the spec before it ever reaches a handler — wrong content type, missing required field, unknown parameter, malformed body all get rejected with a proper 4xx and your handlers stay clean. Endpoints with security in the spec route through your AuthFunc; endpoints with security: [] bypass auth entirely. Listen address and the OAS/Swagger UI paths come from env vars (HTTP_ECHO_LISTENADDRESS, HTTP_ECHO_OASPATH, HTTP_ECHO_SWAGGERUIPATH), graceful shutdown is wired to the context you pass to Start().
The oapi-codegen subpackage holds the validator middleware itself if you want to use it directly with vanilla Echo without the wrapper.
The naming, one final time
- HTTP →
aichteeteapee - server →
serbewr - proxy →
prawxxey - WS →
dabluveees
The README says: “Pronounced ‘HTTP’. The name is the whole joke. Moving on.”
It did not move on. There are three more packages named the same way.
I stand by every single one of them.
github.com/psyb0t/aichteeteapee