Skip to main content
Every handler in Mizu receives a *mizu.Ctx β€” short for context. This is your primary interface for working with HTTP requests and responses. Understanding Ctx is essential for building any Mizu application.

What is Context?

In web development, context refers to all the information associated with a single HTTP request. When a user visits your website:
  1. Their browser sends a request (URL, headers, body)
  2. Your server processes it and creates a response (status, headers, body)
  3. Various metadata is tracked (timing, request ID, user info)
Mizu wraps all of this into a single Ctx object, giving you easy access to everything you need.
The Ctx type is a thin wrapper around Go’s standard http.Request and http.ResponseWriter. It adds convenience methods while maintaining full compatibility with the standard library.

The Mizu Ctx Wrapper

When a request arrives, Mizu creates a Ctx that contains:
ComponentDescriptionAccess Method
RequestThe incoming HTTP requestc.Request()
Response WriterInterface to send the responsec.Writer()
LoggerRequest-scoped structured loggerc.Logger()
ContextGo’s context.Context for cancellationc.Context()
Path ParametersURL path variablesc.Param(name)

Creating Your First Handler with Context

package main

import (
    "github.com/go-mizu/mizu"
)

func hello(c *mizu.Ctx) error {
    // c gives you access to everything about this request
    name := c.Query("name")
    if name == "" {
        name = "World"
    }
    return c.Text(200, "Hello, "+name+"!")
}

func main() {
    app := mizu.New()
    app.Get("/hello", hello)
    app.Listen(":3000")
}
Visit http://localhost:3000/hello?name=Mizu and you’ll see β€œHello, Mizu!”.

Accessing the Request

The request contains everything the client sent. Use c.Request() to access the underlying *http.Request:
func showRequest(c *mizu.Ctx) error {
    req := c.Request()

    // Method: GET, POST, PUT, DELETE, etc.
    method := req.Method

    // Full URL path
    path := req.URL.Path

    // Query string (everything after ?)
    query := req.URL.RawQuery

    // Client's IP address
    remoteAddr := req.RemoteAddr

    // HTTP protocol version
    proto := req.Proto

    c.Logger().Info("request details",
        "method", method,
        "path", path,
        "query", query,
        "remote", remoteAddr,
        "proto", proto,
    )

    return c.Text(200, "Request logged!")
}

Common Request Properties

PropertyDescriptionExample
req.MethodHTTP method"GET", "POST"
req.URL.PathRequest path"/users/123"
req.URL.RawQueryQuery string"page=1&limit=10"
req.HostHost header"localhost:3000"
req.RemoteAddrClient address"192.168.1.1:54321"
req.HeaderAll headershttp.Header map
req.BodyRequest bodyio.ReadCloser
req.ContentLengthBody size1024 (bytes)

Writing Responses

The response writer is how you send data back to the client. While Mizu provides convenient helper methods, you can also access the raw writer:
func rawResponse(c *mizu.Ctx) error {
    w := c.Writer()

    // Set status code
    w.WriteHeader(http.StatusOK)

    // Write response body
    w.Write([]byte("Hello from raw writer!"))

    return nil
}
Once you call w.WriteHeader() or w.Write(), headers are sent to the client and cannot be modified. Always set headers before writing the body.
Mizu provides helper methods that handle common response patterns:
func responses(c *mizu.Ctx) error {
    // Plain text
    return c.Text(200, "Hello, World!")

    // JSON
    return c.JSON(200, map[string]string{"message": "Hello"})

    // HTML
    return c.HTML(200, "<h1>Hello</h1>")

    // No content
    return c.NoContent(204)

    // Redirect
    return c.Redirect(302, "/new-location")
}

The Request Logger

Each request has its own logger that automatically includes request context:
func logging(c *mizu.Ctx) error {
    logger := c.Logger()

    // Different log levels
    logger.Debug("detailed debugging info")
    logger.Info("general information")
    logger.Warn("warning message")
    logger.Error("error occurred", "error", err)

    // Add custom fields
    logger.Info("user action",
        "user_id", 123,
        "action", "login",
        "ip", c.Request().RemoteAddr,
    )

    return c.Text(200, "Logged!")
}

Log Output Example

When using the default development logger:
11:23:45 INF user action user_id=123 action=login ip=127.0.0.1:54321 request_id=abc123
The request_id is automatically added to help trace requests across log entries.

