From 951493debe401ffe7c0ca2b7e6f2c6e51beb14f0 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 28 Dec 2025 20:35:45 -0600 Subject: [PATCH 1/5] feat: add proxy/cookie/redirect + trace helpers and HEAD/OPTIONS requests --- README.md | 120 ++++++++++++++++++++++++++++++++++-- client.go | 66 ++++++++++++++++++-- client_test.go | 32 ++++++++++ docs/examplegen/main.go | 1 + examples/cookiejar/main.go | 18 ++++++ examples/head/main.go | 15 +++++ examples/headctx/main.go | 19 ++++++ examples/options/main.go | 15 +++++ examples/optionsctx/main.go | 19 ++++++ examples/proxy/main.go | 14 +++++ examples/proxyfunc/main.go | 17 +++++ examples/redirect/main.go | 17 +++++ examples/trace/main.go | 14 +++++ examples/traceall/main.go | 14 +++++ options_client.go | 83 +++++++++++++++++++++++++ options_client_test.go | 50 +++++++++++++++ options_debug.go | 36 +++++++++++ options_debug_test.go | 17 +++++ 18 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 examples/cookiejar/main.go create mode 100644 examples/head/main.go create mode 100644 examples/headctx/main.go create mode 100644 examples/options/main.go create mode 100644 examples/optionsctx/main.go create mode 100644 examples/proxy/main.go create mode 100644 examples/proxyfunc/main.go create mode 100644 examples/redirect/main.go create mode 100644 examples/trace/main.go create mode 100644 examples/traceall/main.go diff --git a/README.md b/README.md index d041e21..3d24020 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It keeps req's power and escape hatches, while making the 90% use case feel effo Go Report Card - Tests + Tests

@@ -69,6 +69,16 @@ func main() { } ``` +## Browser Profiles + +Browser profiles provide a simple way to match common client behavior without exposing low-level details. +Internally, profiles may apply transport and protocol settings as needed, but those details are intentionally abstracted. + +```go +c := httpx.New(httpx.AsChrome()) +_ = c +``` + ## Use Any req Feature **httpx** is built on top of the incredible [req](https://github.com/imroc/req) library, and you can always drop down to it when you need something beyond httpx’s helpers. That means every example in req’s docs is available to you with `c.Req()` or `c.Raw()`. @@ -88,6 +98,9 @@ rc.EnableTraceAll() See the full req documentation here: https://req.cool/docs/prologue/quickstart/ +Most users will only need the high-level APIs (browser profiles, request composition, retries, uploads). +When you need deep control over headers, transports, or protocol behavior, `req` is always there. + ## Options in Practice ```go @@ -129,14 +142,14 @@ They are compiled by `example_compile_test.go` to keep docs and code in sync. | **Auth** | [Auth](#auth) [Basic](#basic) [Bearer](#bearer) | | **Browser Profiles** | [AsChrome](#aschrome) [AsFirefox](#asfirefox) [AsMobile](#asmobile) [AsSafari](#assafari) | | **Client** | [Default](#default) [New](#new) [Raw](#raw) [Req](#req) | -| **Client Options** | [BaseURL](#baseurl) [ErrorMapper](#errormapper) [Middleware](#middleware) [Transport](#transport) | -| **Debugging** | [Dump](#dump) [DumpAll](#dumpall) [DumpEachRequest](#dumpeachrequest) [DumpEachRequestTo](#dumpeachrequestto) [DumpTo](#dumpto) [DumpToFile](#dumptofile) | +| **Client Options** | [BaseURL](#baseurl) [CookieJar](#cookiejar) [ErrorMapper](#errormapper) [Middleware](#middleware) [Proxy](#proxy) [ProxyFunc](#proxyfunc) [Redirect](#redirect) [Transport](#transport) | +| **Debugging** | [Dump](#dump) [DumpAll](#dumpall) [DumpEachRequest](#dumpeachrequest) [DumpEachRequestTo](#dumpeachrequestto) [DumpTo](#dumpto) [DumpToFile](#dumptofile) [Trace](#trace) [TraceAll](#traceall) | | **Download Options** | [OutputFile](#outputfile) | | **Errors** | [Error](#error) | | **Request Composition** | [Body](#body) [Form](#form) [Header](#header) [Headers](#headers) [JSON](#json) [Path](#path) [Paths](#paths) [Queries](#queries) [Query](#query) [UserAgent](#useragent) | | **Request Control** | [Before](#before) [Timeout](#timeout) | -| **Requests** | [Delete](#delete) [Get](#get) [Patch](#patch) [Post](#post) [Put](#put) | -| **Requests (Context)** | [DeleteCtx](#deletectx) [GetCtx](#getctx) [PatchCtx](#patchctx) [PostCtx](#postctx) [PutCtx](#putctx) | +| **Requests** | [Delete](#delete) [Get](#get) [Head](#head) [Options](#options) [Patch](#patch) [Post](#post) [Put](#put) | +| **Requests (Context)** | [DeleteCtx](#deletectx) [GetCtx](#getctx) [HeadCtx](#headctx) [OptionsCtx](#optionsctx) [PatchCtx](#patchctx) [PostCtx](#postctx) [PutCtx](#putctx) | | **Retry** | [RetryBackoff](#retrybackoff) [RetryCondition](#retrycondition) [RetryCount](#retrycount) [RetryFixedInterval](#retryfixedinterval) [RetryHook](#retryhook) [RetryInterval](#retryinterval) | | **Retry (Client)** | [Retry](#retry) | | **Upload Options** | [File](#file) [FileBytes](#filebytes) [FileReader](#filereader) [Files](#files) [UploadCallback](#uploadcallback) [UploadCallbackWithInterval](#uploadcallbackwithinterval) [UploadProgress](#uploadprogress) | @@ -292,6 +305,16 @@ c := httpx.New(httpx.BaseURL("https://api.example.com")) _ = c ``` +### CookieJar + +CookieJar sets the cookie jar for the client. + +```go +jar, _ := cookiejar.New(nil) +c := httpx.New(httpx.CookieJar(jar)) +_ = c +``` + ### ErrorMapper ErrorMapper sets a custom error mapper for non-2xx responses. @@ -315,6 +338,33 @@ c := httpx.New(httpx.Middleware(func(_ *req.Client, r *req.Request) error { _ = c ``` +### Proxy + +Proxy sets a proxy URL for the client. + +```go +c := httpx.New(httpx.Proxy("http://localhost:8080")) +_ = c +``` + +### ProxyFunc + +ProxyFunc sets a proxy function for the client. + +```go +c := httpx.New(httpx.ProxyFunc(http.ProxyFromEnvironment)) +_ = c +``` + +### Redirect + +Redirect sets the redirect policy for the client. + +```go +c := httpx.New(httpx.Redirect(req.NoRedirectPolicy())) +_ = c +``` + ### Transport Transport wraps the underlying transport with a custom RoundTripper. @@ -383,6 +433,24 @@ c := httpx.New() _ = httpx.Get[string](c, "https://example.com", httpx.DumpToFile("httpx.dump")) ``` +### Trace + +Trace enables req's request-level trace output. + +```go +c := httpx.New() +_ = httpx.Get[string](c, "https://example.com", httpx.Trace()) +``` + +### TraceAll + +TraceAll enables req's client-level trace output for all requests. + +```go +c := httpx.New(httpx.TraceAll()) +_ = c +``` + ## Download Options ### OutputFile @@ -588,6 +656,26 @@ if res.Err != nil { godump.Dump(res.Body) ``` +### Head + +Head issues a HEAD request using the provided client. + +```go +c := httpx.New() +res := httpx.Head[string](c, "https://example.com") +_ = res +``` + +### Options + +Options issues an OPTIONS request using the provided client. + +```go +c := httpx.New() +res := httpx.Options[string](c, "https://example.com") +_ = res +``` + ### Patch Patch issues a PATCH request using the provided client. @@ -671,6 +759,28 @@ res := httpx.GetCtx[User](c, ctx, "https://api.example.com/users/1") _, _ = res.Body, res.Err ``` +### HeadCtx + +HeadCtx issues a HEAD request using the provided client and context. + +```go +c := httpx.New() +ctx := context.Background() +res := httpx.HeadCtx[string](c, ctx, "https://example.com") +_ = res +``` + +### OptionsCtx + +OptionsCtx issues an OPTIONS request using the provided client and context. + +```go +c := httpx.New() +ctx := context.Background() +res := httpx.OptionsCtx[string](c, ctx, "https://example.com") +_ = res +``` + ### PatchCtx PatchCtx issues a PATCH request using the provided client and context. diff --git a/client.go b/client.go index 2988144..278a657 100644 --- a/client.go +++ b/client.go @@ -216,6 +216,30 @@ func Delete[T any](client *Client, url string, opts ...Option) Result[T] { return do[T](client, nil, methodDelete, url, nil, opts) } +// Head issues a HEAD request using the provided client. +// @group Requests +// +// Example: HEAD request +// +// c := httpx.New() +// res := httpx.Head[string](c, "https://example.com") +// _ = res +func Head[T any](client *Client, url string, opts ...Option) Result[T] { + return do[T](client, nil, methodHead, url, nil, opts) +} + +// Options issues an OPTIONS request using the provided client. +// @group Requests +// +// Example: OPTIONS request +// +// c := httpx.New() +// res := httpx.Options[string](c, "https://example.com") +// _ = res +func Options[T any](client *Client, url string, opts ...Option) Result[T] { + return do[T](client, nil, methodOptions, url, nil, opts) +} + // GetCtx issues a GET request using the provided client and context. // @group Requests (Context) // @@ -310,6 +334,32 @@ func DeleteCtx[T any](client *Client, ctx context.Context, url string, opts ...O return do[T](client, ctx, methodDelete, url, nil, opts) } +// HeadCtx issues a HEAD request using the provided client and context. +// @group Requests (Context) +// +// Example: context-aware HEAD +// +// c := httpx.New() +// ctx := context.Background() +// res := httpx.HeadCtx[string](c, ctx, "https://example.com") +// _ = res +func HeadCtx[T any](client *Client, ctx context.Context, url string, opts ...Option) Result[T] { + return do[T](client, ctx, methodHead, url, nil, opts) +} + +// OptionsCtx issues an OPTIONS request using the provided client and context. +// @group Requests (Context) +// +// Example: context-aware OPTIONS +// +// c := httpx.New() +// ctx := context.Background() +// res := httpx.OptionsCtx[string](c, ctx, "https://example.com") +// _ = res +func OptionsCtx[T any](client *Client, ctx context.Context, url string, opts ...Option) Result[T] { + return do[T](client, ctx, methodOptions, url, nil, opts) +} + func do[T any](client *Client, ctx context.Context, method, url string, body any, opts []Option) Result[T] { var res Result[T] @@ -370,11 +420,13 @@ func (c *Client) mapError(resp *req.Response) error { } const ( - methodGet = "GET" - methodPost = "POST" - methodPut = "PUT" - methodPatch = "PATCH" - methodDelete = "DELETE" + methodGet = "GET" + methodPost = "POST" + methodPut = "PUT" + methodPatch = "PATCH" + methodDelete = "DELETE" + methodHead = "HEAD" + methodOptions = "OPTIONS" ) func send(r *req.Request, method, url string) (*req.Response, error) { @@ -389,6 +441,10 @@ func send(r *req.Request, method, url string) (*req.Response, error) { return r.Patch(url) case methodDelete: return r.Delete(url) + case methodHead: + return r.Head(url) + case methodOptions: + return r.Options(url) default: return nil, fmt.Errorf("httpx: unsupported method %s", method) } diff --git a/client_test.go b/client_test.go index cd21fca..02c8ddf 100644 --- a/client_test.go +++ b/client_test.go @@ -119,6 +119,38 @@ func TestErrorMapper(t *testing.T) { } } +func TestHeadRequest(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Fatalf("method = %s", r.Method) + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + client := New() + res := Head[string](client, server.URL) + if res.Err != nil { + t.Fatalf("unexpected error: %v", res.Err) + } +} + +func TestOptionsRequest(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodOptions { + t.Fatalf("method = %s", r.Method) + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + client := New() + res := Options[string](client, server.URL) + if res.Err != nil { + t.Fatalf("unexpected error: %v", res.Err) + } +} + func TestOptionsApplied(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/users/123" { diff --git a/docs/examplegen/main.go b/docs/examplegen/main.go index e0a6af8..28941cf 100644 --- a/docs/examplegen/main.go +++ b/docs/examplegen/main.go @@ -390,6 +390,7 @@ func writeMain(base string, fd *FuncDoc, importPath string) error { "httptest.": "net/http/httptest", "context.": "context", "regexp.": "regexp", + "cookiejar.": "net/http/cookiejar", "redis.": "github.com/redis/go-redis/v9", "time.": "time", "gocron": "github.com/go-co-op/gocron/v2", diff --git a/examples/cookiejar/main.go b/examples/cookiejar/main.go new file mode 100644 index 0000000..b2d8535 --- /dev/null +++ b/examples/cookiejar/main.go @@ -0,0 +1,18 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "github.com/goforj/httpx" + "net/http/cookiejar" +) + +func main() { + // CookieJar sets the cookie jar for the client. + + // Example: set cookie jar + jar, _ := cookiejar.New(nil) + c := httpx.New(httpx.CookieJar(jar)) + _ = c +} diff --git a/examples/head/main.go b/examples/head/main.go new file mode 100644 index 0000000..edbe107 --- /dev/null +++ b/examples/head/main.go @@ -0,0 +1,15 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/httpx" + +func main() { + // Head issues a HEAD request using the provided client. + + // Example: HEAD request + c := httpx.New() + res := httpx.Head[string](c, "https://example.com") + _ = res +} diff --git a/examples/headctx/main.go b/examples/headctx/main.go new file mode 100644 index 0000000..bdb38b9 --- /dev/null +++ b/examples/headctx/main.go @@ -0,0 +1,19 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "context" + "github.com/goforj/httpx" +) + +func main() { + // HeadCtx issues a HEAD request using the provided client and context. + + // Example: context-aware HEAD + c := httpx.New() + ctx := context.Background() + res := httpx.HeadCtx[string](c, ctx, "https://example.com") + _ = res +} diff --git a/examples/options/main.go b/examples/options/main.go new file mode 100644 index 0000000..c49d913 --- /dev/null +++ b/examples/options/main.go @@ -0,0 +1,15 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/httpx" + +func main() { + // Options issues an OPTIONS request using the provided client. + + // Example: OPTIONS request + c := httpx.New() + res := httpx.Options[string](c, "https://example.com") + _ = res +} diff --git a/examples/optionsctx/main.go b/examples/optionsctx/main.go new file mode 100644 index 0000000..b5656d6 --- /dev/null +++ b/examples/optionsctx/main.go @@ -0,0 +1,19 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "context" + "github.com/goforj/httpx" +) + +func main() { + // OptionsCtx issues an OPTIONS request using the provided client and context. + + // Example: context-aware OPTIONS + c := httpx.New() + ctx := context.Background() + res := httpx.OptionsCtx[string](c, ctx, "https://example.com") + _ = res +} diff --git a/examples/proxy/main.go b/examples/proxy/main.go new file mode 100644 index 0000000..9b4549a --- /dev/null +++ b/examples/proxy/main.go @@ -0,0 +1,14 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/httpx" + +func main() { + // Proxy sets a proxy URL for the client. + + // Example: set proxy URL + c := httpx.New(httpx.Proxy("http://localhost:8080")) + _ = c +} diff --git a/examples/proxyfunc/main.go b/examples/proxyfunc/main.go new file mode 100644 index 0000000..3101bc0 --- /dev/null +++ b/examples/proxyfunc/main.go @@ -0,0 +1,17 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "github.com/goforj/httpx" + "net/http" +) + +func main() { + // ProxyFunc sets a proxy function for the client. + + // Example: set proxy function + c := httpx.New(httpx.ProxyFunc(http.ProxyFromEnvironment)) + _ = c +} diff --git a/examples/redirect/main.go b/examples/redirect/main.go new file mode 100644 index 0000000..a10867f --- /dev/null +++ b/examples/redirect/main.go @@ -0,0 +1,17 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "github.com/goforj/httpx" + "github.com/imroc/req/v3" +) + +func main() { + // Redirect sets the redirect policy for the client. + + // Example: disable redirects + c := httpx.New(httpx.Redirect(req.NoRedirectPolicy())) + _ = c +} diff --git a/examples/trace/main.go b/examples/trace/main.go new file mode 100644 index 0000000..533e2a4 --- /dev/null +++ b/examples/trace/main.go @@ -0,0 +1,14 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/httpx" + +func main() { + // Trace enables req's request-level trace output. + + // Example: trace a single request + c := httpx.New() + _ = httpx.Get[string](c, "https://example.com", httpx.Trace()) +} diff --git a/examples/traceall/main.go b/examples/traceall/main.go new file mode 100644 index 0000000..5d3f4ca --- /dev/null +++ b/examples/traceall/main.go @@ -0,0 +1,14 @@ +//go:build ignore +// +build ignore + +package main + +import "github.com/goforj/httpx" + +func main() { + // TraceAll enables req's client-level trace output for all requests. + + // Example: trace all requests + c := httpx.New(httpx.TraceAll()) + _ = c +} diff --git a/options_client.go b/options_client.go index f43fe54..09e4991 100644 --- a/options_client.go +++ b/options_client.go @@ -2,6 +2,7 @@ package httpx import ( "net/http" + "net/url" "github.com/imroc/req/v3" ) @@ -89,3 +90,85 @@ func (b OptionBuilder) ErrorMapper(fn ErrorMapperFunc) OptionBuilder { c.errorMapper = fn })) } + +// Proxy sets a proxy URL for the client. +// @group Client Options +// +// Applies to client configuration only. +// Example: set proxy URL +// +// c := httpx.New(httpx.Proxy("http://localhost:8080")) +// _ = c +func Proxy(proxyURL string) OptionBuilder { + return OptionBuilder{}.Proxy(proxyURL) +} + +func (b OptionBuilder) Proxy(proxyURL string) OptionBuilder { + return b.add(clientOnly(func(c *Client) { + if proxyURL == "" { + return + } + c.req.SetProxyURL(proxyURL) + })) +} + +// ProxyFunc sets a proxy function for the client. +// @group Client Options +// +// Applies to client configuration only. +// Example: set proxy function +// +// c := httpx.New(httpx.ProxyFunc(http.ProxyFromEnvironment)) +// _ = c +func ProxyFunc(fn func(*http.Request) (*url.URL, error)) OptionBuilder { + return OptionBuilder{}.ProxyFunc(fn) +} + +func (b OptionBuilder) ProxyFunc(fn func(*http.Request) (*url.URL, error)) OptionBuilder { + if fn == nil { + return b + } + return b.add(clientOnly(func(c *Client) { + c.req.SetProxy(fn) + })) +} + +// CookieJar sets the cookie jar for the client. +// @group Client Options +// +// Applies to client configuration only. +// Example: set cookie jar +// +// jar, _ := cookiejar.New(nil) +// c := httpx.New(httpx.CookieJar(jar)) +// _ = c +func CookieJar(jar http.CookieJar) OptionBuilder { + return OptionBuilder{}.CookieJar(jar) +} + +func (b OptionBuilder) CookieJar(jar http.CookieJar) OptionBuilder { + return b.add(clientOnly(func(c *Client) { + c.req.SetCookieJar(jar) + })) +} + +// Redirect sets the redirect policy for the client. +// @group Client Options +// +// Applies to client configuration only. +// Example: disable redirects +// +// c := httpx.New(httpx.Redirect(req.NoRedirectPolicy())) +// _ = c +func Redirect(policies ...req.RedirectPolicy) OptionBuilder { + return OptionBuilder{}.Redirect(policies...) +} + +func (b OptionBuilder) Redirect(policies ...req.RedirectPolicy) OptionBuilder { + return b.add(clientOnly(func(c *Client) { + if len(policies) == 0 { + return + } + c.req.SetRedirectPolicy(policies...) + })) +} diff --git a/options_client_test.go b/options_client_test.go index 478be19..a745875 100644 --- a/options_client_test.go +++ b/options_client_test.go @@ -5,7 +5,10 @@ import ( "errors" "io" "net/http" + "net/http/cookiejar" "net/http/httptest" + "net/url" + "reflect" "testing" "time" @@ -129,6 +132,53 @@ func TestWithErrorMapper(t *testing.T) { } } +func TestWithProxy(t *testing.T) { + c := New(Proxy("http://localhost:8080")) + if c.req.Transport.Options.Proxy == nil { + t.Fatalf("expected proxy to be set") + } +} + +func TestWithProxyFunc(t *testing.T) { + c := New(ProxyFunc(nil)) + if c == nil { + t.Fatalf("expected client") + } + + fn := func(req *http.Request) (*url.URL, error) { + return http.ProxyFromEnvironment(req) + } + c = New(ProxyFunc(fn)) + if c.req.Transport.Options.Proxy == nil { + t.Fatalf("expected proxy func to be set") + } +} + +func TestWithCookieJar(t *testing.T) { + jar, err := cookiejar.New(nil) + if err != nil { + t.Fatalf("cookie jar: %v", err) + } + c := New(CookieJar(jar)) + got := reflect.ValueOf(c.req).Elem().FieldByName("httpClient").Elem().FieldByName("Jar") + if got.IsNil() { + t.Fatalf("expected cookie jar to be set") + } +} + +func TestWithRedirect(t *testing.T) { + c := New(Redirect()) + if c == nil { + t.Fatalf("expected client") + } + + c = New(Redirect(req.NoRedirectPolicy())) + got := reflect.ValueOf(c.req).Elem().FieldByName("httpClient").Elem().FieldByName("CheckRedirect") + if got.IsNil() { + t.Fatalf("expected redirect policy to be set") + } +} + type roundTripperFunc func(*http.Request) (*http.Response, error) func (rt roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { diff --git a/options_debug.go b/options_debug.go index f7c268b..c8469bc 100644 --- a/options_debug.go +++ b/options_debug.go @@ -126,3 +126,39 @@ func (b OptionBuilder) DumpEachRequestTo(output io.Writer) OptionBuilder { }) })) } + +// Trace enables req's request-level trace output. +// @group Debugging +// +// Applies to individual requests only. +// Example: trace a single request +// +// c := httpx.New() +// _ = httpx.Get[string](c, "https://example.com", httpx.Trace()) +func Trace() OptionBuilder { + return OptionBuilder{}.Trace() +} + +func (b OptionBuilder) Trace() OptionBuilder { + return b.add(requestOnly(func(r *req.Request) { + r.EnableTrace() + })) +} + +// TraceAll enables req's client-level trace output for all requests. +// @group Debugging +// +// Applies to the client configuration only. +// Example: trace all requests +// +// c := httpx.New(httpx.TraceAll()) +// _ = c +func TraceAll() OptionBuilder { + return OptionBuilder{}.TraceAll() +} + +func (b OptionBuilder) TraceAll() OptionBuilder { + return b.add(clientOnly(func(c *Client) { + c.req.EnableTraceAll() + })) +} diff --git a/options_debug_test.go b/options_debug_test.go index acf824e..74462a5 100644 --- a/options_debug_test.go +++ b/options_debug_test.go @@ -108,3 +108,20 @@ func TestDumpEachRequestFunction(t *testing.T) { t.Fatalf("request failed: %v", res.Err) } } + +func TestTrace(t *testing.T) { + r := req.C().R() + Trace().applyRequest(r) + traceField := reflect.ValueOf(r).Elem().FieldByName("trace") + if traceField.IsNil() { + t.Fatalf("expected trace to be enabled") + } +} + +func TestTraceAll(t *testing.T) { + c := New(TraceAll()) + traceField := reflect.ValueOf(c.Req()).Elem().FieldByName("trace") + if !traceField.Bool() { + t.Fatalf("expected trace to be enabled") + } +} From 54e9b8cb8c2c9f4f77c905ce10a10aa19c7834dd Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 28 Dec 2025 20:51:40 -0600 Subject: [PATCH 2/5] chore: code cov --- README.md | 2 +- client_test.go | 33 +++++++++++++++++++++++++++++++++ options_client_test.go | 7 +++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d24020..fc81b54 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It keeps req's power and escape hatches, while making the 90% use case feel effo Go Report Card - Tests + Tests

diff --git a/client_test.go b/client_test.go index 02c8ddf..bb1ae9d 100644 --- a/client_test.go +++ b/client_test.go @@ -151,6 +151,39 @@ func TestOptionsRequest(t *testing.T) { } } +func TestHeadCtxRequest(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Fatalf("method = %s", r.Method) + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + client := New() + ctx := context.Background() + res := HeadCtx[string](client, ctx, server.URL) + if res.Err != nil { + t.Fatalf("unexpected error: %v", res.Err) + } +} + +func TestOptionsCtxRequest(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodOptions { + t.Fatalf("method = %s", r.Method) + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + client := New() + ctx := context.Background() + res := OptionsCtx[string](client, ctx, server.URL) + if res.Err != nil { + t.Fatalf("unexpected error: %v", res.Err) + } +} func TestOptionsApplied(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/users/123" { diff --git a/options_client_test.go b/options_client_test.go index a745875..ba7c211 100644 --- a/options_client_test.go +++ b/options_client_test.go @@ -139,6 +139,13 @@ func TestWithProxy(t *testing.T) { } } +func TestWithProxyEmpty(t *testing.T) { + c := New(Proxy("")) + if c.req.Transport.Options.Proxy == nil { + t.Fatalf("expected proxy to remain set") + } +} + func TestWithProxyFunc(t *testing.T) { c := New(ProxyFunc(nil)) if c == nil { From 3047e3421d93fccc41edb25ab657c19ff07a0aab Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 28 Dec 2025 20:59:43 -0600 Subject: [PATCH 3/5] docs: cookie example --- README.md | 4 ++++ docs/examplegen/main.go | 1 + examples/cookiejar/main.go | 8 +++++++- options_client.go | 6 +++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc81b54..21556b6 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,10 @@ CookieJar sets the cookie jar for the client. ```go jar, _ := cookiejar.New(nil) +u, _ := url.Parse("https://example.com") +jar.SetCookies(u, []*http.Cookie{ + {Name: "session", Value: "abc123"}, +}) c := httpx.New(httpx.CookieJar(jar)) _ = c ``` diff --git a/docs/examplegen/main.go b/docs/examplegen/main.go index 28941cf..59ac773 100644 --- a/docs/examplegen/main.go +++ b/docs/examplegen/main.go @@ -391,6 +391,7 @@ func writeMain(base string, fd *FuncDoc, importPath string) error { "context.": "context", "regexp.": "regexp", "cookiejar.": "net/http/cookiejar", + "url.": "net/url", "redis.": "github.com/redis/go-redis/v9", "time.": "time", "gocron": "github.com/go-co-op/gocron/v2", diff --git a/examples/cookiejar/main.go b/examples/cookiejar/main.go index b2d8535..0525ff0 100644 --- a/examples/cookiejar/main.go +++ b/examples/cookiejar/main.go @@ -5,14 +5,20 @@ package main import ( "github.com/goforj/httpx" + "net/http" "net/http/cookiejar" + "net/url" ) func main() { // CookieJar sets the cookie jar for the client. - // Example: set cookie jar + // Example: set cookie jar and seed cookies jar, _ := cookiejar.New(nil) + u, _ := url.Parse("https://example.com") + jar.SetCookies(u, []*http.Cookie{ + {Name: "session", Value: "abc123"}, + }) c := httpx.New(httpx.CookieJar(jar)) _ = c } diff --git a/options_client.go b/options_client.go index 09e4991..b23340a 100644 --- a/options_client.go +++ b/options_client.go @@ -137,9 +137,13 @@ func (b OptionBuilder) ProxyFunc(fn func(*http.Request) (*url.URL, error)) Optio // @group Client Options // // Applies to client configuration only. -// Example: set cookie jar +// Example: set cookie jar and seed cookies // // jar, _ := cookiejar.New(nil) +// u, _ := url.Parse("https://example.com") +// jar.SetCookies(u, []*http.Cookie{ +// {Name: "session", Value: "abc123"}, +// }) // c := httpx.New(httpx.CookieJar(jar)) // _ = c func CookieJar(jar http.CookieJar) OptionBuilder { From 9307845923bac3e570af78cff56f710cbd85dd2b Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 28 Dec 2025 21:12:01 -0600 Subject: [PATCH 4/5] docs: cleanup --- README.md | 109 +++++++++++++++++++++------- examples/auth/main.go | 8 +- examples/basic/main.go | 8 +- examples/bearer/main.go | 8 +- examples/header/main.go | 8 +- examples/headers/main.go | 11 ++- examples/retrybackoff/main.go | 10 ++- examples/retrycondition/main.go | 10 ++- examples/retrycount/main.go | 10 ++- examples/retryfixedinterval/main.go | 10 ++- examples/retryhook/main.go | 8 +- examples/retryinterval/main.go | 10 ++- examples/timeout/main.go | 10 ++- examples/useragent/main.go | 6 +- options_auth.go | 24 ++++-- options_request.go | 35 +++++++-- options_retry.go | 58 +++++++++++---- 17 files changed, 260 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 21556b6..7e4873e 100644 --- a/README.md +++ b/README.md @@ -163,8 +163,12 @@ They are compiled by `example_compile_test.go` to keep docs and code in sync. Auth sets the Authorization header using a scheme and token. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Auth("Token", "abc123")) +// Apply to all requests +c := httpx.New(httpx.Auth("Token", "abc123")) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Auth("Token", "abc123")) ``` ### Basic @@ -172,8 +176,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.Auth("Token", "abc123")) Basic sets HTTP basic authentication headers. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Basic("user", "pass")) +// Apply to all requests +c := httpx.New(httpx.Basic("user", "pass")) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Basic("user", "pass")) ``` ### Bearer @@ -181,8 +189,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.Basic("user", "pass")) Bearer sets the Authorization header with a bearer token. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Bearer("token")) +// Apply to all requests +c := httpx.New(httpx.Bearer("token")) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Bearer("token")) ``` ## Browser Profiles @@ -516,8 +528,12 @@ _ = httpx.Post[any, string](c, "https://example.com", nil, httpx.Form(map[string Header sets a header on a request or client. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Header("X-Trace", "1")) +// Apply to all requests +c := httpx.New(httpx.Header("X-Trace", "1")) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Header("X-Trace", "1")) ``` ### Headers @@ -525,8 +541,15 @@ _ = httpx.Get[string](c, "https://example.com", httpx.Header("X-Trace", "1")) Headers sets multiple headers on a request or client. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Headers(map[string]string{ +// Apply to all requests +c := httpx.New(httpx.Headers(map[string]string{ + "X-Trace": "1", + "Accept": "application/json", +})) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Headers(map[string]string{ "X-Trace": "1", "Accept": "application/json", })) @@ -600,8 +623,12 @@ _ = httpx.Get[string](c, "https://example.com/search", httpx.Query("q", "go", "o UserAgent sets the User-Agent header on a request or client. ```go +// Apply to all requests c := httpx.New(httpx.UserAgent("my-app/1.0")) -_ = httpx.Get[string](c, "https://example.com") +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.UserAgent("my-app/1.0")) ``` ## Request Control @@ -622,8 +649,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.Before(func(r *req.Request Timeout sets a per-request timeout using context cancellation. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.Timeout(2*time.Second)) +// Apply to all requests +c := httpx.New(httpx.Timeout(2 * time.Second)) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.Timeout(2*time.Second)) ``` ## Requests @@ -846,8 +877,12 @@ _, _ = res.Body, res.Err RetryBackoff sets a capped exponential backoff retry interval for a request. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) +// Apply to all requests +c := httpx.New(httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) ``` ### RetryCondition @@ -855,8 +890,14 @@ _ = httpx.Get[string](c, "https://example.com", httpx.RetryBackoff(100*time.Mill RetryCondition sets the retry condition for a request. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { +// Apply to all requests +c := httpx.New(httpx.RetryCondition(func(resp *req.Response, _ error) bool { + return resp != nil && resp.StatusCode == 503 +})) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { return resp != nil && resp.StatusCode == 503 })) ``` @@ -866,8 +907,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.RetryCondition(func(resp * RetryCount enables retry for a request and sets the maximum retry count. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryCount(2)) +// Apply to all requests +c := httpx.New(httpx.RetryCount(2)) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCount(2)) ``` ### RetryFixedInterval @@ -875,8 +920,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.RetryCount(2)) RetryFixedInterval sets a fixed retry interval for a request. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) +// Apply to all requests +c := httpx.New(httpx.RetryFixedInterval(200 * time.Millisecond)) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) ``` ### RetryHook @@ -884,8 +933,12 @@ _ = httpx.Get[string](c, "https://example.com", httpx.RetryFixedInterval(200*tim RetryHook registers a retry hook for a request. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) +// Apply to all requests +c := httpx.New(httpx.RetryHook(func(_ *req.Response, _ error) {})) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) ``` ### RetryInterval @@ -893,8 +946,14 @@ _ = httpx.Get[string](c, "https://example.com", httpx.RetryHook(func(_ *req.Resp RetryInterval sets a custom retry interval function for a request. ```go -c := httpx.New() -_ = httpx.Get[string](c, "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { +// Apply to all requests +c := httpx.New(httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { + return time.Duration(attempt) * 100 * time.Millisecond +})) +httpx.Get[string](c, "https://example.com") + +// Apply to a single request +httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { return time.Duration(attempt) * 100 * time.Millisecond })) ``` diff --git a/examples/auth/main.go b/examples/auth/main.go index ff434fe..b906650 100644 --- a/examples/auth/main.go +++ b/examples/auth/main.go @@ -9,6 +9,10 @@ func main() { // Auth sets the Authorization header using a scheme and token. // Example: custom auth scheme - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Auth("Token", "abc123")) + // Apply to all requests + c := httpx.New(httpx.Auth("Token", "abc123")) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Auth("Token", "abc123")) } diff --git a/examples/basic/main.go b/examples/basic/main.go index fdedc70..89dfea9 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -9,6 +9,10 @@ func main() { // Basic sets HTTP basic authentication headers. // Example: basic auth - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Basic("user", "pass")) + // Apply to all requests + c := httpx.New(httpx.Basic("user", "pass")) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Basic("user", "pass")) } diff --git a/examples/bearer/main.go b/examples/bearer/main.go index dc8392f..e47ede6 100644 --- a/examples/bearer/main.go +++ b/examples/bearer/main.go @@ -9,6 +9,10 @@ func main() { // Bearer sets the Authorization header with a bearer token. // Example: bearer auth - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Bearer("token")) + // Apply to all requests + c := httpx.New(httpx.Bearer("token")) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Bearer("token")) } diff --git a/examples/header/main.go b/examples/header/main.go index aa35934..9e7a712 100644 --- a/examples/header/main.go +++ b/examples/header/main.go @@ -9,6 +9,10 @@ func main() { // Header sets a header on a request or client. // Example: apply a header - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Header("X-Trace", "1")) + // Apply to all requests + c := httpx.New(httpx.Header("X-Trace", "1")) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Header("X-Trace", "1")) } diff --git a/examples/headers/main.go b/examples/headers/main.go index a112875..3875ad5 100644 --- a/examples/headers/main.go +++ b/examples/headers/main.go @@ -9,8 +9,15 @@ func main() { // Headers sets multiple headers on a request or client. // Example: apply headers - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Headers(map[string]string{ + // Apply to all requests + c := httpx.New(httpx.Headers(map[string]string{ + "X-Trace": "1", + "Accept": "application/json", + })) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Headers(map[string]string{ "X-Trace": "1", "Accept": "application/json", })) diff --git a/examples/retrybackoff/main.go b/examples/retrybackoff/main.go index 475ede0..6e5657e 100644 --- a/examples/retrybackoff/main.go +++ b/examples/retrybackoff/main.go @@ -11,7 +11,11 @@ import ( func main() { // RetryBackoff sets a capped exponential backoff retry interval for a request. - // Example: request retry backoff - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) + // Example: retry backoff + // Apply to all requests + c := httpx.New(httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) } diff --git a/examples/retrycondition/main.go b/examples/retrycondition/main.go index c862e89..fb792e3 100644 --- a/examples/retrycondition/main.go +++ b/examples/retrycondition/main.go @@ -12,8 +12,14 @@ func main() { // RetryCondition sets the retry condition for a request. // Example: retry on 503 - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { + // Apply to all requests + c := httpx.New(httpx.RetryCondition(func(resp *req.Response, _ error) bool { + return resp != nil && resp.StatusCode == 503 + })) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { return resp != nil && resp.StatusCode == 503 })) } diff --git a/examples/retrycount/main.go b/examples/retrycount/main.go index c925e5f..a169180 100644 --- a/examples/retrycount/main.go +++ b/examples/retrycount/main.go @@ -8,7 +8,11 @@ import "github.com/goforj/httpx" func main() { // RetryCount enables retry for a request and sets the maximum retry count. - // Example: request retry count - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryCount(2)) + // Example: retry count + // Apply to all requests + c := httpx.New(httpx.RetryCount(2)) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCount(2)) } diff --git a/examples/retryfixedinterval/main.go b/examples/retryfixedinterval/main.go index cfbb0da..e65e43a 100644 --- a/examples/retryfixedinterval/main.go +++ b/examples/retryfixedinterval/main.go @@ -11,7 +11,11 @@ import ( func main() { // RetryFixedInterval sets a fixed retry interval for a request. - // Example: request retry interval - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) + // Example: retry interval + // Apply to all requests + c := httpx.New(httpx.RetryFixedInterval(200 * time.Millisecond)) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) } diff --git a/examples/retryhook/main.go b/examples/retryhook/main.go index c0cbf53..881c752 100644 --- a/examples/retryhook/main.go +++ b/examples/retryhook/main.go @@ -12,6 +12,10 @@ func main() { // RetryHook registers a retry hook for a request. // Example: hook on retry - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) + // Apply to all requests + c := httpx.New(httpx.RetryHook(func(_ *req.Response, _ error) {})) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) } diff --git a/examples/retryinterval/main.go b/examples/retryinterval/main.go index 061ea09..54c1bce 100644 --- a/examples/retryinterval/main.go +++ b/examples/retryinterval/main.go @@ -13,8 +13,14 @@ func main() { // RetryInterval sets a custom retry interval function for a request. // Example: custom retry interval - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { + // Apply to all requests + c := httpx.New(httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { + return time.Duration(attempt) * 100 * time.Millisecond + })) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { return time.Duration(attempt) * 100 * time.Millisecond })) } diff --git a/examples/timeout/main.go b/examples/timeout/main.go index db1fc80..68cd57e 100644 --- a/examples/timeout/main.go +++ b/examples/timeout/main.go @@ -11,7 +11,11 @@ import ( func main() { // Timeout sets a per-request timeout using context cancellation. - // Example: per-request timeout - c := httpx.New() - _ = httpx.Get[string](c, "https://example.com", httpx.Timeout(2*time.Second)) + // Example: timeout + // Apply to all requests + c := httpx.New(httpx.Timeout(2 * time.Second)) + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.Timeout(2*time.Second)) } diff --git a/examples/useragent/main.go b/examples/useragent/main.go index 7c0fafc..bccacfb 100644 --- a/examples/useragent/main.go +++ b/examples/useragent/main.go @@ -9,6 +9,10 @@ func main() { // UserAgent sets the User-Agent header on a request or client. // Example: set a User-Agent + // Apply to all requests c := httpx.New(httpx.UserAgent("my-app/1.0")) - _ = httpx.Get[string](c, "https://example.com") + httpx.Get[string](c, "https://example.com") + + // Apply to a single request + httpx.Get[string](httpx.Default(), "https://example.com", httpx.UserAgent("my-app/1.0")) } diff --git a/options_auth.go b/options_auth.go index fc1fc0b..e789509 100644 --- a/options_auth.go +++ b/options_auth.go @@ -12,8 +12,12 @@ import ( // // Example: custom auth scheme // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Auth("Token", "abc123")) +// // Apply to all requests +// c := httpx.New(httpx.Auth("Token", "abc123")) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Auth("Token", "abc123")) func Auth(scheme, token string) OptionBuilder { return OptionBuilder{}.Auth(scheme, token) } @@ -36,8 +40,12 @@ func (b OptionBuilder) Auth(scheme, token string) OptionBuilder { // // Example: bearer auth // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Bearer("token")) +// // Apply to all requests +// c := httpx.New(httpx.Bearer("token")) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Bearer("token")) func Bearer(token string) OptionBuilder { return OptionBuilder{}.Bearer(token) } @@ -60,8 +68,12 @@ func (b OptionBuilder) Bearer(token string) OptionBuilder { // // Example: basic auth // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Basic("user", "pass")) +// // Apply to all requests +// c := httpx.New(httpx.Basic("user", "pass")) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Basic("user", "pass")) func Basic(user, pass string) OptionBuilder { return OptionBuilder{}.Basic(user, pass) } diff --git a/options_request.go b/options_request.go index 5566c26..9bd40d9 100644 --- a/options_request.go +++ b/options_request.go @@ -15,8 +15,12 @@ import ( // Applies to both client defaults and request-time headers. // Example: apply a header // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Header("X-Trace", "1")) +// // Apply to all requests +// c := httpx.New(httpx.Header("X-Trace", "1")) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Header("X-Trace", "1")) func Header(key, value string) OptionBuilder { return OptionBuilder{}.Header(key, value) } @@ -38,8 +42,15 @@ func (b OptionBuilder) Header(key, value string) OptionBuilder { // Applies to both client defaults and request-time headers. // Example: apply headers // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Headers(map[string]string{ +// // Apply to all requests +// c := httpx.New(httpx.Headers(map[string]string{ +// "X-Trace": "1", +// "Accept": "application/json", +// })) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Headers(map[string]string{ // "X-Trace": "1", // "Accept": "application/json", // })) @@ -64,8 +75,12 @@ func (b OptionBuilder) Headers(values map[string]string) OptionBuilder { // Applies to both client defaults and request-time headers. // Example: set a User-Agent // +// // Apply to all requests // c := httpx.New(httpx.UserAgent("my-app/1.0")) -// _ = httpx.Get[string](c, "https://example.com") +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.UserAgent("my-app/1.0")) func UserAgent(value string) OptionBuilder { return OptionBuilder{}.UserAgent(value) } @@ -244,10 +259,14 @@ func (b OptionBuilder) Form(values map[string]string) OptionBuilder { // @group Request Control // // Applies to both client defaults (via WithTimeout) and individual requests. -// Example: per-request timeout +// Example: timeout // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.Timeout(2*time.Second)) +// // Apply to all requests +// c := httpx.New(httpx.Timeout(2 * time.Second)) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.Timeout(2*time.Second)) func Timeout(d time.Duration) OptionBuilder { return OptionBuilder{}.Timeout(d) } diff --git a/options_retry.go b/options_retry.go index 86091a2..b4f0b75 100644 --- a/options_retry.go +++ b/options_retry.go @@ -10,10 +10,14 @@ import ( // @group Retry // // Applies to both client defaults and individual requests. -// Example: request retry count +// Example: retry count // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryCount(2)) +// // Apply to all requests +// c := httpx.New(httpx.RetryCount(2)) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCount(2)) func RetryCount(count int) OptionBuilder { return OptionBuilder{}.RetryCount(count) } @@ -33,10 +37,14 @@ func (b OptionBuilder) RetryCount(count int) OptionBuilder { // @group Retry // // Applies to both client defaults and individual requests. -// Example: request retry interval +// Example: retry interval +// +// // Apply to all requests +// c := httpx.New(httpx.RetryFixedInterval(200 * time.Millisecond)) +// httpx.Get[string](c, "https://example.com") // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryFixedInterval(200*time.Millisecond)) func RetryFixedInterval(interval time.Duration) OptionBuilder { return OptionBuilder{}.RetryFixedInterval(interval) } @@ -56,10 +64,14 @@ func (b OptionBuilder) RetryFixedInterval(interval time.Duration) OptionBuilder // @group Retry // // Applies to both client defaults and individual requests. -// Example: request retry backoff +// Example: retry backoff +// +// // Apply to all requests +// c := httpx.New(httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) +// httpx.Get[string](c, "https://example.com") // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryBackoff(100*time.Millisecond, 2*time.Second)) func RetryBackoff(min, max time.Duration) OptionBuilder { return OptionBuilder{}.RetryBackoff(min, max) } @@ -81,8 +93,14 @@ func (b OptionBuilder) RetryBackoff(min, max time.Duration) OptionBuilder { // Applies to both client defaults and individual requests. // Example: custom retry interval // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { +// // Apply to all requests +// c := httpx.New(httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { +// return time.Duration(attempt) * 100 * time.Millisecond +// })) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryInterval(func(_ *req.Response, attempt int) time.Duration { // return time.Duration(attempt) * 100 * time.Millisecond // })) func RetryInterval(fn req.GetRetryIntervalFunc) OptionBuilder { @@ -106,8 +124,14 @@ func (b OptionBuilder) RetryInterval(fn req.GetRetryIntervalFunc) OptionBuilder // Applies to both client defaults and individual requests. // Example: retry on 503 // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { +// // Apply to all requests +// c := httpx.New(httpx.RetryCondition(func(resp *req.Response, _ error) bool { +// return resp != nil && resp.StatusCode == 503 +// })) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryCondition(func(resp *req.Response, _ error) bool { // return resp != nil && resp.StatusCode == 503 // })) func RetryCondition(condition req.RetryConditionFunc) OptionBuilder { @@ -131,8 +155,12 @@ func (b OptionBuilder) RetryCondition(condition req.RetryConditionFunc) OptionBu // Applies to both client defaults and individual requests. // Example: hook on retry // -// c := httpx.New() -// _ = httpx.Get[string](c, "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) +// // Apply to all requests +// c := httpx.New(httpx.RetryHook(func(_ *req.Response, _ error) {})) +// httpx.Get[string](c, "https://example.com") +// +// // Apply to a single request +// httpx.Get[string](httpx.Default(), "https://example.com", httpx.RetryHook(func(_ *req.Response, _ error) {})) func RetryHook(hook req.RetryHookFunc) OptionBuilder { return OptionBuilder{}.RetryHook(hook) } From 0c23851d0f69168a37779409958c372974d06ee6 Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 28 Dec 2025 21:21:16 -0600 Subject: [PATCH 5/5] docs: example refinement --- README.md | 28 +++++++++++++++++++--------- client.go | 26 ++++++++++++++++---------- examples/delete/main.go | 2 +- examples/deletectx/main.go | 2 +- examples/get/main.go | 7 ++++++- examples/getctx/main.go | 2 +- examples/patch/main.go | 2 +- examples/patchctx/main.go | 2 +- examples/post/main.go | 2 +- examples/postctx/main.go | 2 +- examples/put/main.go | 2 +- examples/putctx/main.go | 2 +- 12 files changed, 50 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 7e4873e..ca14916 100644 --- a/README.md +++ b/README.md @@ -670,13 +670,15 @@ type DeleteResponse struct { c := httpx.New() res := httpx.Delete[DeleteResponse](c, "https://api.example.com/users/1") -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is DeleteResponse ``` ### Get Get issues a GET request using the provided client. +_Example: fetch GitHub pull requests (typed)_ + ```go type PullRequest struct { Number int `json:"number"` @@ -691,6 +693,14 @@ if res.Err != nil { godump.Dump(res.Body) ``` +_Example: bind to a string body_ + +```go +c2 := httpx.New() +res2 := httpx.Get[string](c2, "https://httpbin.org/uuid") +_, _ = res2.Body, res2.Err // Body is string +``` + ### Head Head issues a HEAD request using the provided client. @@ -725,7 +735,7 @@ type User struct { c := httpx.New() res := httpx.Patch[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ### Post @@ -742,7 +752,7 @@ type User struct { c := httpx.New() res := httpx.Post[CreateUser, User](c, "https://api.example.com/users", CreateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ### Put @@ -759,7 +769,7 @@ type User struct { c := httpx.New() res := httpx.Put[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ## Requests (Context) @@ -776,7 +786,7 @@ type DeleteResponse struct { c := httpx.New() ctx := context.Background() res := httpx.DeleteCtx[DeleteResponse](c, ctx, "https://api.example.com/users/1") -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is DeleteResponse ``` ### GetCtx @@ -791,7 +801,7 @@ type User struct { c := httpx.New() ctx := context.Background() res := httpx.GetCtx[User](c, ctx, "https://api.example.com/users/1") -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ### HeadCtx @@ -831,7 +841,7 @@ type User struct { c := httpx.New() ctx := context.Background() res := httpx.PatchCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ### PostCtx @@ -849,7 +859,7 @@ type User struct { c := httpx.New() ctx := context.Background() res := httpx.PostCtx[CreateUser, User](c, ctx, "https://api.example.com/users", CreateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ### PutCtx @@ -867,7 +877,7 @@ type User struct { c := httpx.New() ctx := context.Background() res := httpx.PutCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -_, _ = res.Body, res.Err +_, _ = res.Body, res.Err // Body is User ``` ## Retry diff --git a/client.go b/client.go index 278a657..7a1c96f 100644 --- a/client.go +++ b/client.go @@ -126,7 +126,7 @@ func (c *Client) Raw() *req.Client { // Get issues a GET request using the provided client. // @group Requests // -// Example: fetch GitHub pull requests +// Example: fetch GitHub pull requests (typed) // // type PullRequest struct { // Number int `json:"number"` @@ -139,6 +139,12 @@ func (c *Client) Raw() *req.Client { // return // } // godump.Dump(res.Body) +// +// Example: bind to a string body +// +// c2 := httpx.New() +// res2 := httpx.Get[string](c2, "https://httpbin.org/uuid") +// _, _ = res2.Body, res2.Err // Body is string func Get[T any](client *Client, url string, opts ...Option) Result[T] { return do[T](client, nil, methodGet, url, nil, opts) } @@ -157,7 +163,7 @@ func Get[T any](client *Client, url string, opts ...Option) Result[T] { // // c := httpx.New() // res := httpx.Post[CreateUser, User](c, "https://api.example.com/users", CreateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func Post[In any, Out any](client *Client, url string, body In, opts ...Option) Result[Out] { return do[Out](client, nil, methodPost, url, body, opts) } @@ -176,7 +182,7 @@ func Post[In any, Out any](client *Client, url string, body In, opts ...Option) // // c := httpx.New() // res := httpx.Put[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func Put[In any, Out any](client *Client, url string, body In, opts ...Option) Result[Out] { return do[Out](client, nil, methodPut, url, body, opts) } @@ -195,7 +201,7 @@ func Put[In any, Out any](client *Client, url string, body In, opts ...Option) R // // c := httpx.New() // res := httpx.Patch[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func Patch[In any, Out any](client *Client, url string, body In, opts ...Option) Result[Out] { return do[Out](client, nil, methodPatch, url, body, opts) } @@ -211,7 +217,7 @@ func Patch[In any, Out any](client *Client, url string, body In, opts ...Option) // // c := httpx.New() // res := httpx.Delete[DeleteResponse](c, "https://api.example.com/users/1") -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is DeleteResponse func Delete[T any](client *Client, url string, opts ...Option) Result[T] { return do[T](client, nil, methodDelete, url, nil, opts) } @@ -252,7 +258,7 @@ func Options[T any](client *Client, url string, opts ...Option) Result[T] { // c := httpx.New() // ctx := context.Background() // res := httpx.GetCtx[User](c, ctx, "https://api.example.com/users/1") -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func GetCtx[T any](client *Client, ctx context.Context, url string, opts ...Option) Result[T] { return do[T](client, ctx, methodGet, url, nil, opts) } @@ -272,7 +278,7 @@ func GetCtx[T any](client *Client, ctx context.Context, url string, opts ...Opti // c := httpx.New() // ctx := context.Background() // res := httpx.PostCtx[CreateUser, User](c, ctx, "https://api.example.com/users", CreateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func PostCtx[In any, Out any](client *Client, ctx context.Context, url string, body In, opts ...Option) Result[Out] { return do[Out](client, ctx, methodPost, url, body, opts) } @@ -292,7 +298,7 @@ func PostCtx[In any, Out any](client *Client, ctx context.Context, url string, b // c := httpx.New() // ctx := context.Background() // res := httpx.PutCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func PutCtx[In any, Out any](client *Client, ctx context.Context, url string, body In, opts ...Option) Result[Out] { return do[Out](client, ctx, methodPut, url, body, opts) } @@ -312,7 +318,7 @@ func PutCtx[In any, Out any](client *Client, ctx context.Context, url string, bo // c := httpx.New() // ctx := context.Background() // res := httpx.PatchCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is User func PatchCtx[In any, Out any](client *Client, ctx context.Context, url string, body In, opts ...Option) Result[Out] { return do[Out](client, ctx, methodPatch, url, body, opts) } @@ -329,7 +335,7 @@ func PatchCtx[In any, Out any](client *Client, ctx context.Context, url string, // c := httpx.New() // ctx := context.Background() // res := httpx.DeleteCtx[DeleteResponse](c, ctx, "https://api.example.com/users/1") -// _, _ = res.Body, res.Err +// _, _ = res.Body, res.Err // Body is DeleteResponse func DeleteCtx[T any](client *Client, ctx context.Context, url string, opts ...Option) Result[T] { return do[T](client, ctx, methodDelete, url, nil, opts) } diff --git a/examples/delete/main.go b/examples/delete/main.go index e8b8b2f..b329f87 100644 --- a/examples/delete/main.go +++ b/examples/delete/main.go @@ -15,5 +15,5 @@ func main() { c := httpx.New() res := httpx.Delete[DeleteResponse](c, "https://api.example.com/users/1") - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is DeleteResponse } diff --git a/examples/deletectx/main.go b/examples/deletectx/main.go index 105e0ba..16569f8 100644 --- a/examples/deletectx/main.go +++ b/examples/deletectx/main.go @@ -19,5 +19,5 @@ func main() { c := httpx.New() ctx := context.Background() res := httpx.DeleteCtx[DeleteResponse](c, ctx, "https://api.example.com/users/1") - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is DeleteResponse } diff --git a/examples/get/main.go b/examples/get/main.go index 9610dc6..8c7b5fc 100644 --- a/examples/get/main.go +++ b/examples/get/main.go @@ -11,7 +11,7 @@ import ( func main() { // Get issues a GET request using the provided client. - // Example: fetch GitHub pull requests + // Example: fetch GitHub pull requests (typed) type PullRequest struct { Number int `json:"number"` Title string `json:"title"` @@ -23,4 +23,9 @@ func main() { return } godump.Dump(res.Body) + + // Example: bind to a string body + c2 := httpx.New() + res2 := httpx.Get[string](c2, "https://httpbin.org/uuid") + _, _ = res2.Body, res2.Err // Body is string } diff --git a/examples/getctx/main.go b/examples/getctx/main.go index faecc1c..c5bdcda 100644 --- a/examples/getctx/main.go +++ b/examples/getctx/main.go @@ -19,5 +19,5 @@ func main() { c := httpx.New() ctx := context.Background() res := httpx.GetCtx[User](c, ctx, "https://api.example.com/users/1") - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/patch/main.go b/examples/patch/main.go index 7ccec2a..4c22c44 100644 --- a/examples/patch/main.go +++ b/examples/patch/main.go @@ -18,5 +18,5 @@ func main() { c := httpx.New() res := httpx.Patch[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/patchctx/main.go b/examples/patchctx/main.go index 6783c02..c59fb7a 100644 --- a/examples/patchctx/main.go +++ b/examples/patchctx/main.go @@ -22,5 +22,5 @@ func main() { c := httpx.New() ctx := context.Background() res := httpx.PatchCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/post/main.go b/examples/post/main.go index 13d08d8..5ed3892 100644 --- a/examples/post/main.go +++ b/examples/post/main.go @@ -18,5 +18,5 @@ func main() { c := httpx.New() res := httpx.Post[CreateUser, User](c, "https://api.example.com/users", CreateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/postctx/main.go b/examples/postctx/main.go index 0655d80..c6d1864 100644 --- a/examples/postctx/main.go +++ b/examples/postctx/main.go @@ -22,5 +22,5 @@ func main() { c := httpx.New() ctx := context.Background() res := httpx.PostCtx[CreateUser, User](c, ctx, "https://api.example.com/users", CreateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/put/main.go b/examples/put/main.go index c85ec6c..3ef6f4f 100644 --- a/examples/put/main.go +++ b/examples/put/main.go @@ -18,5 +18,5 @@ func main() { c := httpx.New() res := httpx.Put[UpdateUser, User](c, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User } diff --git a/examples/putctx/main.go b/examples/putctx/main.go index e14772e..84fa4e2 100644 --- a/examples/putctx/main.go +++ b/examples/putctx/main.go @@ -22,5 +22,5 @@ func main() { c := httpx.New() ctx := context.Background() res := httpx.PutCtx[UpdateUser, User](c, ctx, "https://api.example.com/users/1", UpdateUser{Name: "Ana"}) - _, _ = res.Body, res.Err + _, _ = res.Body, res.Err // Body is User }