Table of contents

How do I handle form submissions and POST requests in Go?

Handling form submissions and POST requests is a fundamental aspect of web development in Go. Whether you're building web scrapers, APIs, or web applications, understanding how to properly handle different types of POST requests is crucial for effective data processing and user interaction.

Understanding POST Requests in Go

POST requests allow clients to send data to a server, making them essential for form submissions, file uploads, and API interactions. Go's net/http package provides robust tools for handling these requests efficiently.

Basic Form Handling

Simple HTML Form Processing

Here's how to handle a basic HTML form submission:

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
)

// Form data structure
type ContactForm struct {
    Name    string
    Email   string
    Message string
}

func main() {
    http.HandleFunc("/", showForm)
    http.HandleFunc("/submit", handleFormSubmission)

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func showForm(w http.ResponseWriter, r *http.Request) {
    html := `
    <!DOCTYPE html>
    <html>
    <head><title>Contact Form</title></head>
    <body>
        <form method="POST" action="/submit">
            <label>Name: <input type="text" name="name" required></label><br>
            <label>Email: <input type="email" name="email" required></label><br>
            <label>Message: <textarea name="message" required></textarea></label><br>
            <button type="submit">Submit</button>
        </form>
    </body>
    </html>`

    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, html)
}

func handleFormSubmission(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Parse form data
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "Error parsing form", http.StatusBadRequest)
        return
    }

    // Extract form values
    form := ContactForm{
        Name:    r.FormValue("name"),
        Email:   r.FormValue("email"),
        Message: r.FormValue("message"),
    }

    // Validate required fields
    if form.Name == "" || form.Email == "" || form.Message == "" {
        http.Error(w, "All fields are required", http.StatusBadRequest)
        return
    }

    // Process the form data
    fmt.Printf("Received form submission: %+v\n", form)

    // Send response
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"status": "success", "message": "Form submitted successfully"}`)
}

Advanced Form Validation

For more robust form handling with validation:

package main

import (
    "encoding/json"
    "net/http"
    "regexp"
    "strings"
)

type User struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

type Response struct {
    Success bool              `json:"success"`
    Data    interface{}       `json:"data,omitempty"`
    Errors  []ValidationError `json:"errors,omitempty"`
}

func validateUser(user User) []ValidationError {
    var errors []ValidationError

    // Username validation
    if len(user.Username) < 3 {
        errors = append(errors, ValidationError{
            Field:   "username",
            Message: "Username must be at least 3 characters long",
        })
    }

    // Email validation
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(user.Email) {
        errors = append(errors, ValidationError{
            Field:   "email",
            Message: "Invalid email format",
        })
    }

    // Password validation
    if len(user.Password) < 8 {
        errors = append(errors, ValidationError{
            Field:   "password",
            Message: "Password must be at least 8 characters long",
        })
    }

    return errors
}

func handleUserRegistration(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User

    // Check content type
    contentType := r.Header.Get("Content-Type")

    if strings.Contains(contentType, "application/json") {
        // Handle JSON request
        decoder := json.NewDecoder(r.Body)
        if err := decoder.Decode(&user); err != nil {
            response := Response{
                Success: false,
                Errors: []ValidationError{{
                    Field:   "body",
                    Message: "Invalid JSON format",
                }},
            }
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(response)
            return
        }
    } else {
        // Handle form data
        if err := r.ParseForm(); err != nil {
            http.Error(w, "Error parsing form", http.StatusBadRequest)
            return
        }

        user = User{
            Username: r.FormValue("username"),
            Email:    r.FormValue("email"),
            Password: r.FormValue("password"),
        }
    }

    // Validate user data
    if validationErrors := validateUser(user); len(validationErrors) > 0 {
        response := Response{
            Success: false,
            Errors:  validationErrors,
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(response)
        return
    }

    // Process successful registration
    // (In real application, save to database, hash password, etc.)

    response := Response{
        Success: true,
        Data: map[string]string{
            "message": "User registered successfully",
            "user_id": "12345", // Generated user ID
        },
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(response)
}

Handling File Uploads

Multipart Form Data

File uploads require special handling using multipart forms:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strings"
)

func handleFileUpload(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Parse multipart form (32MB max memory)
    err := r.ParseMultipartForm(32 << 20)
    if err != nil {
        http.Error(w, "Error parsing multipart form", http.StatusBadRequest)
        return
    }

    // Get file from form
    file, header, err := r.FormFile("upload")
    if err != nil {
        http.Error(w, "Error retrieving file", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // Validate file type
    allowedTypes := map[string]bool{
        ".jpg":  true,
        ".jpeg": true,
        ".png":  true,
        ".gif":  true,
        ".pdf":  true,
    }

    ext := strings.ToLower(filepath.Ext(header.Filename))
    if !allowedTypes[ext] {
        http.Error(w, "File type not allowed", http.StatusBadRequest)
        return
    }

    // Create uploads directory if it doesn't exist
    uploadDir := "./uploads"
    if err := os.MkdirAll(uploadDir, 0755); err != nil {
        http.Error(w, "Error creating upload directory", http.StatusInternalServerError)
        return
    }

    // Create destination file
    dstPath := filepath.Join(uploadDir, header.Filename)
    dst, err := os.Create(dstPath)
    if err != nil {
        http.Error(w, "Error creating destination 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
    }

    // Get other form fields
    description := r.FormValue("description")
    category := r.FormValue("category")

    fmt.Fprintf(w, "File uploaded successfully!\n")
    fmt.Fprintf(w, "Filename: %s\n", header.Filename)
    fmt.Fprintf(w, "Size: %d bytes\n", header.Size)
    fmt.Fprintf(w, "Description: %s\n", description)
    fmt.Fprintf(w, "Category: %s\n", category)
}

Making POST Requests as a Client

Simple POST Request

Here's how to make POST requests to external services:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "strings"
)

// User represents user data for API requests
type APIUser struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Response represents API response
type APIResponse struct {
    Success bool        `json:"success"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

