██╗░░██╗████████╗████████╗██████╗░██╗░░░██╗████████╗██╗██╗░░░░░
██║░░██║╚══██╔══╝╚══██╔══╝██╔══██╗██║░░░██║╚══██╔══╝██║██║░░░░░
███████║░░░██║░░░░░░██║░░░██████╔╝██║░░░██║░░░██║░░░██║██║░░░░░
██╔══██║░░░██║░░░░░░██║░░░██╔═══╝░██║░░░██║░░░██║░░░██║██║░░░░░
██║░░██║░░░██║░░░░░░██║░░░██║░░░░░╚██████╔╝░░░██║░░░██║███████╗
╚═╝░░╚═╝░░░╚═╝░░░░░░╚═╝░░░╚═╝░░░░░░╚═════╝░░░░╚═╝░░░╚═╝╚══════╝
A comprehensive HTTP utilities library for Go that provides common HTTP functionality including JWT handling, middleware, proxying, file serving, and more.
- 🍪 Cookie Management - Easy cookie setting and retrieval with security options
- 🔐 JWT Utilities - JWT encoding/decoding with custom claims support
- 🔄 HTTP Retry Client - Configurable retry logic with exponential backoff
- 🛠️ Middleware - Logging, session context, and middleware chaining
- 🔄 Reverse Proxy - Simple reverse proxy with development mode support
- 📁 File Serving - Static file serving with SPA fallback support
- 📤 Streaming Upload - Multipart upload server/client helpers with strict limits
- 📥 Streaming Download - Download server/client helpers with parsed metadata
- 📏 Request Limiting - Request body size limiting for security
- ⚡ Zero Dependencies - Minimal external dependencies (only JWT library)
go get ella.to/httputil@0.0.7package main
import (
"net/http"
"time"
"ella.to/httputil"
)
func main() {
// Create a retry client
client, _ := httputil.NewRetryClient(
httputil.WithMaxRetries(3),
httputil.WithHeaders(map[string]string{
"User-Agent": "my-app/1.0",
}),
)
// Set up middleware
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
httputil.SetCookie(w, "session", "abc123", 24*time.Hour, true)
w.Write([]byte("Hello World"))
})
// Chain middleware
handler := httputil.Chain(mux, httputil.WithLogging)
http.ListenAndServe(":8080", handler)
}Sets an HTTP cookie with security options.
func SetCookie(w http.ResponseWriter, key CookieKey, value string, maxAge time.Duration, secure bool)Parameters:
w- HTTP response writerkey- Cookie name (typed as CookieKey for type safety)value- Cookie valuemaxAge- Cookie expiration duration (0 deletes the cookie)secure- Whether cookie should only be sent over HTTPS
Example:
// Set a secure session cookie for 24 hours
httputil.SetCookie(w, httputil.CookieKey("session"), "user123", 24*time.Hour, true)
// Delete a cookie
httputil.SetCookie(w, httputil.CookieKey("old_session"), "", 0, false)Retrieves a cookie value from the request.
func GetCookie(key CookieKey, r *http.Request) (string, error)Returns: Cookie value and error (wrapped with context if cookie not found)
Example:
sessionID, err := httputil.GetCookie(httputil.CookieKey("session"), r)
if err != nil {
// Handle missing or invalid cookie
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Use sessionID...func New(secretKey string) *JwtExample:
jwtHandler := httputil.New("your-secret-key")func (j *Jwt) Encode(claims JwtClaims) (string, error)Example:
type UserClaims struct {
*httputil.JwtRegisteredClaims
UserID string `json:"user_id"`
Role string `json:"role"`
}
func (c *UserClaims) ParseToken(token *httputil.JwtToken) error {
if !token.Valid {
return errors.New("invalid token")
}
return nil
}
claims := &UserClaims{
JwtRegisteredClaims: &httputil.JwtRegisteredClaims{
ExpiresAt: httputil.JwtNewNumericDate(time.Now().Add(24 * time.Hour)),
Subject: "user123",
},
UserID: "12345",
Role: "admin",
}
token, err := jwtHandler.Encode(claims)func (j *Jwt) Decode(jwt string, claims JwtClaims) errorNote: Claims must implement the TokenParser interface:
type TokenParser interface {
ParseToken(token *JwtToken) error
}Example:
decodedClaims := &UserClaims{JwtRegisteredClaims: &httputil.JwtRegisteredClaims{}}
err := jwtHandler.Decode(token, decodedClaims)
if err != nil {
// Handle invalid token
}
// Use decodedClaims.UserID, decodedClaims.Role, etc.func NewRetryClient(opts ...retryTransportOpt) (*http.Client, error)Available Options:
WithMaxRetries(int)- Maximum number of retries (default: 3)WithInitialDelay(time.Duration)- Initial delay between retries (default: 1s)WithMaxDelay(time.Duration)- Maximum delay cap (default: 30s)WithHeaders(map[string]string)- Headers to inject into every request
Example:
client, err := httputil.NewRetryClient(
httputil.WithMaxRetries(5),
httputil.WithInitialDelay(500*time.Millisecond),
httputil.WithMaxDelay(10*time.Second),
httputil.WithHeaders(map[string]string{
"User-Agent": "my-service/1.0",
"Accept": "application/json",
}),
)
// Use like any http.Client
resp, err := client.Get("https://api.example.com/data")Retry Behavior:
- Retries on 5xx server errors and 429 (Too Many Requests)
- Uses exponential backoff with jitter
- Preserves request body for retries
- No retry on 4xx client errors (except 429)
Chains multiple middleware functions together.
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.HandlerExample:
handler := httputil.Chain(
yourHandler,
httputil.WithLogging,
sessionMiddleware,
authMiddleware,
)Logs HTTP requests with method, path, status code, and response size.
func WithLogging(next http.Handler) http.HandlerExample:
handler := httputil.WithLogging(yourHandler)Log Output:
INFO http called method=GET path=/api/users code=200 size=1024
ERROR http called method=POST path=/api/users code=500 size=0
Extracts session information from Bearer tokens or cookies and adds it to request context.
func WithSessionContext[T any](
cookieKey CookieKey,
ctxKey ContextKey,
ctxTokenKey ContextKey,
parseSession func(token string) (T, error)
) func(next http.Handler) http.HandlerParameters:
cookieKey- Cookie name to check for tokenctxKey- Context key to store parsed sessionctxTokenKey- Context key to store raw token (empty string to skip)parseSession- Function to parse token into session data
Example:
type User struct {
ID string
Role string
}
parseSession := func(token string) (User, error) {
// Parse your token (JWT, database lookup, etc.)
return User{ID: "123", Role: "admin"}, nil
}
middleware := httputil.WithSessionContext(
httputil.CookieKey("auth_token"),
httputil.ContextKey("user"),
httputil.ContextKey("token"), // Store raw token
parseSession,
)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if user, ok := r.Context().Value(httputil.ContextKey("user")).(User); ok {
fmt.Fprintf(w, "Hello, %s (Role: %s)", user.ID, user.Role)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}))Token Sources (in priority order):
Authorization: Bearer <token>header- Cookie specified by
cookieKey
Creates a simple reverse proxy to another HTTP service.
func ReverseProxy(rawURL string) (http.HandlerFunc, error)Example:
// Proxy all requests to another server
proxyHandler, err := httputil.ReverseProxy("http://backend:8080")
if err != nil {
log.Fatal(err)
}
http.Handle("/api/", proxyHandler)Sets up development-mode proxying with exceptions for certain paths.
func DevProxy(mux *http.ServeMux, service http.Handler, isDev bool, proxyAddr string, exceptions []string) errorParameters:
mux- HTTP mux to configureservice- Handler for your main serviceisDev- Whether in development modeproxyAddr- Address of development server (e.g., frontend dev server)exceptions- Paths that should go to service instead of proxy
Example:
mux := http.NewServeMux()
serviceHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API Response"))
})
// In development: proxy UI requests to dev server, API requests to service
err := httputil.DevProxy(
mux,
serviceHandler,
true, // isDev
"http://localhost:3000", // frontend dev server
[]string{"/api", "/auth"}, // exceptions - these go to service
)
// Requests to /api/* -> serviceHandler
// Requests to /* -> proxy to localhost:3000Serves static files from a filesystem with SPA (Single Page Application) fallback.
func ServeFile(fs fs.FS) http.HandlerExample with embedded files:
//go:embed static/*
var staticFiles embed.FS
func main() {
staticFS, _ := fs.Sub(staticFiles, "static")
http.Handle("/", httputil.ServeFile(staticFS))
}Example with directory:
http.Handle("/static/", httputil.ServeFile(os.DirFS("./public")))SPA Behavior:
- If requested file exists, serves it normally
- If file doesn't exist, sets path to "/" and serves that (typically index.html)
- Perfect for React/Vue/Angular SPAs with client-side routing
Applies size limit to request body (note: function name has typo but works correctly).
func ReadLimter(size int64, w http.ResponseWriter, r *http.Request)Example:
func handler(w http.ResponseWriter, r *http.Request) {
// Limit request body to 1MB
httputil.ReadLimter(1024*1024, w, r)
body, err := io.ReadAll(r.Body)
if err != nil {
// Handle size limit exceeded
return
}
// Process body...
}Applies size limit and reads the body in one operation.
func ReadBodyLimiter(size int64, w http.ResponseWriter, r *http.Request) ([]byte, error)Example:
func apiHandler(w http.ResponseWriter, r *http.Request) {
// Limit and read body (max 10KB)
body, err := httputil.ReadBodyLimiter(10*1024, w, r)
if err != nil {
// ReadBodyLimiter already set 400 status
w.Write([]byte("Request too large"))
return
}
// Parse JSON, etc.
var data map[string]interface{}
json.Unmarshal(body, &data)
// Process data...
}Streams multipart file uploads on the server with hard limits.
func StreamMultipartUpload(
r *http.Request,
limits UploadLimits,
onFile func(file UploadedFile, content io.Reader) error,
) (UploadSummary, error)Limits:
MaxFileSize- Maximum bytes per file (0means unlimited)MaxFiles- Maximum number of files (0means unlimited)MaxTotalBytes- Maximum bytes across all files (0means unlimited)
Example (server):
func uploadHandler(w http.ResponseWriter, r *http.Request) {
summary, err := httputil.StreamMultipartUpload(r, httputil.UploadLimits{
MaxFileSize: 10 << 20, // 10MB per file
MaxFiles: 5,
MaxTotalBytes: 25 << 20, // 25MB total
}, func(file httputil.UploadedFile, content io.Reader) error {
// Stream directly to storage (disk, S3, etc.) without buffering full file.
dst, err := os.Create("./uploads/" + file.FileName)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, content)
return err
})
if err != nil {
var limitErr *httputil.UploadLimitError
if errors.As(err, &limitErr) {
http.Error(w, limitErr.Error(), http.StatusRequestEntityTooLarge)
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintf(w, "uploaded files=%d bytes=%d", summary.Files, summary.TotalBytes)
}Creates a streaming multipart upload request on the client.
func NewMultipartUploadRequest(
ctx context.Context,
method string,
url string,
fields map[string]string,
files []UploadRequestFile,
limits UploadLimits,
) (*http.Request, error)Example (client):
req, err := httputil.NewMultipartUploadRequest(
context.Background(),
http.MethodPost,
"https://api.example.com/upload",
map[string]string{"folder": "avatars"},
[]httputil.UploadRequestFile{
{
FieldName: "files",
FileName: "profile.png",
ContentType: "image/png",
Reader: fileReader,
Size: fileSize, // set -1 if unknown
},
},
httputil.UploadLimits{MaxFileSize: 10 << 20, MaxFiles: 3, MaxTotalBytes: 20 << 20},
)
if err != nil {
// Includes UploadLimitError when limits are exceeded
}
resp, err := http.DefaultClient.Do(req)Server helper for streamed file downloads. The provider inspects request data and returns an io.ReadCloser stream.
func ServeDownload(
w http.ResponseWriter,
r *http.Request,
provider func(r *http.Request) (DownloadSource, error),
) errorExample (server):
func downloadHandler(w http.ResponseWriter, r *http.Request) {
err := httputil.ServeDownload(w, r, func(req *http.Request) (httputil.DownloadSource, error) {
fileID := req.URL.Query().Get("id")
rc, size, err := openFileFromStore(fileID)
if err != nil {
return httputil.DownloadSource{}, err
}
return httputil.DownloadSource{
Name: "report.csv",
ContentType: "text/csv",
Size: size,
Reader: rc,
}, nil
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
}Client helper that executes a request and parses Content-Disposition, Content-Type, and Content-Length.
func Download(client *http.Client, req *http.Request) (*DownloadResponse, error)Example (client):
req, _ := http.NewRequest(http.MethodGet, "https://api.example.com/download?id=123", nil)
resp, err := httputil.Download(http.DefaultClient, req)
if err != nil {
return
}
defer resp.Body.Close()
fmt.Println("name:", resp.Metadata.FileName)
fmt.Println("type:", resp.Metadata.ContentType)
fmt.Println("size:", resp.Metadata.Size)
_, _ = io.Copy(dstFile, resp.Body)The browser can enforce the same constraints before sending to the server, and can parse download metadata from response headers.
type UploadLimits = {
maxFileSize: number;
maxFiles: number;
maxTotalBytes: number;
};
async function uploadFiles(endpoint: string, files: File[], limits: UploadLimits) {
if (files.length > limits.maxFiles) {
throw new Error(`too many files: ${files.length} > ${limits.maxFiles}`);
}
let total = 0;
for (const file of files) {
if (file.size > limits.maxFileSize) {
throw new Error(`file too large: ${file.name}`);
}
total += file.size;
}
if (total > limits.maxTotalBytes) {
throw new Error(`total bytes too large: ${total} > ${limits.maxTotalBytes}`);
}
const formData = new FormData();
for (const file of files) {
formData.append("files", file, file.name);
}
const res = await fetch(endpoint, {
method: "POST",
body: formData,
});
if (!res.ok) {
throw new Error(`upload failed: ${res.status}`);
}
return res.text();
}function parseFileName(contentDisposition: string | null): string | undefined {
if (!contentDisposition) return undefined;
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
const plainMatch = contentDisposition.match(/filename="?([^";]+)"?/i);
return plainMatch?.[1];
}
async function downloadFile(endpoint: string) {
const res = await fetch(endpoint, { method: "GET" });
if (!res.ok) {
throw new Error(`download failed: ${res.status}`);
}
const fileName = parseFileName(res.headers.get("content-disposition")) ?? "download.bin";
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
const size = Number(res.headers.get("content-length") ?? "-1");
const blob = await res.blob();
const url = URL.createObjectURL(new Blob([blob], { type: contentType }));
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
return { fileName, contentType, size };
}value, err := httputil.GetCookie(key, r)
if err != nil {
if errors.Is(err, httputil.ErrParsingCookie) {
// Handle cookie parsing error
}
}err := jwtHandler.Decode(token, claims)
if err != nil {
// Could be: invalid signature, expired token, malformed token, etc.
log.Printf("JWT decode error: %v", err)
}client, err := httputil.NewRetryClient(
httputil.WithMaxRetries(-1), // Invalid!
)
if err != nil {
// Handle configuration error
}Here's a complete example showing multiple features working together:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"ella.to/httputil"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
type UserClaims struct {
*httputil.JwtRegisteredClaims
UserID string `json:"user_id"`
Role string `json:"role"`
}
func (c *UserClaims) ParseToken(token *httputil.JwtToken) error {
if !token.Valid {
return fmt.Errorf("invalid token")
}
return nil
}
func main() {
// Setup JWT
jwtHandler := httputil.New("super-secret-key")
// Setup middleware
parseSession := func(token string) (User, error) {
claims := &UserClaims{JwtRegisteredClaims: &httputil.JwtRegisteredClaims{}}
err := jwtHandler.Decode(token, claims)
if err != nil {
return User{}, err
}
return User{ID: claims.UserID, Role: claims.Role}, nil
}
sessionMiddleware := httputil.WithSessionContext(
httputil.CookieKey("session"),
httputil.ContextKey("user"),
httputil.ContextKey(""),
parseSession,
)
// Setup routes
mux := http.NewServeMux()
// Login endpoint
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// Limit request size
body, err := httputil.ReadBodyLimiter(1024, w, r)
if err != nil {
w.Write([]byte("Request too large"))
return
}
var loginReq struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.Unmarshal(body, &loginReq); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Authenticate user (simplified)
if loginReq.Username == "admin" && loginReq.Password == "secret" {
claims := &UserClaims{
JwtRegisteredClaims: &httputil.JwtRegisteredClaims{
ExpiresAt: httputil.JwtNewNumericDate(time.Now().Add(24 * time.Hour)),
Subject: loginReq.Username,
},
UserID: "admin123",
Role: "admin",
}
token, err := jwtHandler.Encode(claims)
if err != nil {
http.Error(w, "Token generation failed", http.StatusInternalServerError)
return
}
// Set secure cookie
httputil.SetCookie(w, httputil.CookieKey("session"), token, 24*time.Hour, true)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"token": token})
} else {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}
})
// Protected endpoint
mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(httputil.ContextKey("user")).(User)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
// Setup development proxy for frontend
err := httputil.DevProxy(
mux,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "API endpoint not found", http.StatusNotFound)
}),
true, // development mode
"http://localhost:3000", // frontend dev server
[]string{"/api", "/login", "/profile"}, // API exceptions
)
if err != nil {
log.Fatal("DevProxy setup failed:", err)
}
// Chain all middleware
handler := httputil.Chain(
mux,
sessionMiddleware,
httputil.WithLogging,
)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}The library uses custom types for better type safety:
type CookieKey string // For cookie names
type ContextKey string // For context keysThis prevents mixing up string parameters and provides better IDE support.
The library includes comprehensive tests with no mocking - all tests use real HTTP servers and actual functionality. Run tests with:
go test ./...MIT License - see LICENSE.md for details.