Working with Go’s context.Context

Every request includes a context.Context that signals when the request should stop. This is essential for:
  • Timeouts: Stop processing if it takes too long
  • Cancellation: Stop if the client disconnects
  • Value Passing: Store request-scoped data

Checking for Cancellation

func longOperation(c *mizu.Ctx) error {
    ctx := c.Context()

    // Simulate a long operation
    for i := 0; i < 100; i++ {
        select {
        case <-ctx.Done():
            // Client disconnected or timeout
            c.Logger().Info("request cancelled", "reason", ctx.Err())
            return ctx.Err()
        default:
            // Continue processing
            time.Sleep(100 * time.Millisecond)
        }
    }

    return c.Text(200, "Completed!")
}

Passing Context to Database Calls

Always pass the request context to database and external service calls:
func getUser(c *mizu.Ctx) error {
    ctx := c.Context()
    id := c.Param("id")

    // The context will cancel the query if the request is cancelled
    user, err := db.GetUserByID(ctx, id)
    if err != nil {
        return err
    }

    return c.JSON(200, user)
}

Request Timeouts

You can add timeouts to prevent slow requests from consuming resources:
func withTimeout(c *mizu.Ctx) error {
    // Create a timeout context
    ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
    defer cancel()

    // Use the timeout context for external calls
    result, err := slowExternalAPI(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return c.Text(504, "Request timed out")
        }
        return err
    }

    return c.JSON(200, result)
}

Working with Headers

Headers provide metadata about requests and responses:

Reading Request Headers

func readHeaders(c *mizu.Ctx) error {
    req := c.Request()

    // Get a single header
    contentType := req.Header.Get("Content-Type")
    userAgent := req.Header.Get("User-Agent")
    auth := req.Header.Get("Authorization")

    // Get all values for a header (some headers can appear multiple times)
    acceptLanguages := req.Header.Values("Accept-Language")

    // Check if header exists
    if contentType == "" {
        return c.Text(400, "Content-Type header required")
    }

    return c.JSON(200, map[string]any{
        "content_type": contentType,
        "user_agent":   userAgent,
        "languages":    acceptLanguages,
    })
}

Setting Response Headers

func setHeaders(c *mizu.Ctx) error {
    // Use c.Header() to set response headers
    c.Header().Set("X-Custom-Header", "custom-value")
    c.Header().Set("Cache-Control", "max-age=3600")
    c.Header().Add("Set-Cookie", "session=abc123")

    // IMPORTANT: Set headers BEFORE writing the response body
    return c.Text(200, "Headers set!")
}

Common Headers

HeaderPurposeExample
Content-TypeResponse formatapplication/json
Cache-ControlCaching rulesmax-age=3600
AuthorizationAuthenticationBearer token123
X-Request-IDRequest trackinguuid-here
Set-CookieSet browser cookiesession=abc

Storing Values in Context

Sometimes you need to pass data between middleware and handlers. Use context values:

Setting Values in Middleware

type contextKey string

const userKey contextKey = "user"

func authMiddleware(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        // Validate token and get user
        token := c.Request().Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            return c.JSON(401, map[string]string{"error": "unauthorized"})
        }

        // Store user in context
        ctx := context.WithValue(c.Context(), userKey, user)
        c.SetContext(ctx)

        return next(c)
    }
}

Reading Values in Handlers

func protectedHandler(c *mizu.Ctx) error {
    // Retrieve user from context
    user, ok := c.Context().Value(userKey).(*User)
    if !ok {
        return c.JSON(401, map[string]string{"error": "not authenticated"})
    }

    return c.JSON(200, map[string]any{
        "message": "Hello, " + user.Name,
        "user_id": user.ID,
    })
}

Helper Functions for Type Safety

Create helper functions to avoid repetitive type assertions:
// GetUser retrieves the authenticated user from context
func GetUser(c *mizu.Ctx) (*User, bool) {
    user, ok := c.Context().Value(userKey).(*User)
    return user, ok
}

// MustGetUser retrieves the user or panics (use after auth middleware)
func MustGetUser(c *mizu.Ctx) *User {
    user, ok := GetUser(c)
    if !ok {
        panic("user not found in context - auth middleware missing?")
    }
    return user
}