func postJSONData() error {
    // Prepare data
    user := APIUser{
        Name:  "John Doe",
        Email: "john@example.com",
        Age:   30,
    }

    // Convert to JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        return fmt.Errorf("error marshaling JSON: %v", err)
    }

    // Create POST request
    resp, err := http.Post(
        "https://api.example.com/users",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()

    // Read response
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("error reading response: %v", err)
    }

    // Parse response
    var apiResp APIResponse
    if err := json.Unmarshal(body, &apiResp); err != nil {
        return fmt.Errorf("error parsing response: %v", err)
    }

    fmt.Printf("Response: %+v\n", apiResp)
    return nil
}

func postFormData() error {
    // Prepare form data
    formData := url.Values{
        "name":     {"John Doe"},
        "email":    {"john@example.com"},
        "message":  {"Hello from Go!"},
    }

    // Create POST request with form data
    resp, err := http.PostForm("https://api.example.com/contact", formData)
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()

    // Read response
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("error reading response: %v", err)
    }

    fmt.Printf("Status: %s\n", resp.Status)
    fmt.Printf("Response: %s\n", string(body))
    return nil
}

func postWithCustomHeaders() error {
    // Prepare JSON data
    data := map[string]interface{}{
        "action": "create_user",
        "payload": map[string]string{
            "username": "newuser",
            "email":    "user@example.com",
        },
    }

    jsonData, _ := json.Marshal(data)

    // Create request
    req, err := http.NewRequest("POST", "https://api.example.com/action", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("error creating request: %v", err)
    }

    // Set headers
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer your-api-token")
    req.Header.Set("User-Agent", "Go-WebScraper/1.0")
    req.Header.Set("X-API-Version", "v1")

    // Make request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Response: %s\n", string(body))
    return nil
}

Advanced POST Request Handling

Authentication and Session Management

For applications requiring authentication, similar to how you might handle authentication in Puppeteer:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "time"
)

type LoginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type LoginResponse struct {
    Success bool   `json:"success"`
    Token   string `json:"token,omitempty"`
    Message string `json:"message,omitempty"`
}

