Skip to main content
โšก Calmops

Go + Fetch API: Setting and Sending Cookies Across Origins

Introduction

Setting cookies in a cross-origin setup (Go backend on port 4000, JavaScript frontend on port 3000) requires careful configuration on both sides. The Fetch API doesn’t send or accept cookies by default โ€” you must explicitly enable credentials: 'include'. The Go server must also set the correct CORS headers.

Go Backend: Setting Cookies

package main

import (
    "net/http"
    "time"
)

func loginHandler(w http.ResponseWriter, r *http.Request) {
    // Authenticate user...

    // Set the session cookie
    cookie := &http.Cookie{
        Name:     "session_token",
        Value:    "abc123xyz",
        Path:     "/",
        HttpOnly: true,                          // not accessible via JavaScript
        Secure:   true,                          // HTTPS only (required in production)
        SameSite: http.SameSiteLaxMode,          // CSRF protection
        MaxAge:   int((24 * time.Hour).Seconds()), // 24 hours
    }

    http.SetCookie(w, cookie)
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"status": "logged in"}`))
}
Attribute Purpose Recommendation
HttpOnly Prevents JavaScript access (XSS protection) Always set to true
Secure Only sent over HTTPS true in production, false in local dev
SameSite Controls cross-site sending Lax for most apps, Strict for sensitive
MaxAge Expiry in seconds Set appropriate lifetime
Path Which paths the cookie applies to Usually /
Domain Which domains receive the cookie Leave empty for current domain

SameSite Values

http.SameSiteStrictMode  // Never sent cross-site (most secure, breaks some flows)
http.SameSiteLaxMode     // Sent on top-level navigation (recommended default)
http.SameSiteNoneMode    // Always sent cross-site (requires Secure=true)

For cross-origin requests (frontend on different domain/port), you need SameSiteNoneMode with Secure: true:

cookie := &http.Cookie{
    Name:     "session_token",
    Value:    token,
    Path:     "/",
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteNoneMode,  // required for cross-origin
    MaxAge:   86400,
}

CORS Configuration in Go

For cross-origin requests with cookies, the server must set specific CORS headers:

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")

        // Allow specific origins (never use "*" with credentials)
        allowedOrigins := map[string]bool{
            "http://localhost:3000":  true,
            "https://myapp.com":     true,
        }

        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
        }

        // CRITICAL: must be "true" (not "*") when using credentials
        w.Header().Set("Access-Control-Allow-Credentials", "true")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        // Handle preflight requests
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/login", loginHandler)
    mux.HandleFunc("/profile", profileHandler)

    handler := corsMiddleware(mux)
    http.ListenAndServe(":4000", handler)
}

Critical rule: When credentials: 'include' is used, Access-Control-Allow-Origin cannot be * โ€” it must be the specific origin.

JavaScript Frontend: Fetch with Credentials

// Login request โ€” sends credentials and accepts cookies
async function login(email, password) {
    const response = await fetch('http://localhost:4000/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify({ email, password }),
        credentials: 'include',  // CRITICAL: send and accept cookies
        mode: 'cors',
        cache: 'no-cache',
    });

    if (!response.ok) {
        throw new Error(`Login failed: ${response.status}`);
    }

    return response.json();
}

// Subsequent authenticated request โ€” cookie sent automatically
async function getProfile() {
    const response = await fetch('http://localhost:4000/profile', {
        credentials: 'include',  // sends the session cookie
    });

    return response.json();
}

// Logout โ€” clear the cookie
async function logout() {
    await fetch('http://localhost:4000/logout', {
        method: 'POST',
        credentials: 'include',
    });
}

credentials Options

Value Behavior
'omit' Never send cookies (default)
'same-origin' Send cookies only to same origin
'include' Always send cookies (required for cross-origin)

Reading Cookies in Go

func profileHandler(w http.ResponseWriter, r *http.Request) {
    // Read a specific cookie
    cookie, err := r.Cookie("session_token")
    if err != nil {
        if err == http.ErrNoCookie {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    token := cookie.Value
    // Validate token and get user...

    w.Write([]byte(`{"user": "alice"}`))
}

Deleting Cookies

To delete a cookie, set it with MaxAge: -1 or Expires in the past:

func logoutHandler(w http.ResponseWriter, r *http.Request) {
    // Delete the session cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "session_token",
        Value:    "",
        Path:     "/",
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
        MaxAge:   -1,  // delete immediately
    })

    w.Write([]byte(`{"status": "logged out"}`))
}

Complete Example

package main

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

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

func main() {
    mux := http.NewServeMux()

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

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

        // Validate credentials (simplified)
        if req.Email != "[email protected]" || req.Password != "password" {
            http.Error(w, "Invalid credentials", http.StatusUnauthorized)
            return
        }

        // Set session cookie
        http.SetCookie(w, &http.Cookie{
            Name:     "session",
            Value:    "user-session-token-here",
            Path:     "/",
            HttpOnly: true,
            Secure:   false, // true in production
            SameSite: http.SameSiteLaxMode,
            MaxAge:   int((24 * time.Hour).Seconds()),
        })

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    mux.HandleFunc("/me", func(w http.ResponseWriter, r *http.Request) {
        cookie, err := r.Cookie("session")
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Validate session token...
        _ = cookie.Value

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"email": "[email protected]"})
    })

    http.ListenAndServe(":4000", corsMiddleware(mux))
}

Troubleshooting

Problem Cause Fix
Cookie not set Secure=true on HTTP Use HTTPS or set Secure=false in dev
Cookie not sent Missing credentials: 'include' Add to all Fetch calls
CORS error Allow-Origin: * with credentials Use specific origin, not *
Cookie blocked SameSite=Strict cross-origin Use SameSite=None; Secure
Cookie not visible in DevTools HttpOnly=true Expected โ€” HttpOnly hides from JS

Resources

Comments