func handler(c *mizu.Ctx) error {
    user := MustGetUser(c)
    return c.Text(200, "Hello, "+user.Name)
}

Context Best Practices

Do’s

// DO: Pass context to external calls
result, err := db.Query(c.Context(), "SELECT ...")

// DO: Check for cancellation in long operations
select {
case <-c.Context().Done():
    return c.Context().Err()
default:
    // continue
}

// DO: Use typed context keys
type contextKey string
const myKey contextKey = "myValue"

// DO: Set headers before writing body
c.Header().Set("X-Custom", "value")
return c.JSON(200, data)

Don’ts

// DON'T: Ignore context in database calls
result, err := db.Query(context.Background(), "SELECT ...")  // Bad!

// DON'T: Use string keys for context values
ctx := context.WithValue(c.Context(), "user", user)  // Bad - use typed key

// DON'T: Store large objects in context
ctx := context.WithValue(c.Context(), "data", hugeDataSet)  // Bad!

// DON'T: Modify context after passing to goroutines
go func() {
    c.SetContext(newCtx)  // Race condition!
}()

Testing with Context

When writing tests, you can create a test context:
func TestHandler(t *testing.T) {
    // Create a new app
    app := mizu.New()

    // Register your handler
    app.Get("/hello", helloHandler)

    // Create a test request
    req := httptest.NewRequest("GET", "/hello?name=Test", nil)
    rec := httptest.NewRecorder()

    // Serve the request
    app.ServeHTTP(rec, req)

    // Check the response
    if rec.Code != 200 {
        t.Errorf("expected 200, got %d", rec.Code)
    }

    if rec.Body.String() != "Hello, Test!" {
        t.Errorf("unexpected body: %s", rec.Body.String())
    }
}

Testing with Context Values

func TestProtectedHandler(t *testing.T) {
    app := mizu.New()

    // Add auth middleware
    app.Use(authMiddleware)
    app.Get("/protected", protectedHandler)

    // Create request with auth header
    req := httptest.NewRequest("GET", "/protected", nil)
    req.Header.Set("Authorization", "Bearer valid-token")
    rec := httptest.NewRecorder()

    app.ServeHTTP(rec, req)

    if rec.Code != 200 {
        t.Errorf("expected 200, got %d", rec.Code)
    }
}

Complete Example

Here’s a complete example showing context usage in a real-world scenario:
package main

import (
    "context"
    "errors"
    "time"

    "github.com/go-mizu/mizu"
)

type contextKey string

const (
    requestIDKey contextKey = "request_id"
    userKey      contextKey = "user"
)

type User struct {
    ID   int
    Name string
}

// Middleware to add request ID
func requestIDMiddleware(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        requestID := generateUUID()
        ctx := context.WithValue(c.Context(), requestIDKey, requestID)
        c.SetContext(ctx)
        c.Header().Set("X-Request-ID", requestID)
        return next(c)
    }
}

// Get user from database with timeout
func getUser(c *mizu.Ctx) error {
    id := c.Param("id")

    // Create a 5-second timeout
    ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
    defer cancel()

    // Simulate database call
    user, err := fetchUserFromDB(ctx, id)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            c.Logger().Error("database timeout", "user_id", id)
            return c.JSON(504, map[string]string{"error": "request timeout"})
        }
        return err
    }

    c.Logger().Info("user fetched", "user_id", id, "name", user.Name)
    return c.JSON(200, user)
}

func main() {
    app := mizu.New()

    // Apply middleware
    app.Use(requestIDMiddleware)

    // Routes
    app.Get("/users/{id}", getUser)

    app.Listen(":3000")
}

// Helper functions (implementations omitted)
func generateUUID() string { return "abc-123" }
func fetchUserFromDB(ctx context.Context, id string) (*User, error) {
    // Check for cancellation
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }
    return &User{ID: 1, Name: "Alice"}, nil
}

Summary

MethodDescription
c.Request()Get the *http.Request
c.Writer()Get the http.ResponseWriter
c.Logger()Get the request-scoped logger
c.Context()Get the context.Context
c.SetContext(ctx)Replace the context (for middleware)
c.Header()Get response headers for modification
c.Param(name)Get URL path parameter
c.Query(name)Get query string parameter

What’s Next