type Claims struct {
    Username string    `json:"username"`
    Exp      time.Time  `json:"exp"`
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var loginReq LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // Validate credentials (in real app, check against database)
    if loginReq.Username == "admin" && loginReq.Password == "password123" {
        // Generate token (in real app, use JWT or similar)
        token := generateSessionToken(loginReq.Username)

        response := LoginResponse{
            Success: true,
            Token:   token,
            Message: "Login successful",
        }

        // Set secure cookie
        http.SetCookie(w, &http.Cookie{
            Name:     "session_token",
            Value:    token,
            Expires:  time.Now().Add(24 * time.Hour),
            HttpOnly: true,
            Secure:   true, // Use in production with HTTPS
            SameSite: http.SameSiteStrictMode,
        })

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(response)
    } else {
        response := LoginResponse{
            Success: false,
            Message: "Invalid credentials",
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusUnauthorized)
        json.NewEncoder(w).Encode(response)
    }
}

func generateSessionToken(username string) string {
    // In production, use proper JWT or secure session tokens
    return fmt.Sprintf("token_%s_%d", username, time.Now().Unix())
}

// Middleware for protected routes
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            // Check cookie as fallback
            if cookie, err := r.Cookie("session_token"); err == nil {
                token = cookie.Value
            }
        }

        if token == "" || !isValidToken(token) {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        next(w, r)
    }
}

func isValidToken(token string) bool {
    // In production, validate JWT or check session store
    return strings.HasPrefix(token, "token_")
}

CSRF Protection

Implementing CSRF protection for form submissions:

package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type CSRFManager struct {
    tokens map[string]time.Time
    mutex  sync.RWMutex
}

func NewCSRFManager() *CSRFManager {
    manager := &CSRFManager{
        tokens: make(map[string]time.Time),
    }

    // Clean expired tokens every hour
    go manager.cleanupExpiredTokens()

    return manager
}

func (c *CSRFManager) GenerateToken() string {
    bytes := make([]byte, 32)
    rand.Read(bytes)
    token := hex.EncodeToString(bytes)

    c.mutex.Lock()
    c.tokens[token] = time.Now().Add(1 * time.Hour)
    c.mutex.Unlock()

    return token
}

func (c *CSRFManager) ValidateToken(token string) bool {
    c.mutex.RLock()
    expiry, exists := c.tokens[token]
    c.mutex.RUnlock()

    if !exists {
        return false
    }

    if time.Now().After(expiry) {
        c.mutex.Lock()
        delete(c.tokens, token)
        c.mutex.Unlock()
        return false
    }

    return true
}

func (c *CSRFManager) cleanupExpiredTokens() {
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()

    for range ticker.C {
        c.mutex.Lock()
        now := time.Now()
        for token, expiry := range c.tokens {
            if now.After(expiry) {
                delete(c.tokens, token)
            }
        }
        c.mutex.Unlock()
    }
}

var csrfManager = NewCSRFManager()

func showProtectedForm(w http.ResponseWriter, r *http.Request) {
    csrfToken := csrfManager.GenerateToken()

    html := fmt.Sprintf(`
    <!DOCTYPE html>
    <html>
    <head><title>Protected Form</title></head>
    <body>
        <form method="POST" action="/protected-submit">
            <input type="hidden" name="csrf_token" value="%s">
            <label>Message: <textarea name="message" required></textarea></label><br>
            <button type="submit">Submit</button>
        </form>
    </body>
    </html>`, csrfToken)

    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, html)
}

func handleProtectedSubmit(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    if err := r.ParseForm(); err != nil {
        http.Error(w, "Error parsing form", http.StatusBadRequest)
        return
    }

    // Validate CSRF token
    csrfToken := r.FormValue("csrf_token")
    if !csrfManager.ValidateToken(csrfToken) {
        http.Error(w, "Invalid CSRF token", http.StatusForbidden)
        return
    }

    message := r.FormValue("message")
    fmt.Fprintf(w, "Protected form submitted successfully! Message: %s", message)
}

Best Practices and Security Considerations

Input Sanitization and Validation

Always validate and sanitize user input to prevent security vulnerabilities:

package main

import (
    "html"
    "regexp"
    "strings"
    "unicode/utf8"
)

func sanitizeInput(input string) string {
    // Remove potentially dangerous characters
    input = html.EscapeString(input)

    // Trim whitespace
    input = strings.TrimSpace(input)

    // Remove null bytes
    input = strings.ReplaceAll(input, "\x00", "")

    return input
}

func validateEmail(email string) bool {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    return emailRegex.MatchString(email)
}

func validateStringLength(str string, minLen, maxLen int) bool {
    length := utf8.RuneCountInString(str)
    return length >= minLen && length <= maxLen
}

