diff --git a/HWS.md b/HWS.md new file mode 100644 index 0000000..13279da --- /dev/null +++ b/HWS.md @@ -0,0 +1,1162 @@ +# HWS + +A lightweight, opinionated HTTP web server framework for Go built on top of the standard library's `net/http`. + +## Overview + +`HWS` (H Web Server) provides a structured approach to building HTTP servers in Go. It leverages Go 1.22+ routing patterns, provides built-in middleware for common tasks, and offers a clean API for handling errors and logging. + +## Installation + +```bash +go get git.haelnorr.com/h/golib/hws +``` + +## Key Concepts + +### Design Philosophy + +1. **Built on Standards**: Uses `net/http` with Go 1.22+ routing patterns +2. **Opinionated Defaults**: Sensible defaults for timeouts, logging, and compression +3. **Structured Errors**: Type-safe error handling with customizable error pages +4. **Middleware First**: Built-in middleware for timing, logging, and compression +5. **Production Ready**: Health checks, graceful shutdown, and timeout controls + +### Server Lifecycle + +1. **Create** server with configuration +2. **Configure** logger, error pages, and ignored paths +3. **Define** routes with methods and handlers +4. **Add** routes to server +5. **Add** middleware (required before start) +6. **Start** server with context +7. **Wait** for server to be ready +8. **Shutdown** gracefully when done + +## Quick Start + +### Basic Server + +```go +package main + +import ( + "context" + "git.haelnorr.com/h/golib/hws" + "net/http" +) + +func main() { + // Create server + server, err := hws.NewServer(&hws.Config{ + Host: "127.0.0.1", + Port: 3000, + GZIP: true, + }) + if err != nil { + panic(err) + } + + // Define routes + routes := []hws.Route{ + { + Path: "/", + Method: hws.MethodGET, + Handler: http.HandlerFunc(homeHandler), + }, + } + + // Add routes and middleware + server.AddRoutes(routes...) + server.AddMiddleware() + + // Start server + ctx := context.Background() + server.Start(ctx) + + // Wait for ready + <-server.Ready() + + // Block forever + select {} +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) +} +``` + +## Configuration + +### Config Struct + +```go +type Config struct { + Host string // Host to listen on + Port uint64 // Port to listen on + TrustedHost string // Trusted hostname/domain + GZIP bool // Enable GZIP compression + ReadHeaderTimeout time.Duration // Timeout for reading headers + WriteTimeout time.Duration // Timeout for writing responses + IdleTimeout time.Duration // Timeout for idle connections +} +``` + +### Using Environment Variables + +Load configuration from environment variables with `ConfigFromEnv()`: + +```go +config, err := hws.ConfigFromEnv() +if err != nil { + return err +} + +server, err := hws.NewServer(config) +``` + +Environment variables: + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `HWS_HOST` | string | `127.0.0.1` | Host to bind to | +| `HWS_PORT` | uint64 | `3000` | Port to listen on | +| `HWS_TRUSTED_HOST` | string | Same as `HWS_HOST` | Trusted hostname | +| `HWS_GZIP` | bool | `false` | Enable GZIP compression | +| `HWS_READ_HEADER_TIMEOUT` | int | `2` | Header read timeout (seconds) | +| `HWS_WRITE_TIMEOUT` | int | `10` | Write timeout (seconds) | +| `HWS_IDLE_TIMEOUT` | int | `120` | Idle timeout (seconds) | + +### Manual Configuration + +```go +config := &hws.Config{ + Host: "0.0.0.0", + Port: 8080, + TrustedHost: "example.com", + GZIP: true, + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, +} + +server, err := hws.NewServer(config) +``` + +### Default Values + +If fields are not specified, the following defaults are applied: + +- `Host`: `127.0.0.1` +- `Port`: `3000` +- `TrustedHost`: Same as `Host` +- `GZIP`: `false` +- `ReadHeaderTimeout`: `2 seconds` +- `WriteTimeout`: `10 seconds` +- `IdleTimeout`: `120 seconds` + +## Routing + +### Route Definition + +Routes use Go 1.22+ routing patterns with explicit HTTP methods: + +```go +type Route struct { + Path string // URL path pattern + Method Method // HTTP method + Handler http.Handler // Request handler +} +``` + +### HTTP Methods + +Available method constants: + +```go +hws.MethodGET +hws.MethodPOST +hws.MethodPUT +hws.MethodPATCH +hws.MethodDELETE +hws.MethodHEAD +hws.MethodOPTIONS +hws.MethodCONNECT +hws.MethodTRACE +``` + +### Basic Routes + +```go +routes := []hws.Route{ + { + Path: "/", + Method: hws.MethodGET, + Handler: http.HandlerFunc(homeHandler), + }, + { + Path: "/about", + Method: hws.MethodGET, + Handler: http.HandlerFunc(aboutHandler), + }, + { + Path: "/contact", + Method: hws.MethodPOST, + Handler: http.HandlerFunc(contactHandler), + }, +} + +err := server.AddRoutes(routes...) +``` + +### Path Parameters + +Use Go 1.22+ path parameters: + +```go +routes := []hws.Route{ + { + Path: "/users/{id}", + Method: hws.MethodGET, + Handler: http.HandlerFunc(getUserHandler), + }, + { + Path: "/posts/{post_id}/comments/{comment_id}", + Method: hws.MethodGET, + Handler: http.HandlerFunc(getCommentHandler), + }, +} + +func getUserHandler(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + w.Write([]byte("User ID: " + id)) +} + +func getCommentHandler(w http.ResponseWriter, r *http.Request) { + postID := r.PathValue("post_id") + commentID := r.PathValue("comment_id") + w.Write([]byte(fmt.Sprintf("Post: %s, Comment: %s", postID, commentID))) +} +``` + +### Wildcard Routes + +```go +routes := []hws.Route{ + { + Path: "/static/", + Method: hws.MethodGET, + Handler: http.StripPrefix("/static/", staticHandler), + }, + { + Path: "/api/", + Method: hws.MethodGET, + Handler: apiHandler, + }, +} +``` + +### REST API Routes + +```go +routes := []hws.Route{ + // List users + { + Path: "/api/users", + Method: hws.MethodGET, + Handler: http.HandlerFunc(listUsersHandler), + }, + // Create user + { + Path: "/api/users", + Method: hws.MethodPOST, + Handler: http.HandlerFunc(createUserHandler), + }, + // Get user + { + Path: "/api/users/{id}", + Method: hws.MethodGET, + Handler: http.HandlerFunc(getUserHandler), + }, + // Update user + { + Path: "/api/users/{id}", + Method: hws.MethodPUT, + Handler: http.HandlerFunc(updateUserHandler), + }, + // Delete user + { + Path: "/api/users/{id}", + Method: hws.MethodDELETE, + Handler: http.HandlerFunc(deleteUserHandler), + }, +} +``` + +## Middleware + +### Middleware Execution Order + +Middleware runs in the following order: + +1. **Timer** - Records request start time (automatic) +2. **Custom middleware** - Your middleware in the order provided +3. **GZIP** - Compresses responses if enabled (automatic) +4. **Logging** - Logs request details (automatic) +5. **Route handler** - Your route handler + +### Adding Middleware + +```go +// Custom middleware functions +authMiddleware := createAuthMiddleware() +corsMiddleware := createCORSMiddleware() + +// Add in order (they execute in reverse order of addition) +err := server.AddMiddleware(authMiddleware, corsMiddleware) +``` + +### Creating Middleware - Method 1: Using NewMiddleware + +`server.NewMiddleware` creates middleware from a `MiddlewareFunc`: + +```go +type MiddlewareFunc func(w http.ResponseWriter, r *http.Request) (*http.Request, *HWSError) +``` + +This approach provides structured error handling: + +```go +// Authentication middleware +authMiddleware := server.NewMiddleware(func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) { + token := r.Header.Get("Authorization") + if token == "" { + return r, &hws.HWSError{ + StatusCode: http.StatusUnauthorized, + Message: "Missing authorization token", + Error: errors.New("no auth token"), + Level: hws.ErrorWARN, + RenderErrorPage: true, // Stop request chain and render error page + } + } + + // Validate token (simplified) + userID, err := validateToken(token) + if err != nil { + return r, &hws.HWSError{ + StatusCode: http.StatusUnauthorized, + Message: "Invalid token", + Error: err, + Level: hws.ErrorWARN, + RenderErrorPage: true, + } + } + + // Add user to context + ctx := context.WithValue(r.Context(), "userID", userID) + return r.WithContext(ctx), nil +}) + +server.AddMiddleware(authMiddleware) +``` + +### Creating Middleware - Method 2: Standard net/http + +Use standard `net/http` middleware pattern: + +```go +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +server.AddMiddleware(corsMiddleware) +``` + +### Middleware Examples + +#### Request ID Middleware + +```go +requestIDMiddleware := server.NewMiddleware(func(w http.ResponseWriter, r *http.Request) (*http.Request, *hws.HWSError) { + requestID := uuid.New().String() + w.Header().Set("X-Request-ID", requestID) + + ctx := context.WithValue(r.Context(), "requestID", requestID) + return r.WithContext(ctx), nil +}) +``` + +#### Rate Limiting Middleware + +```go +func rateLimitMiddleware(limiter *rate.Limiter) hws.Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !limiter.Allow() { + http.Error(w, "Too Many Requests", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) + } +} + +limiter := rate.NewLimiter(10, 100) // 10 req/sec, burst of 100 +server.AddMiddleware(rateLimitMiddleware(limiter)) +``` + +#### Security Headers Middleware + +```go +securityHeadersMiddleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + next.ServeHTTP(w, r) + }) +} + +server.AddMiddleware(securityHeadersMiddleware) +``` + +#### Recovery Middleware + +```go +recoveryMiddleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + server.LogError(hws.HWSError{ + StatusCode: http.StatusInternalServerError, + Message: "Panic recovered", + Error: fmt.Errorf("panic: %v", err), + Level: hws.ErrorFATAL, + }) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} + +server.AddMiddleware(recoveryMiddleware) +``` + +## Error Handling + +### HWSError Type + +```go +type HWSError struct { + StatusCode int // HTTP status code + Message string // Error message + Error error // Underlying error + Level ErrorLevel // Log level (default: ErrorERROR) + RenderErrorPage bool // Render error page and stop chain +} +``` + +### Error Levels + +```go +hws.ErrorDEBUG // Debug information +hws.ErrorINFO // Informational messages +hws.ErrorWARN // Warning conditions +hws.ErrorERROR // Error conditions (default) +hws.ErrorFATAL // Fatal errors (terminates program) +hws.ErrorPANIC // Panic-level errors +``` + +### Custom Error Pages + +Implement the `ErrorPage` interface: + +```go +type ErrorPage interface { + Render(ctx context.Context, w io.Writer) error +} +``` + +Register error page function: + +```go +// Using templ or any template engine +server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) { + return errorPages.Error(errorCode), nil +}) + +// Manual implementation +type CustomErrorPage struct { + Code int +} + +func (e *CustomErrorPage) Render(ctx context.Context, w io.Writer) error { + html := fmt.Sprintf(` + + +
Something went wrong.
+ + + `, e.Code, e.Code) + + _, err := w.Write([]byte(html)) + return err +} + +server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) { + return &CustomErrorPage{Code: errorCode}, nil +}) +``` + +### Throwing Errors + +```go +func getUserHandler(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + user, err := database.GetUser(id) + if err != nil { + server.ThrowError(w, r, hws.HWSError{ + StatusCode: http.StatusNotFound, + Message: "User not found", + Error: err, + Level: hws.ErrorWARN, + RenderErrorPage: true, // Renders error page and stops + }) + return + } + + // Success response + json.NewEncoder(w).Encode(user) +} +``` + +### Error Without Rendering Page + +Use `RenderErrorPage: false` to log error but continue: + +```go +server.ThrowError(w, r, hws.HWSError{ + StatusCode: http.StatusInternalServerError, + Message: "Failed to send email notification", + Error: err, + Level: hws.ErrorWARN, + RenderErrorPage: false, // Log only, don't render error page +}) + +// Continue with response +w.Write([]byte("User created, but email notification failed")) +``` + +### Fatal Errors + +```go +server.ThrowFatal(w, err) // Logs fatal error and terminates +``` + +## Logging + +### Adding a Logger + +```go +import "git.haelnorr.com/h/golib/hlog" + +logger, err := hlog.NewLogger(&hlog.Config{ + Level: "info", + PrettyPrint: true, +}) +if err != nil { + return err +} + +err = server.AddLogger(logger) +if err != nil { + return err +} +``` + +### Ignoring Paths + +Prevent logging for specific paths: + +```go +err := server.LoggerIgnorePaths( + "/static/", + "/favicon.ico", + "/healthz", +) +``` + +### What Gets Logged + +The logging middleware automatically captures: + +- Request method (GET, POST, etc.) +- Request path +- Response status code +- Request duration +- Client IP address +- Error details (if any) + +Example log output: + +``` +{"level":"info","method":"GET","path":"/api/users/123","status":200,"duration":"45ms","ip":"192.168.1.1","time":"2026-01-11T10:30:00Z","message":"Request completed"} + +{"level":"warn","method":"POST","path":"/api/users","status":400,"duration":"12ms","ip":"192.168.1.1","error":"invalid email","time":"2026-01-11T10:30:05Z","message":"User validation failed"} +``` + +### Manual Logging + +Use the logger directly in handlers: + +```go +func handler(w http.ResponseWriter, r *http.Request) { + logger.Info("Processing user request") + + server.LogError(hws.HWSError{ + StatusCode: http.StatusBadRequest, + Message: "Invalid input", + Error: errors.New("missing field"), + Level: hws.ErrorWARN, + }) +} +``` + +## Static File Serving + +### SafeFileServer + +`SafeFileServer` prevents directory listing by returning 404 for directory requests: + +```go +import "embed" + +//go:embed static/* +var staticFS embed.FS + +func setupStatic(server *hws.Server) error { + httpFS := http.FS(staticFS) + staticHandler, err := hws.SafeFileServer(&httpFS) + if err != nil { + return err + } + + routes := []hws.Route{ + { + Path: "/static/", + Method: hws.MethodGET, + Handler: http.StripPrefix("/static/", staticHandler), + }, + } + + return server.AddRoutes(routes...) +} +``` + +### Using OS Filesystem + +```go +fs := http.Dir("./public") +httpFS := http.FileSystem(fs) +staticHandler, err := hws.SafeFileServer(&httpFS) +if err != nil { + return err +} + +routes := []hws.Route{ + { + Path: "/assets/", + Method: hws.MethodGET, + Handler: http.StripPrefix("/assets/", staticHandler), + }, +} +``` + +### Ignoring Static File Logs + +```go +server.LoggerIgnorePaths("/static/", "/assets/", "/favicon.ico") +``` + +## Server Lifecycle + +### Starting the Server + +```go +ctx := context.Background() +err := server.Start(ctx) +if err != nil { + return err +} +``` + +The `Start` method: +1. Validates that routes have been added +2. Applies middleware if not already done +3. Starts the HTTP server in a goroutine +4. Begins polling for server readiness + +### Checking Server Status + +```go +// Wait for server to be ready +<-server.Ready() + +// Check if ready without blocking +if server.IsReady() { + fmt.Println("Server is ready") +} + +// Get server address +addr := server.Addr() // e.g., "127.0.0.1:3000" +``` + +### Graceful Shutdown + +```go +import ( + "os" + "os/signal" + "syscall" +) + +func main() { + // ... server setup ... + + server.Start(ctx) + + // Wait for interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + // Shutdown with timeout + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + logger.Fatal("Shutdown failed", err) + } + + logger.Info("Server shutdown complete") +} +``` + +### Health Check + +The server automatically provides a health check endpoint at `/healthz`: + +```bash +curl http://localhost:3000/healthz +# Returns: 200 OK (no body) +``` + +This endpoint: +- Always returns `200 OK` when server is running +- Is used internally to determine server readiness +- Should be ignored in logging: `server.LoggerIgnorePaths("/healthz")` + +## Complete Production Example + +```go +package main + +import ( + "context" + "database/sql" + "embed" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "git.haelnorr.com/h/golib/hlog" + "git.haelnorr.com/h/golib/hws" + "git.haelnorr.com/h/golib/hwsauth" + _ "github.com/lib/pq" +) + +//go:embed static/* +var staticFS embed.FS + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + // Load configuration + config, err := hws.ConfigFromEnv() + if err != nil { + return err + } + + // Create server + server, err := hws.NewServer(config) + if err != nil { + return err + } + + // Setup logger + logger, err := hlog.NewLogger(&hlog.Config{ + Level: "info", + PrettyPrint: os.Getenv("ENV") != "production", + }) + if err != nil { + return err + } + + server.AddLogger(logger) + server.LoggerIgnorePaths("/static/", "/favicon.ico", "/healthz") + + // Setup error pages + server.AddErrorPage(func(errorCode int) (hws.ErrorPage, error) { + return &ErrorPage{Code: errorCode}, nil + }) + + // Setup database + db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) + if err != nil { + return err + } + defer db.Close() + + // Setup static files + httpFS := http.FS(staticFS) + staticHandler, err := hws.SafeFileServer(&httpFS) + if err != nil { + return err + } + + // Define routes + routes := []hws.Route{ + // Static files + { + Path: "/static/", + Method: hws.MethodGET, + Handler: http.StripPrefix("/static/", staticHandler), + }, + + // Public pages + { + Path: "/", + Method: hws.MethodGET, + Handler: http.HandlerFunc(homeHandler), + }, + { + Path: "/about", + Method: hws.MethodGET, + Handler: http.HandlerFunc(aboutHandler), + }, + + // API endpoints + { + Path: "/api/users", + Method: hws.MethodGET, + Handler: http.HandlerFunc(listUsersHandler(db)), + }, + { + Path: "/api/users/{id}", + Method: hws.MethodGET, + Handler: http.HandlerFunc(getUserHandler(db)), + }, + { + Path: "/api/users", + Method: hws.MethodPOST, + Handler: http.HandlerFunc(createUserHandler(server, db)), + }, + } + + // Register routes + if err := server.AddRoutes(routes...); err != nil { + return err + } + + // Add custom middleware + corsMiddleware := createCORSMiddleware() + securityMiddleware := createSecurityHeadersMiddleware() + + if err := server.AddMiddleware(corsMiddleware, securityMiddleware); err != nil { + return err + } + + // Start server + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := server.Start(ctx); err != nil { + return err + } + + logger.Info(fmt.Sprintf("Server started on %s", server.Addr())) + + // Wait for interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + logger.Info("Shutting down server...") + + // Graceful shutdown + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + if err := server.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("shutdown failed: %w", err) + } + + logger.Info("Server shutdown complete") + return nil +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Welcome to the API")) +} + +func aboutHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("About this API")) +} + +func listUsersHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Implementation + } +} + +func getUserHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + // Implementation + } +} + +func createUserHandler(server *hws.Server, db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Implementation with error handling + if err := validateInput(r); err != nil { + server.ThrowError(w, r, hws.HWSError{ + StatusCode: http.StatusBadRequest, + Message: "Invalid input", + Error: err, + Level: hws.ErrorWARN, + RenderErrorPage: false, + }) + return + } + // ... create user ... + } +} + +func createCORSMiddleware() hws.Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func createSecurityHeadersMiddleware() hws.Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + next.ServeHTTP(w, r) + }) + } +} + +type ErrorPage struct { + Code int +} + +func (e *ErrorPage) Render(ctx context.Context, w io.Writer) error { + html := fmt.Sprintf(` + + +Something went wrong. Please try again later.
+ +`, e.Code, e.Code) + + _, err := w.Write([]byte(html)) + return err +} +``` + +## Integration with hwsauth + +`hws` integrates seamlessly with `hwsauth` for JWT authentication: + +```go +import "git.haelnorr.com/h/golib/hwsauth" + +// Setup authentication +authConfig, _ := hwsauth.ConfigFromEnv() +auth, _ := hwsauth.NewAuthenticator[*User, bun.Tx]( + authConfig, + loadUserFunc, + server, + beginTxFunc, + logger, + errorPageFunc, +) + +// Apply authentication middleware +server.AddMiddleware(auth.Authenticate()) + +// Ignore public paths +auth.IgnorePaths("/", "/static/", "/login", "/register") + +// Protected routes +routes := []hws.Route{ + { + Path: "/dashboard", + Method: hws.MethodGET, + Handler: auth.LoginReq(dashboardHandler), + }, + { + Path: "/change-password", + Method: hws.MethodPOST, + Handler: auth.LoginReq(auth.FreshReq(changePasswordHandler)), + }, +} +``` + +See the [hwsauth documentation](./HWSAuth.md) for details. + +## Best Practices + +### Configuration +- Use environment variables for production configuration +- Set appropriate timeouts for your use case +- Enable GZIP for production to reduce bandwidth + +### Routing +- Use RESTful patterns for API endpoints +- Leverage path parameters for dynamic routes +- Group related routes together + +### Middleware +- Add recovery middleware to catch panics +- Use rate limiting for public endpoints +- Apply security headers for all responses +- Order middleware carefully (auth before business logic) + +### Error Handling +- Always provide meaningful error messages +- Use appropriate HTTP status codes +- Set `RenderErrorPage: false` for API endpoints +- Log errors with appropriate levels + +### Logging +- Ignore static asset paths to reduce noise +- Use structured logging with context +- Don't log sensitive information +- Monitor error rates and patterns + +### Static Files +- Always use `SafeFileServer` to prevent directory listing +- Serve static files from embedded FS in production +- Ignore static paths in logging +- Use caching headers for static assets + +### Lifecycle +- Implement graceful shutdown +- Handle OS signals properly +- Wait for server readiness before tests +- Set reasonable shutdown timeouts + +### Security +- Validate all user input +- Use HTTPS in production +- Set security headers +- Implement rate limiting +- Use CORS appropriately + +## Troubleshooting + +### Server won't start + +Check that: +- Routes have been added with `AddRoutes()` +- Middleware has been added with `AddMiddleware()` +- Port is not already in use +- Host is valid (IP or hostname) + +### Routes not matching + +- Ensure HTTP method matches exactly +- Check path pattern syntax (Go 1.22+ patterns) +- Verify route was added before starting server +- Check middleware isn't blocking requests + +### Middleware not executing + +- Verify `AddMiddleware()` was called after `AddRoutes()` +- Check middleware order (they execute in reverse) +- Ensure middleware calls `next.ServeHTTP()` + +### Error pages not rendering + +- Verify `AddErrorPage()` was called +- Check `RenderErrorPage: true` in `HWSError` +- Ensure `ErrorPage.Render()` writes to the writer +- Look for errors in error page rendering logic + +### Logs not appearing + +- Check logger was added with `AddLogger()` +- Verify log level in hlog configuration +- Check if path is in ignored paths list +- Ensure request completes (middleware chain) + +## See Also + +- [hwsauth](./HWSAuth.md) - JWT authentication middleware +- [hlog](./hlog.md) - Structured logging +- [jwt](./JWT.md) - JWT token management + +## Links + +- [Source Code](https://git.haelnorr.com/h/golib/hws) +- [Issue Tracker](https://git.haelnorr.com/h/golib/hws/issues) +- [Examples](https://git.haelnorr.com/h/golib/hws/tree/master/examples) + diff --git a/Home.md b/Home.md index 321ac6b..64f7a6a 100644 --- a/Home.md +++ b/Home.md @@ -4,11 +4,14 @@ Welcome to the golib documentation wiki. This wiki provides comprehensive docume ## Modules -### [JWT](JWT.md) -JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs. +### [HWS](HWS.md) +H Web Server - A lightweight, opinionated HTTP web server framework for Go built on top of the standard library. Features Go 1.22+ routing patterns, built-in middleware, structured error handling, and production-ready defaults. ### [HWSAuth](HWSAuth.md) -JWT-based authentication middleware for the hws web framework. Provides complete authentication solution with access tokens, refresh tokens, automatic token rotation, and seamless database/ORM integration. +JWT-based authentication middleware for the HWS web framework. Provides complete authentication solution with access tokens, refresh tokens, automatic token rotation, and seamless database/ORM integration. + +### [JWT](JWT.md) +JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs. ## Installation @@ -19,3 +22,4 @@ go get git.haelnorr.com/h/golib/jwt ## Contributing For issues, feature requests, or contributions, please visit the [golib repository](https://git.haelnorr.com/h/golib). +