ESC
Type to search...
S
Soli Docs

Middleware

Filter HTTP requests and responses. Middleware enables cross-cutting concerns like authentication, logging, and CORS handling.

Request Logging

Soli includes built-in request logging at the server level with timing information:

[LOG] GET /users - 200 (1.234ms)
[LOG] POST /login - 302 (12.876ms)
[LOG] GET /missing - 404 (0.102ms)

To disable request logging, set the environment variable:

# Disable request logging
SOLI_REQUEST_LOG=false soli serve myapp

# Or
SOLI_REQUEST_LOG=0 soli serve myapp

Middleware Attributes

Middleware files in app/middleware/ are loaded automatically. Control behavior using special comment attributes:

Attribute Description
# order: N Execution order (lower runs first, default: 100)
# global_only: true Runs for ALL requests, cannot be scoped to specific routes
# scope_only: true Only runs when explicitly scoped, never globally

Creating Middleware

Middleware functions receive a request hash and return a result hash:

# order: 5
# global_only: true

def add_cors_headers(req: Any)    # Continue to next middleware/handler
    {
        "continue": true,
        "request": req
    }
end
# order: 20
# scope_only: true

def authenticate(req: Any)    let headers = req["headers"];
    let api_key = "";

    if has_key(headers, "X-Api-Key")
        api_key = headers["X-Api-Key"];
    end

    if api_key == ""
        # Short-circuit with error response
        return {
            "continue": false,
            "response": {
                "status": 401,
                "headers": {"Content-Type": "application/json"},
                "body": json_stringify({
                    "error": "Unauthorized",
                    "message": "API key required"
                })
            }
        };
    end

    # Continue to handler
    {
        "continue": true,
        "request": req
    }
end

Return Format

Middleware must return a hash with one of these formats:

{
    "continue": true,
    "request": req
}
{
    "continue": false,
    "response": {
        "status": 401,
        "body": "Unauthorized"
    }
}

Execution Order

Requests flow through middleware layers before reaching your controller.

Step 1 Global Middleware (Sorted by order)
Step 2 Scoped Middleware (Specific to route)
Target Controller Action
Return Response sent to client

Scoping Middleware to Routes

Use the middleware() function in routes to apply scope-only middleware:

# Public routes (no auth required)
get("/", "home#index");
get("/login", "auth#login");

# Protected routes (auth middleware applied)
middleware("authenticate", ->
    get("/dashboard", "dashboard#index");
    get("/profile", "users#profile");
    post("/settings", "users#update_settings");
end);

# Multiple middleware
middleware(["authenticate", "admin_only"], ->
    get("/admin", "admin#index");
    get("/admin/users", "admin#users");
end);

Best Practices

  • • Middleware files in app/middleware/ are loaded automatically — no imports needed
  • • Use order to control execution sequence (lower runs first)
  • • Use global_only: true for middleware that must run on every request (CORS, security headers)
  • • Use scope_only: true for authentication to prevent accidental global application
  • • Keep global middleware lightweight; expensive operations belong in scoped middleware

Next Steps