func validateAlphanumeric(str string) bool {
    alphanumericRegex := regexp.MustCompile(`^[a-zA-Z0-9]+$`)
    return alphanumericRegex.MatchString(str)
}

Rate Limiting

Implement rate limiting to prevent abuse, especially useful when dealing with form submissions or when making requests to external APIs:

package main

import (
    "net/http"
    "sync"
    "time"
)

type RateLimiter struct {
    clients map[string][]time.Time
    mutex   sync.RWMutex
    limit   int
    window  time.Duration
}

func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
    return &RateLimiter{
        clients: make(map[string][]time.Time),
        limit:   limit,
        window:  window,
    }
}

func (rl *RateLimiter) Allow(clientIP string) bool {
    rl.mutex.Lock()
    defer rl.mutex.Unlock()

    now := time.Now()
    windowStart := now.Add(-rl.window)

    // Get or create client record
    requests, exists := rl.clients[clientIP]
    if !exists {
        requests = make([]time.Time, 0)
    }

    // Filter out old requests
    validRequests := make([]time.Time, 0)
    for _, requestTime := range requests {
        if requestTime.After(windowStart) {
            validRequests = append(validRequests, requestTime)
        }
    }

    // Check if limit exceeded
    if len(validRequests) >= rl.limit {
        rl.clients[clientIP] = validRequests
        return false
    }

    // Add current request
    validRequests = append(validRequests, now)
    rl.clients[clientIP] = validRequests

    return true
}

// Middleware for rate limiting
func rateLimitMiddleware(rateLimiter *RateLimiter) func(http.HandlerFunc) http.HandlerFunc {
    return func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            clientIP := r.RemoteAddr

            if !rateLimiter.Allow(clientIP) {
                http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
                return
            }

            next(w, r)
        }
    }
}

Testing POST Requests

Unit Testing Form Handlers

Testing your POST request handlers is crucial for ensuring reliability:

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "net/url"
    "strings"
    "testing"
)

func TestHandleFormSubmission(t *testing.T) {
    // Test valid form submission
    formData := url.Values{
        "name":    {"John Doe"},
        "email":   {"john@example.com"},
        "message": {"Test message"},
    }

    req := httptest.NewRequest("POST", "/submit", strings.NewReader(formData.Encode()))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    w := httptest.NewRecorder()
    handleFormSubmission(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("Expected status 200, got %d", w.Code)
    }

    // Test missing fields
    invalidData := url.Values{
        "name": {"John Doe"},
        // Missing email and message
    }

    req2 := httptest.NewRequest("POST", "/submit", strings.NewReader(invalidData.Encode()))
    req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    w2 := httptest.NewRecorder()
    handleFormSubmission(w2, req2)

    if w2.Code != http.StatusBadRequest {
        t.Errorf("Expected status 400, got %d", w2.Code)
    }
}

func TestHandleUserRegistration(t *testing.T) {
    // Test valid JSON request
    user := User{
        Username: "testuser",
        Email:    "test@example.com",
        Password: "password123",
    }

    jsonData, _ := json.Marshal(user)

    req := httptest.NewRequest("POST", "/register", bytes.NewBuffer(jsonData))
    req.Header.Set("Content-Type", "application/json")

    w := httptest.NewRecorder()
    handleUserRegistration(w, req)

    if w.Code != http.StatusCreated {
        t.Errorf("Expected status 201, got %d", w.Code)
    }

    var response Response
    json.Unmarshal(w.Body.Bytes(), &response)

    if !response.Success {
        t.Errorf("Expected success true, got %v", response.Success)
    }
}

Conclusion

Handling form submissions and POST requests in Go requires understanding the different types of data formats, proper validation, security considerations, and error handling. By implementing these patterns and best practices, you can build robust web applications and APIs that handle user input securely and efficiently.

Whether you're processing simple contact forms, handling file uploads, or integrating with external APIs, Go's net/http package provides all the tools necessary for comprehensive POST request handling. Remember to always validate input, implement proper error handling, and consider security measures like CSRF protection and rate limiting in production applications.

For complex scenarios involving browser automation and form interactions, you might also want to explore how to handle browser sessions in Puppeteer or learn about handling AJAX requests using Puppeteer for more advanced web scraping and testing scenarios.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon