Skip to content

Domain Client ​

The domain client (DomainClient) provides request management for a specific domain, automatically maintaining cookies and headers. It solves the pain point of manually passing auth headers and tracking cookies across multiple requests with a plain Client — session state is injected into every request automatically, and response cookies are captured automatically.

text
DomainClient architecture
├── client         underlying Client (reuses connection pool, middleware chain)
├── baseURL        domain scope (e.g. https://api.example.com/v1)
├── parsedURL      cached parse result (avoids re-running url.Parse per request)
├── domain         hostname (without port)
└── SessionManager session state
      ├── headers  map[string]string  session-level request headers
      └── cookies  map[string]*Cookie session-level cookies

DomainClienter Interface ​

go
type DomainClienter interface {
    Client

    URL() string
    Domain() string

    SetHeader(key, value string) error
    SetHeaders(headers map[string]string) error
    DeleteHeader(key string)
    ClearHeaders()
    GetHeaders() map[string]string

    SetCookie(cookie *http.Cookie) error
    SetCookies(cookies []*http.Cookie) error
    DeleteCookie(name string)
    ClearCookies()
    GetCookies() []*http.Cookie
    GetCookie(name string) *http.Cookie

    Session() *SessionManager
}

DomainClienter implements both the Client interface (with Get/Post/Put/Patch/Delete/Head/Options/Request/Download/Close) and the session-management methods. Using the interface type rather than the concrete type is recommended for easier testing and implementation swapping.

Complete Method Table ​

HTTP Request Methods (inherited from Client) ​

MethodSignatureDescription
Get(path string, opts ...RequestOption) (*Result, error)GET request
Post(path string, opts ...RequestOption) (*Result, error)POST request
Put(path string, opts ...RequestOption) (*Result, error)PUT request
Patch(path string, opts ...RequestOption) (*Result, error)PATCH request
Delete(path string, opts ...RequestOption) (*Result, error)DELETE request
Head(path string, opts ...RequestOption) (*Result, error)HEAD request
Options(path string, opts ...RequestOption) (*Result, error)OPTIONS request
Request(ctx, method, path string, opts ...RequestOption) (*Result, error)Generic request with context
Download(ctx, path string, cfg *DownloadConfig, opts ...RequestOption) (*DownloadResult, error)File download
Close() errorClose the client and release resources

The concrete type *DomainClient provides implementations for all of the above: DomainClient.Get, DomainClient.Post, DomainClient.Put, DomainClient.Patch, DomainClient.Delete, DomainClient.Head, DomainClient.Options, DomainClient.Request, DomainClient.Download, and DomainClient.Close (the last two are detailed in their own sections below).

URL Access Methods ​

MethodSignatureDescription
URLURL() stringBase URL (the baseURL passed at construction)
DomainDomain() stringDomain (hostname, without port)
SessionSession() *SessionManagerUnderlying session manager

On the interface these are defined as DomainClienter.URL, DomainClienter.Domain, and DomainClienter.Session, implemented by DomainClient.URL, DomainClient.Domain, and DomainClient.Session.

URL() / Domain() / Session() / Close() are all nil-receiver-safe: with a nil receiver they return "", "", nil, and nil respectively.

Session Header Management ​

MethodSignatureDescription
SetHeader(key, value string) errorSet a single session header (validates CRLF safety)
SetHeaders(headers map[string]string) errorSet session headers in bulk
DeleteHeader(key string)Delete a single session header
ClearHeaders()Clear all session headers
GetHeaders() map[string]stringGet a copy of the session headers

On the interface these are defined as DomainClienter.SetHeader, DomainClienter.SetHeaders, DomainClienter.DeleteHeader, DomainClienter.ClearHeaders, and DomainClienter.GetHeaders; the implementations delegate to the embedded SessionManager (see Session Management).

MethodSignatureDescription
SetCookie(cookie *http.Cookie) errorSet a single session cookie
SetCookies(cookies []*http.Cookie) errorSet session cookies in bulk
DeleteCookie(name string)Delete a cookie by name
ClearCookies()Clear all cookies
GetCookies() []*http.CookieGet a copy of all cookies
GetCookie(name string) *http.CookieGet a copy of a cookie by name

The corresponding interface definitions are DomainClienter.SetCookie, DomainClienter.SetCookies, DomainClienter.DeleteCookie, DomainClienter.ClearCookies, DomainClienter.GetCookies, and DomainClienter.GetCookie, likewise delegating to the SessionManager.

NewDomain ​

go
func NewDomain(baseURL string, cfg Config) (DomainClienter, error)

Creates a domain-scoped client. Cookies are automatically enabled. Pass a Config value, or use NewDomainDefault(baseURL) as a zero-argument shortcut.

go
// Using default config (or call NewDomainDefault for the same effect)
dc, err := httpc.NewDomain("https://api.example.com", httpc.DefaultConfig())
if err != nil {
    log.Fatal(err)
}
defer dc.Close()

// Using custom configuration
cfg := httpc.DefaultConfig()
cfg.Timeouts.Request = 60 * time.Second
dc, err := httpc.NewDomain("https://api.example.com", cfg)
if err != nil {
    log.Fatal(err)
}
defer dc.Close()

Parameters:

ParameterTypeDescription
baseURLstringBase URL (must include scheme and host)
cfgConfigConfiguration value (recommended: obtain via DefaultConfig() or a preset function)

Returns: DomainClienter interface (not the concrete *DomainClient type).

Error conditions:

ConditionError message
baseURL missing scheme or hostbase URL must include scheme and host
Configuration validation failureinvalid configuration: ...

Cookies are auto-enabled

NewDomain internally forces cfg.Connection.EnableCookies = true, regardless of whether your passed-in cfg enables cookies. This is because the core value of the domain client is maintaining a cookie session across requests.

NewDomainDefault ​

go
func NewDomainDefault(baseURL string) (DomainClienter, error)

Convenience constructor, equivalent to NewDomain(baseURL, DefaultConfig()).

go
dc, err := httpc.NewDomainDefault("https://api.example.com")
if err != nil {
    log.Fatal(err)
}
defer dc.Close()

HTTP Methods ​

All methods accept relative paths or absolute URLs:

go
// Relative path: auto-concatenated with baseURL
result, err := dc.Get("/users")
result, err := dc.Post("/users", httpc.WithJSON(data))
result, err := dc.Put("/users/1", httpc.WithJSON(data))
result, err := dc.Patch("/users/1", httpc.WithJSON(data))
result, err := dc.Delete("/users/1")
result, err := dc.Head("/users/1")
result, err := dc.Options("/users")

// Absolute URL: used directly
result, err := dc.Get("https://other-api.com/data")

Request ​

go
result, err := dc.Request(ctx, "GET", "/users", options...)

Generic request method with context support for timeout and cancellation control. DomainClient satisfies the Client interface by implementing this method.

URL Concatenation Rules In Detail ​

The buildURL method is responsible for joining the request path with baseURL. The rules:

text
buildURL(pathStr):

  ① pathStr is empty -> return baseURL
  ② pathStr starts with http:// or https:// -> absolute URL, returned directly
  ③ Otherwise -> relative-path join:
       a. Clone the cached parsedURL (to avoid mutating the original)
       b. Parse pathStr to split path / query / fragment
       c. result.Path = path.Join(basePath, parsedPath)
       d. Trailing-slash preservation: if the original path ends with /, so does the result
       e. Path-traversal defense: the result must stay within the base-path scope
       f. Query-parameter merge: base query params + path query params
       g. Fragment passthrough

Concatenation Examples ​

Input pathResult (baseURL = https://api.example.com/v1)
""https://api.example.com/v1
/usershttps://api.example.com/v1/users
usershttps://api.example.com/v1/users
/users/https://api.example.com/v1/users/ (trailing slash preserved)
/users?page=1https://api.example.com/v1/users?page=1
search?q=gohttps://api.example.com/v1/search?q=go
https://other.com/apihttps://other.com/api (absolute URL used directly)

Query-Parameter Merge ​

When baseURL itself carries query parameters, the request path's query parameters are appended to them:

text
baseURL  = https://api.example.com/v1?lang=zh
path     = /users?page=1
result   = https://api.example.com/v1/users?lang=zh&page=1

Path-Traversal Defense ​

buildURL checks that the joined path stays within the base-path scope, preventing path-traversal attacks:

text
baseURL = https://api.example.com/v1
path    = ../admin/delete       <- after path.Clean this becomes /admin/delete

Check: is /admin/delete within the /v1 scope?
Result: no -> returns error "path escapes base URL scope"

Absolute-URL recognition

Only paths prefixed with http:// or https:// are recognized as absolute URLs; other protocols (e.g. ftp://) are not recognized as absolute, are joined as relative paths, and usually cause the request to fail. No scope check is performed when the base path is empty or /.

Automatic Session Management ​

The domain client's session management has three phases:

text
Session management across the request lifecycle:

  ① prepareOptions() (before send)
     Read session headers and cookies from the SessionManager
     -> convert to RequestOption and inject into the request

  ② captureFromOptions() (before send)
     Extract cookies and headers from the user-supplied RequestOptions
     -> store into the SessionManager (update if present, skip otherwise)

  ③ UpdateFromResult() (after send)
     Extract Set-Cookie from the response
     -> store into the SessionManager
go
dc, _ := httpc.NewDomainDefault("https://api.example.com")

// Session headers: injected into every subsequent request
dc.SetHeader("Authorization", "Bearer token-abc")
dc.SetHeader("Accept-Language", "zh-CN")

// After login, the response's Set-Cookie is auto-captured
dc.Post("/login", httpc.WithJSON(loginData))
// Subsequent requests automatically carry the login cookies

// You can also set cookies manually
dc.SetCookie(&http.Cookie{Name: "session", Value: "xyz"})

// Inspect session state
dc.GetCookie("session")  // -> *http.Cookie copy
dc.GetHeaders()          // -> map[string]string copy

Thread safety

SessionManager is protected internally by a sync.RWMutex; SetHeader/SetCookie/GetCookie and similar methods can be called concurrently safely. prepareOptions uses a read-then-write non-atomic sequence — the session state is designed for eventual consistency. Concurrent requests may interleave during prepareOptions, but each request captures a consistent snapshot at its prepareOptions moment.

Double-Option-Execution Caveat ​

prepareSessionOptions applies the user-supplied RequestOption twice before the request is sent: once in captureFromOptions to capture session state, and once in client.Request for the actual request.

text
prepareSessionOptions(options):
  ① managedOptions = prepareOptions()        <- read session state
  ② allOptions = managedOptions + options    <- merge
  ③ captureFromOptions(options)              <- applied once to a temp request (capture session)
  ④ return allOptions -> client.Request()    <- applied a second time to the actual request

Avoid side-effect options

The following options execute twice in DomainClient, leading to unexpected behavior:

Problematic optionReason
Counter incrementsIncremented twice per request
Nonce random generationDifferent values generated in the capture phase and the request phase
WithOnRequest / WithOnResponseCallbacks are explicitly cleared and do not fire twice (safe)

If you need side-effect options, use the underlying Client to send the request directly, or manage state outside the option.

Download Method ​

go
func (dc *DomainClient) Download(ctx context.Context, path string, cfg *DownloadConfig, options ...RequestOption) (*DownloadResult, error)

Downloads a file to cfg.FilePath; path is resolved relative to baseURL. The signature matches the package-level Download and Client.Download — Download is the single canonical download entry point across all three. cfg must not be nil, and cfg.FilePath must be set (otherwise ErrEmptyFilePath is returned).

go
cfg := httpc.DefaultDownloadConfig()
cfg.FilePath = "/tmp/report.pdf"
cfg.Overwrite = true

result, err := dc.Download(ctx, "/files/report.pdf", cfg)

Download response cookies are automatically captured into the session (via UpdateFromCookies). As with Request, request options are applied twice.

Relationship to the Client Interface ​

DomainClient satisfies both interfaces via compile-time assertions:

go
var _ Client = (*DomainClient)(nil)           // implements Client
var _ DomainClienter = (*DomainClient)(nil)   // implements DomainClienter
text
Interface hierarchy:
  Doer                                    <- minimal interface (only Request)
    └── Client                             <- + HTTP methods + Download + Close
          └── DomainClienter               <- + URL/Domain/Session + session header/cookie management
                └── *DomainClient          <- concrete implementation

DomainClienter embeds Client, so any function that accepts a Client parameter also accepts a DomainClienter. This lets DomainClient be used seamlessly wherever a Client is expected, while providing additional session-management capabilities.

Complete REST API Client Wrapper Example ​

The example below shows how to use DomainClient to wrap a GitHub API client that automatically manages auth headers and pagination query parameters.

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/httpc"
)

// GitHubClient wraps the GitHub REST API
type GitHubClient struct {
	dc httpc.DomainClienter
}

// NewGitHubClient creates a GitHub API client
func NewGitHubClient(token string) (*GitHubClient, error) {
	cfg := httpc.DefaultConfig()
	cfg.Timeouts.Request = 30 * time.Second

	dc, err := httpc.NewDomain("https://api.github.com", cfg)
	if err != nil {
		return nil, err
	}

	// Set session-level request headers
	if err := dc.SetHeader("Authorization", "Bearer "+token); err != nil {
		return nil, fmt.Errorf("set auth header: %w", err)
	}
	if err := dc.SetHeader("Accept", "application/vnd.github+json"); err != nil {
		return nil, fmt.Errorf("set accept header: %w", err)
	}
	if err := dc.SetHeader("X-GitHub-Api-Version", "2022-11-28"); err != nil {
		return nil, fmt.Errorf("set api version: %w", err)
	}

	return &GitHubClient{dc: dc}, nil
}

// Close releases resources
func (g *GitHubClient) Close() error { return g.dc.Close() }

// GetUser fetches user info (relative path auto-concatenated with baseURL)
func (g *GitHubClient) GetUser(username string) (*httpc.Result, error) {
	return g.dc.Get(fmt.Sprintf("/users/%s", username))
}

// ListUserRepos lists a user's repositories (with pagination parameters)
func (g *GitHubClient) ListUserRepos(username string, page, perPage int) (*httpc.Result, error) {
	return g.dc.Get(fmt.Sprintf("/users/%s/repos?page=%d&per_page=%d", username, page, perPage))
}

func main() {
	client, err := NewGitHubClient("ghp_your_token_here")
	if err != nil {
		panic(err)
	}
	defer client.Close()

	// Every request automatically carries Authorization, Accept, X-GitHub-Api-Version headers
	result, err := client.GetUser("torvalds")
	if err != nil {
		panic(err)
	}
	fmt.Printf("status code: %d\n", result.StatusCode())

	// Parse the JSON response via Unmarshal
	var user struct {
		Login string `json:"login"`
	}
	if err := result.Unmarshal(&user); err != nil {
		panic(err)
	}
	fmt.Printf("username: %s\n", user.Login)

	repos, err := client.ListUserRepos("torvalds", 1, 5)
	if err != nil {
		panic(err)
	}
	fmt.Printf("repo list status code: %d\n", repos.StatusCode())
	// Sample output:
	// status code: 200
	// username: torvalds
	// repo list status code: 200
}

Interface return type

NewDomain and NewDomainDefault return the DomainClienter interface rather than the concrete *DomainClient type, making it easy to substitute a mock in tests. Type-assert when you need to access the concrete type.

See Also ​