Skip to main content
The TLS Client includes a built-in cookie jar that automatically handles cookies across requests, similar to how browsers work. This guide covers how to use the cookie jar effectively. Always create a cookie jar to maintain session state:
import (
    tls_client "github.com/bogdanfinn/tls-client"
    "github.com/bogdanfinn/tls-client/profiles"
)

jar := tls_client.NewCookieJar()

options := []tls_client.HttpClientOption{
    tls_client.WithTimeoutSeconds(30),
    tls_client.WithClientProfile(profiles.Chrome_133),
    tls_client.WithCookieJar(jar),
}

client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
if err != nil {
    log.Fatal(err)
}
Without a cookie jar, cookies from server responses are not stored and cookies are not sent with subsequent requests.

Skip existing cookies

Prevent overwriting existing cookies with the same name:
jar := tls_client.NewCookieJar(
    tls_client.WithSkipExisting(),
)

tls_client.WithCookieJar(jar)
This is useful when you want to preserve manually set cookies even if the server tries to update them. The cookie jar automatically handles cookies from server responses:
import http "github.com/bogdanfinn/fhttp"

// First request - server sets cookies
req, err := http.NewRequest(http.MethodGet, "https://example.com/login", nil)
if err != nil {
    log.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

// Cookies from Set-Cookie header are automatically stored

// Second request - cookies are automatically sent
req2, err := http.NewRequest(http.MethodGet, "https://example.com/dashboard", nil)
if err != nil {
    log.Fatal(err)
}

resp2, err := client.Do(req2)
// Cookies are included in the Cookie header automatically

Getting cookies

Retrieve cookies for a specific URL:
import "net/url"

u, err := url.Parse("https://example.com")
if err != nil {
    log.Fatal(err)
}

cookies := client.GetCookies(u)

for _, cookie := range cookies {
    log.Printf("Name: %s, Value: %s, Domain: %s, Path: %s",
        cookie.Name,
        cookie.Value,
        cookie.Domain,
        cookie.Path,
    )
}
The URL’s domain and path determine which cookies are returned. Only cookies that match the URL’s domain and path are included.

Setting cookies manually

Set cookies programmatically before making requests:
import (
    http "github.com/bogdanfinn/fhttp"
    "net/url"
)

u, err := url.Parse("https://example.com")
if err != nil {
    log.Fatal(err)
}

cookies := []*http.Cookie{
    {
        Name:   "session_id",
        Value:  "abc123",
        Path:   "/",
        Domain: "example.com",
        MaxAge: 3600,
    },
    {
        Name:   "user_pref",
        Value:  "dark_mode",
        Path:   "/",
        Domain: "example.com",
    },
}

client.SetCookies(u, cookies)

// Now make request - cookies will be sent automatically
resp, err := client.Get("https://example.com/dashboard")
When creating cookies, you can set various attributes:
cookie := &http.Cookie{
    Name:   "session",
    Value:  "xyz789",
    
    // Domain and path matching
    Domain: ".example.com",  // Matches all subdomains
    Path:   "/",             // Matches all paths
    
    // Expiration
    MaxAge: 86400,           // 24 hours in seconds
    // Or use Expires:
    // Expires: time.Now().Add(24 * time.Hour),
    
    // Security flags
    Secure:   true,          // Only sent over HTTPS
    HttpOnly: true,          // Not accessible to JavaScript
    SameSite: http.SameSiteStrictMode,  // CSRF protection
}

client.SetCookies(u, []*http.Cookie{cookie})
Replace the entire cookie jar to clear all cookies:
// Create a new empty jar
newJar := tls_client.NewCookieJar()

// Replace the client's jar
client.SetCookieJar(newJar)

// All previous cookies are now cleared

Getting the current jar

Retrieve the current cookie jar:
currentJar := client.GetCookieJar()

// Use the jar with another client
newClient.SetCookieJar(currentJar)

Expired cookies

The cookie jar automatically filters out expired cookies:
import "net/url"

u, err := url.Parse("https://example.com")
if err != nil {
    log.Fatal(err)
}

// Set a cookie that expires immediately
expiredCookie := &http.Cookie{
    Name:   "temp",
    Value:  "data",
    MaxAge: -1,  // Already expired
}

client.SetCookies(u, []*http.Cookie{expiredCookie})

// Get cookies - expired cookie won't be included
cookies := client.GetCookies(u)
log.Printf("Active cookies: %d", len(cookies))  // Won't include expired cookie

Complete example: Login session

A practical example showing cookie-based authentication:
package main

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

    http "github.com/bogdanfinn/fhttp"
    tls_client "github.com/bogdanfinn/tls-client"
    "github.com/bogdanfinn/tls-client/profiles"
)

