Request Handling Guide

Master request processing in Squirrel, including parsing forms, handling JSON, working with headers, and file uploads.

JSONFile UploadForm Data
Reading Request Data
Access different types of request data (query params, headers, body)
package main

import (
    "fmt"
    "net/http"
    "strconv"
    "github.com/squirrel-land/squirrel"
)

func searchHandler(w http.ResponseWriter, r *http.Request) {
    // Get query parameters
    query := r.URL.Query()
    
    // Single value
    searchTerm := query.Get("q")
    if searchTerm == "" {
        http.Error(w, "Missing search query", http.StatusBadRequest)
        return
    }
    
    // Multiple values
    categories := query["category"] // []string
    
    // With default value
    pageStr := query.Get("page")
    page := 1
    if pageStr != "" {
        if p, err := strconv.Atoi(pageStr); err == nil {
            page = p
        }
    }
    
    // Limit with validation
    limitStr := query.Get("limit")
    limit := 10 // default
    if limitStr != "" {
        if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
            limit = l
        }
    }
    
    fmt.Fprintf(w, "Search: %s, Page: %d, Limit: %d, Categories: %v", 
        searchTerm, page, limit, categories)
}

func main() {
    mux := squirrel.NewSqurlMux()
    
    // Example: /search?q=golang&category=web&category=api&page=2&limit=20
    mux.HandleFunc("/search", searchHandler)
    
    http.ListenAndServe(":8080", mux)
}
JSON Request Handling
Parse and validate JSON request bodies
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "github.com/squirrel-land/squirrel"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name" validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=18,max=120"`
}

type ErrorResponse struct {
    Error   string            `json:"error"`
    Details map[string]string `json:"details,omitempty"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    // Ensure content type is JSON
    if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
        writeErrorResponse(w, "Content-Type must be application/json", http.StatusBadRequest)
        return
    }
    
    // Limit request body size (1MB)
    r.Body = http.MaxBytesReader(w, r.Body, 1048576)
    
    var user User
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields() // Reject unknown fields
    
    if err := decoder.Decode(&user); err != nil {
        var errorMsg string
        switch {
        case strings.Contains(err.Error(), "unknown field"):
            errorMsg = "Unknown field in JSON"
        case strings.Contains(err.Error(), "invalid character"):
            errorMsg = "Invalid JSON format"
        default:
            errorMsg = "Error parsing JSON"
        }
        writeErrorResponse(w, errorMsg, http.StatusBadRequest)
        return
    }
    
    // Validate required fields
    if user.Name == "" {
        writeErrorResponse(w, "Name is required", http.StatusBadRequest)
        return
    }
    
    if user.Email == "" {
        writeErrorResponse(w, "Email is required", http.StatusBadRequest)
        return
    }
    
    // Basic email validation
    if !strings.Contains(user.Email, "@") {
        writeErrorResponse(w, "Invalid email format", http.StatusBadRequest)
        return
    }
    
    // Age validation
    if user.Age < 18 || user.Age > 120 {
        writeErrorResponse(w, "Age must be between 18 and 120", http.StatusBadRequest)
        return
    }
    
    // Simulate saving user (assign ID)
    user.ID = 123
    
    // Return created user
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func writeErrorResponse(w http.ResponseWriter, message string, statusCode int) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}

// Batch processing example
func batchCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
    var users []User
    
    if err := json.NewDecoder(r.Body).Decode(&users); err != nil {
        writeErrorResponse(w, "Invalid JSON array", http.StatusBadRequest)
        return
    }
    
    if len(users) == 0 {
        writeErrorResponse(w, "Empty user array", http.StatusBadRequest)
        return
    }
    
    if len(users) > 100 {
        writeErrorResponse(w, "Too many users (max 100)", http.StatusBadRequest)
        return
    }
    
    // Process each user
    var createdUsers []User
    var errors []string
    
    for i, user := range users {
        if user.Name == "" || user.Email == "" {
            errors = append(errors, fmt.Sprintf("User %d: missing required fields", i))
            continue
        }
        
        user.ID = 1000 + i // Assign ID
        createdUsers = append(createdUsers, user)
    }
    
    response := map[string]interface{}{
        "created": createdUsers,
        "errors":  errors,
        "total":   len(users),
        "success": len(createdUsers),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func main() {
    mux := squirrel.NewSqurlMux()
    
    mux.Post("/users", createUserHandler)
    mux.Post("/users/batch", batchCreateUsersHandler)
    
    http.ListenAndServe(":8080", mux)
}
Form Data Handling
Process HTML forms and URL-encoded data
package main

import (
    "fmt"
    "net/http"
    "strconv"
    "github.com/squirrel-land/squirrel"
)

func contactFormHandler(w http.ResponseWriter, r *http.Request) {
    // Parse form data
    if err := r.ParseForm(); err != nil {
        http.Error(w, "Error parsing form", http.StatusBadRequest)
        return
    }
    
    // Get form values
    name := r.FormValue("name")
    email := r.FormValue("email")
    message := r.FormValue("message")
    
    // Get multiple values (checkboxes, multiple selects)
    interests := r.Form["interests"] // []string
    
    // Get with default value
    newsletter := r.FormValue("newsletter") == "on"
    
    // Convert string to int
    ageStr := r.FormValue("age")
    age := 0
    if ageStr != "" {
        if a, err := strconv.Atoi(ageStr); err == nil {
            age = a
        }
    }
    
    response := fmt.Sprintf(`Form Data Received:
Name: %s
Email: %s
Age: %d
Message: %s
Newsletter: %t
Interests: %v`, name, email, age, message, newsletter, interests)
    
    fmt.Fprint(w, response)
}

// Handle both GET (show form) and POST (process form)
func userFormHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // Show HTML form
        html := `<!DOCTYPE html>
<html>
<head><title>User Form</title></head>
<body>
    <form method="POST">
        <label>Name: <input type="text" name="name" required></label><br>
        <label>Email: <input type="email" name="email" required></label><br>
        <label>Age: <input type="number" name="age" min="18"></label><br>
        <label>Message: <textarea name="message"></textarea></label><br>
        <label><input type="checkbox" name="newsletter"> Subscribe to newsletter</label><br>
        <label>Interests:</label><br>
        <label><input type="checkbox" name="interests" value="web"> Web Development</label><br>
        <label><input type="checkbox" name="interests" value="mobile"> Mobile Development</label><br>
        <label><input type="checkbox" name="interests" value="ai"> AI/ML</label><br>
        <button type="submit">Submit</button>
    </form>
</body>
</html>`
        w.Header().Set("Content-Type", "text/html")
        fmt.Fprint(w, html)
        
    case http.MethodPost:
        contactFormHandler(w, r)
        
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    mux := squirrel.NewSqurlMux()
    
    mux.HandleFunc("/contact", contactFormHandler)
    mux.HandleFunc("/form", userFormHandler)
    
    http.ListenAndServe(":8080", mux)
}
File Upload Handling
Handle single and multiple file uploads with validation
package main

import (
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
    "strings"
    "github.com/squirrel-land/squirrel"
)

const (
    MaxFileSize = 10 << 20 // 10MB
    UploadDir   = "./uploads"
)

func init() {
    // Create upload directory if it doesn't exist
    os.MkdirAll(UploadDir, 0755)
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    // Limit request size
    r.Body = http.MaxBytesReader(w, r.Body, MaxFileSize)
    
    // Parse multipart form
    if err := r.ParseMultipartForm(MaxFileSize); err != nil {
        http.Error(w, "File too large", http.StatusBadRequest)
        return
    }
    
    // Get file from form
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "Error retrieving file", http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // Validate file
    if err := validateFile(header); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Create unique filename
    filename := generateUniqueFilename(header.Filename)
    filepath := filepath.Join(UploadDir, filename)
    
    // Create destination file
    dst, err := os.Create(filepath)
    if err != nil {
        http.Error(w, "Error creating file", http.StatusInternalServerError)
        return
    }
    defer dst.Close()
    
    // Copy file content
    if _, err := io.Copy(dst, file); err != nil {
        http.Error(w, "Error saving file", http.StatusInternalServerError)
        return
    }
    
    // Return success response
    response := map[string]interface{}{
        "success":  true,
        "filename": filename,
        "size":     header.Size,
        "message":  "File uploaded successfully",
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func validateFile(header *multipart.FileHeader) error {
    // Check file size
    if header.Size > MaxFileSize {
        return fmt.Errorf("file too large (max %d bytes)", MaxFileSize)
    }
    
    if header.Size == 0 {
        return fmt.Errorf("empty file")
    }
    
    // Check file extension
    allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx"}
    ext := strings.ToLower(filepath.Ext(header.Filename))
    
    allowed := false
    for _, allowedExt := range allowedExts {
        if ext == allowedExt {
            allowed = true
            break
        }
    }
    
    if !allowed {
        return fmt.Errorf("file type not allowed: %s", ext)
    }
    
    return nil
}

func generateUniqueFilename(originalName string) string {
    ext := filepath.Ext(originalName)
    name := strings.TrimSuffix(originalName, ext)
    
    // Clean filename
    name = strings.ReplaceAll(name, " ", "_")
    name = regexp.MustCompile(`[^a-zA-Z0-9_-]`).ReplaceAllString(name, "")
    
    // Add timestamp for uniqueness
    timestamp := time.Now().Unix()
    return fmt.Sprintf("%s_%d%s", name, timestamp, ext)
}

// Multiple file upload
func multipleUploadHandler(w http.ResponseWriter, r *http.Request) {
    r.Body = http.MaxBytesReader(w, r.Body, MaxFileSize*5) // Allow up to 5 files
    
    if err := r.ParseMultipartForm(MaxFileSize * 5); err != nil {
        http.Error(w, "Request too large", http.StatusBadRequest)
        return
    }
    
    files := r.MultipartForm.File["files"]
    if len(files) == 0 {
        http.Error(w, "No files provided", http.StatusBadRequest)
        return
    }
    
    if len(files) > 5 {
        http.Error(w, "Too many files (max 5)", http.StatusBadRequest)
        return
    }
    
    var uploadedFiles []map[string]interface{}
    var errors []string
    
    for i, fileHeader := range files {
        file, err := fileHeader.Open()
        if err != nil {
            errors = append(errors, fmt.Sprintf("File %d: error opening file", i))
            continue
        }
        
        if err := validateFile(fileHeader); err != nil {
            errors = append(errors, fmt.Sprintf("File %d: %s", i, err.Error()))
            file.Close()
            continue
        }
        
        filename := generateUniqueFilename(fileHeader.Filename)
        filepath := filepath.Join(UploadDir, filename)
        
        dst, err := os.Create(filepath)
        if err != nil {
            errors = append(errors, fmt.Sprintf("File %d: error creating file", i))
            file.Close()
            continue
        }
        
        if _, err := io.Copy(dst, file); err != nil {
            errors = append(errors, fmt.Sprintf("File %d: error saving file", i))
            dst.Close()
            file.Close()
            continue
        }
        
        uploadedFiles = append(uploadedFiles, map[string]interface{}{
            "original": fileHeader.Filename,
            "filename": filename,
            "size":     fileHeader.Size,
        })
        
        dst.Close()
        file.Close()
    }
    
    response := map[string]interface{}{
        "success":       len(uploadedFiles) > 0,
        "uploaded":      uploadedFiles,
        "errors":        errors,
        "total_files":   len(files),
        "success_count": len(uploadedFiles),
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func main() {
    mux := squirrel.NewSqurlMux()
    
    mux.Post("/upload", uploadHandler)
    mux.Post("/upload/multiple", multipleUploadHandler)
    
    // Serve uploaded files
    mux.HandleFunc("/files/", http.StripPrefix("/files/", 
        http.FileServer(http.Dir(UploadDir))).ServeHTTP)
    
    http.ListenAndServe(":8080", mux)
}