func main() {
    // Create client with cookie jar
    jar := tls_client.NewCookieJar()
    
    options := []tls_client.HttpClientOption{
        tls_client.WithTimeoutSeconds(30),
        tls_client.WithClientProfile(profiles.Chrome_133),
        tls_client.WithCookieJar(jar),
    }
    
    client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
    if err != nil {
        log.Fatal(err)
    }

    // Step 1: Login
    loginData := map[string]string{
        "username": "[email protected]",
        "password": "secret123",
    }
    
    jsonData, err := json.Marshal(loginData)
    if err != nil {
        log.Fatal(err)
    }
    
    loginReq, err := http.NewRequest(
        http.MethodPost,
        "https://api.example.com/auth/login",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    loginReq.Header.Set("Content-Type", "application/json")
    
    loginResp, err := client.Do(loginReq)
    if err != nil {
        log.Fatal(err)
    }
    defer loginResp.Body.Close()
    
    if loginResp.StatusCode != 200 {
        log.Fatalf("Login failed with status: %d", loginResp.StatusCode)
    }
    
    log.Println("Login successful")
    
    // Check cookies set by server
    u, _ := url.Parse("https://api.example.com")
    cookies := client.GetCookies(u)
    
    log.Printf("Received %d cookies:", len(cookies))
    for _, cookie := range cookies {
        log.Printf("  - %s: %s", cookie.Name, cookie.Value)
    }

    // Step 2: Make authenticated request
    // Cookies are automatically included
    dashboardReq, err := http.NewRequest(
        http.MethodGet,
        "https://api.example.com/dashboard",
        nil,
    )
    if err != nil {
        log.Fatal(err)
    }
    
    dashboardResp, err := client.Do(dashboardReq)
    if err != nil {
        log.Fatal(err)
    }
    defer dashboardResp.Body.Close()
    
    body, _ := io.ReadAll(dashboardResp.Body)
    fmt.Printf("Dashboard response: %s\n", string(body))

    // Step 3: Logout (clear session)
    logoutReq, err := http.NewRequest(
        http.MethodPost,
        "https://api.example.com/auth/logout",
        nil,
    )
    if err != nil {
        log.Fatal(err)
    }
    
    logoutResp, err := client.Do(logoutReq)
    if err != nil {
        log.Fatal(err)
    }
    defer logoutResp.Body.Close()
    
    log.Println("Logged out")
}

Sharing cookies between clients

You can share the same cookie jar across multiple clients:
// Create shared jar
sharedJar := tls_client.NewCookieJar()

// Client 1
client1, err := tls_client.NewHttpClient(
    tls_client.NewNoopLogger(),
    tls_client.WithClientProfile(profiles.Chrome_133),
    tls_client.WithCookieJar(sharedJar),
)

// Client 2 - shares the same cookies
client2, err := tls_client.NewHttpClient(
    tls_client.NewNoopLogger(),
    tls_client.WithClientProfile(profiles.Firefox_133),
    tls_client.WithCookieJar(sharedJar),
)

// Cookies set by client1 are available to client2
client1.Get("https://example.com/set-cookie")
cookies := client2.GetCookies(mustParseURL("https://example.com"))
Sharing a cookie jar between clients is not thread-safe. If you need concurrent access, implement your own locking mechanism.
The built-in cookie jar is in-memory only. To persist cookies across restarts:
import (
    "encoding/json"
    "os"
    "net/url"
)

// Save cookies to file
func saveCookies(client tls_client.HttpClient, filename string) error {
    u, _ := url.Parse("https://example.com")
    cookies := client.GetCookies(u)
    
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    return json.NewEncoder(file).Encode(cookies)
}

// Load cookies from file
func loadCookies(client tls_client.HttpClient, filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    var cookies []*http.Cookie
    if err := json.NewDecoder(file).Decode(&cookies); err != nil {
        return err
    }
    
    u, _ := url.Parse("https://example.com")
    client.SetCookies(u, cookies)
    
    return nil
}

Debugging cookies

View cookies in requests and responses with debug mode:
options := []tls_client.HttpClientOption{
    tls_client.WithTimeoutSeconds(30),
    tls_client.WithClientProfile(profiles.Chrome_133),
    tls_client.WithCookieJar(jar),
    tls_client.WithDebug(),  // Enable debug logging
}

client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)

// Debug output will show:
// - Cookies sent in request
// - Cookies received in response

Best practices

2
Create a cookie jar for any client that needs to maintain session state.
3
Use skip existing for manual control
4
Enable WithSkipExisting() when you need to set cookies manually and prevent the server from overwriting them.
5
Clear cookies between sessions
6
Replace the cookie jar or create a new client when starting a new session:
7
client.SetCookieJar(tls_client.NewCookieJar())
9
When setting cookies manually, use proper domain, path, and security flags.
The cookie jar respects standard cookie rules including domain matching, path matching, expiration, and the Secure flag.

Next steps

Build docs developers (and LLMs) love