高级示例
自定义重试策略
仅对 502/503/504 重试,使用固定延迟:
内部类型
RetryPolicy.ShouldRetry 的 resp 参数类型 ResponseReader 为内部接口(定义在 internal/types 包中),外部包无法直接引用。自定义 RetryPolicy 必须在与 httpc 同一模块内的包中实现。大多数场景可通过 RetryConfig 配置满足需求。以下示例展示实现模式,实际代码需在 httpc 模块内部编译。
// 注意:ResponseReader 是内部类型(internal/types 包)。
// 此代码仅能在 github.com/cybergodev/httpc 模块内部编译。
// 大多数用户应通过 RetryConfig 和 WithMaxRetries 配置重试。
type selectiveRetry struct {
maxAttempts int
baseDelay time.Duration
}
// 判断是否应该重试
func (p *selectiveRetry) ShouldRetry(resp ResponseReader, err error, attempt int) bool {
if attempt >= p.maxAttempts {
return false
}
if err != nil {
return true // 网络错误重试
}
return resp.StatusCode() == 502 || resp.StatusCode() == 503 || resp.StatusCode() == 504
}
func (p *selectiveRetry) GetDelay(attempt int) time.Duration {
return p.baseDelay * time.Duration(attempt+1)
}
func (p *selectiveRetry) MaxRetries() int {
return p.maxAttempts
}
// 应用自定义策略
cfg := httpc.DefaultConfig()
cfg.Retry.CustomPolicy = &selectiveRetry{maxAttempts: 5, baseDelay: time.Second}外部项目的替代方案 — 使用 RetryConfig 配置:
package main
import (
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
func main() {
cfg := httpc.DefaultConfig()
cfg.Retry.MaxRetries = 5
cfg.Retry.Delay = 500 * time.Millisecond
cfg.Retry.BackoffFactor = 1.5
cfg.Retry.EnableJitter = true
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
result, err := client.Get("https://api.example.com/unstable")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.StatusCode())
}超时与重试组合
三层时间控制各司其职:context 管整组尝试的总预算,WithTimeout 管单次尝试,WithMaxRetries 管尝试次数:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
func main() {
client, err := httpc.NewDefault()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// ctx 30s:含全部重试与退避等待的总预算
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := client.Post("https://httpbin.org/post",
httpc.WithJSON(map[string]string{"data": "important"}),
httpc.WithContext(ctx), // 总预算
httpc.WithTimeout(10*time.Second), // 单次尝试上限
httpc.WithMaxRetries(3), // 最多重试 3 次
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.StatusCode()) // 输出:200
fmt.Println("尝试次数:", result.Meta.Attempts) // 输出:尝试次数:1(失败重试后增加)
fmt.Println("总耗时:", result.Meta.Duration)
}需要禁用重试(如非幂等的创建操作)时,WithMaxRetries(0) 显式声明:
result, err := client.Post("https://httpbin.org/post",
httpc.WithJSON(map[string]string{"action": "create"}),
httpc.WithMaxRetries(0), // 不重试:避免重复创建
)完整中间件链
package main
import (
"encoding/json"
"log"
"sync/atomic"
"time"
"github.com/cybergodev/httpc"
)
func main() {
// 请求计数器
var requestCount int64
// 指标收集
metricsMiddleware := httpc.MetricsMiddleware(
&httpc.MetricsConfig{OnMetrics: func(method, url string, statusCode int, duration time.Duration, err error) {
atomic.AddInt64(&requestCount, 1)
log.Printf("[METRICS] %s %s -> %d (%v)", method, url, statusCode, duration)
}},
)
// 审计日志(JSON 格式)
auditCfg := httpc.DefaultAuditConfig()
auditCfg.Format = "json"
auditCfg.IncludeHeaders = true
auditCfg.MaskHeaders = []string{"Authorization", "Cookie"}
auditCfg.SanitizeError = true
auditCfg.OnAudit = func(event httpc.AuditEvent) {
data, _ := json.Marshal(event)
log.Printf("[AUDIT] %s", data)
}
auditMiddleware := httpc.AuditMiddleware(auditCfg)
cfg := httpc.DefaultConfig()
cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
httpc.RecoveryMiddleware(), // panic 恢复
httpc.TimeoutMiddleware(&httpc.TimeoutMiddlewareConfig{Duration: 30 * time.Second}), // 强制超时
httpc.RequestIDMiddleware(httpc.DefaultRequestIDConfig()), // 请求 ID
httpc.LoggingMiddleware(&httpc.LoggingConfig{LogFunc: func(format string, args ...any) {
log.Printf("[HTTP] "+format, args...)
}}),
metricsMiddleware,
auditMiddleware,
}
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
_, err = client.Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
log.Printf("总请求数:%d", atomic.LoadInt64(&requestCount))
}请求/响应回调
不需要完整中间件时,WithOnRequest / WithOnResponse 两个请求级回调即可覆盖轻量的观测与调试需求——发送前拿到可修改的请求、完成后拿到可读取的响应:
package main
import (
"fmt"
"log"
"github.com/cybergodev/httpc"
)
func main() {
client, err := httpc.NewDefault()
if err != nil {
log.Fatal(err)
}
defer client.Close()
onRequest := func(req httpc.RequestMutator) error {
fmt.Printf("[请求] %s %s(携带 %d 个请求头)\n",
req.Method(), req.URL(), len(req.Headers()))
return nil // 返回非 nil error 会中止请求
}
onResponse := func(resp httpc.ResponseMutator) error {
fmt.Printf("[响应] %d %s,耗时 %v,尝试 %d 次\n",
resp.StatusCode(), resp.Status(), resp.Duration(), resp.Attempts())
return nil
}
result, err := client.Get("https://httpbin.org/get",
httpc.WithOnRequest(onRequest),
httpc.WithOnResponse(onResponse),
httpc.WithQuery("test", "callbacks"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println("状态码:", result.StatusCode()) // 输出:状态码:200
}与中间件的分工:回调是单请求的便捷钩子(日志、调试、简单指标);要跨全部请求、可组合、可短路请求的管道化处理,请用中间件链。
REST API 客户端封装
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
type APIClient struct {
dc httpc.DomainClienter
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func NewAPIClient(baseURL, token string) (*APIClient, error) {
dc, err := httpc.NewDomainDefault(baseURL)
if err != nil {
return nil, err
}
if err := dc.SetHeader("Authorization", "Bearer "+token); err != nil {
dc.Close()
return nil, err
}
if err := dc.SetHeader("Accept", "application/json"); err != nil {
dc.Close()
return nil, err
}
return &APIClient{dc: dc}, nil
}
func (c *APIClient) GetUser(ctx context.Context, id int) (*User, error) {
result, err := c.dc.Request(ctx, "GET", fmt.Sprintf("/users/%d", id))
if err != nil {
return nil, err
}
if !result.IsSuccess() {
return nil, fmt.Errorf("API error: %d", result.StatusCode())
}
var user User
if err := result.Unmarshal(&user); err != nil {
return nil, err
}
return &user, nil
}
func (c *APIClient) CreateUser(ctx context.Context, name string) (*User, error) {
result, err := c.dc.Request(ctx, "POST", "/users",
httpc.WithJSON(map[string]string{"name": name}),
)
if err != nil {
return nil, err
}
var user User
if err := result.Unmarshal(&user); err != nil {
return nil, err
}
return &user, nil
}
func (c *APIClient) Close() error {
return c.dc.Close()
}
func main() {
api, err := NewAPIClient("https://api.example.com", "my-token")
if err != nil {
log.Fatal(err)
}
defer api.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建用户
user, err := api.CreateUser(ctx, "Alice")
if err != nil {
log.Fatal(err)
}
fmt.Printf("创建:%+v\n", user)
// 获取用户
user, err = api.GetUser(ctx, user.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("获取:%+v\n", user)
}域名客户端(自定义配置)
NewDomainDefault(baseURL) 之外,NewDomain(baseURL, cfg) 接受完整 Config——预设、超时、重试、代理全部可定制。域名客户端自动管理会话头与 Cookie,单次请求还能临时覆盖会话头:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/cybergodev/httpc"
)
func main() {
// 在默认配置基础上逐字段定制
cfg := httpc.DefaultConfig()
cfg.Timeouts.Request = 15 * time.Second
cfg.Retry.MaxRetries = 2
cfg.Defaults.UserAgent = "domain-client-demo/1.0"
dc, err := httpc.NewDomain("https://httpbin.org", cfg)
if err != nil {
log.Fatal(err)
}
defer dc.Close()
fmt.Println("Base URL:", dc.URL()) // 输出:Base URL: https://httpbin.org
fmt.Println("Domain:", dc.Domain()) // 输出:Domain: httpbin.org
// 会话头:随该域名下的每个请求自动携带
if err := dc.SetHeaders(map[string]string{
"X-API-Version": "v1",
"X-Client-ID": "client-123",
}); err != nil {
log.Fatal(err)
}
// 单次请求覆盖会话头,不影响持久会话
resp, err := dc.Get("/get",
httpc.WithHeader("X-API-Version", "v2"), // 仅本次请求生效
)
if err != nil {
log.Fatal(err)
}
fmt.Println("请求状态:", resp.StatusCode()) // 输出:请求状态:200
fmt.Println("会话头数量:", len(dc.GetHeaders())) // 输出:会话头数量:2
// 手动注入会话 Cookie;响应 Cookie 也会自动并入会话
if err := dc.SetCookies([]*http.Cookie{
{Name: "session", Value: "abc123"},
}); err != nil {
log.Fatal(err)
}
resp2, err := dc.Get("/cookies") // 会话 Cookie 自动随请求发送
if err != nil {
log.Fatal(err)
}
fmt.Println("第二次请求:", resp2.StatusCode()) // 输出:第二次请求:200
// Session() 拿到底层 SessionManager(下一节演示其独立用法)
session := dc.Session()
session.UpdateFromResult(resp2) // 把本次响应的 Cookie 合并进会话
fmt.Println("会话 Cookie 数量:", len(session.GetCookies()))
}相对路径与选项副作用
path 参数写相对路径(如 /get)即可自动拼接 baseURL;完整 URL(带 scheme)则直接使用。另外域名客户端会把请求选项执行两次(一次捕获会话状态、一次实际请求),避免传入含计数器、nonce 等副作用的选项。
Cookie 高级用法
四种发送 Cookie 的方式
// 1. WithCookies(完整 http.Cookie 结构,可带属性;单个 Cookie 传单元素切片)
result, err := client.Get("https://httpbin.org/cookies",
httpc.WithCookies([]http.Cookie{{
Name: "auth_token",
Value: "xyz789",
Path: "/api",
Expires: time.Now().Add(24 * time.Hour),
Secure: true,
HttpOnly: true,
}}),
)
// 2. Cookie 字符串(从浏览器 DevTools 直接复制)
result, err = client.Get("https://httpbin.org/cookies",
httpc.WithCookieString("cookie1=value1; cookie2=value2"),
)
// 3. Cookie map
result, err = client.Get("https://httpbin.org/cookies",
httpc.WithCookieMap(map[string]string{
"theme": "dark",
"lang": "en",
}),
)
// 4. 多种方式组合使用
result, err = client.Get("https://httpbin.org/cookies",
httpc.WithCookieString("session=abc123"),
httpc.WithCookies([]http.Cookie{{Name: "manual", Value: "cookie"}}),
)读取响应 Cookie 与自动管理
result, _ := client.Get("https://httpbin.org/response-headers?Set-Cookie=session=abc123")
fmt.Println(len(result.Response.Cookies)) // 输出示例:1(响应携带的 Cookie 数)
if c := result.GetCookie("session"); c != nil { // 按名精确查找
fmt.Println(c.Value) // 输出:abc123
}
fmt.Println(result.HasCookie("nonexistent")) // 输出:false
// 跨请求自动管理:开启 Cookie Jar 后,响应 Cookie 自动存储并在后续请求中回带
cfg := httpc.DefaultConfig()
cfg.Connection.EnableCookies = true
jarClient, _ := httpc.New(cfg)
defer jarClient.Close()
_, _ = jarClient.Get("https://httpbin.org/cookies/set?session=xyz789") // Cookie 存入 Jar
resp, _ := jarClient.Get("https://httpbin.org/cookies") // 自动回带
fmt.Println(resp.StatusCode()) // 输出:200会话级的 Cookie 与安全校验(WithSecureCookie + StrictCookieSecurityConfig)详见域名客户端与会话。
SessionManager 独立会话
会话(持久头 + Cookie)不绑定域名客户端,可以独立创建、独立注入任意请求。适合「同一服务多套身份」或「手工编排请求选项」的场景:
package main
import (
"fmt"
"log"
"net/http"
"github.com/cybergodev/httpc"
)
func main() {
session, err := httpc.NewSessionManagerDefault()
if err != nil {
log.Fatal(err)
}
// 持久头:应用到走这个会话的所有请求
if err := session.SetHeader("Authorization", "Bearer my-token"); err != nil {
log.Fatal(err)
}
if err := session.SetHeaders(map[string]string{
"X-API-Version": "v2",
"X-Client-ID": "session-demo",
}); err != nil {
log.Fatal(err)
}
// 持久 Cookie
if err := session.SetCookies([]*http.Cookie{
{Name: "session_id", Value: "abc123"},
{Name: "preferences", Value: "theme_dark"},
}); err != nil {
log.Fatal(err)
}
fmt.Println("会话头:", len(session.GetHeaders())) // 输出:会话头:3
fmt.Println("会话 Cookie:", len(session.GetCookies())) // 输出:会话 Cookie: 2
if c := session.GetCookie("session_id"); c != nil {
fmt.Printf("找到 Cookie: %s = %s\n", c.Name, c.Value) // 输出:找到 Cookie: session_id = abc123
}
// 选择性删除与整体清空
session.DeleteHeader("X-API-Version")
session.DeleteCookie("preferences")
fmt.Println("删除后头/Cookie:", len(session.GetHeaders()), "/", len(session.GetCookies()))
// 输出:删除后头/Cookie: 2 / 1
session.ClearHeaders()
session.ClearCookies()
fmt.Println("清空后头/Cookie:", len(session.GetHeaders()), "/", len(session.GetCookies()))
// 输出:清空后头/Cookie: 0 / 0
}并发下载
package main
import (
"context"
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/cybergodev/httpc"
)
func main() {
urls := map[string]string{
"file1.zip": "https://example.com/files/file1.zip",
"file2.zip": "https://example.com/files/file2.zip",
"file3.zip": "https://example.com/files/file3.zip",
}
client, _ := httpc.NewDefault()
defer client.Close()
var successCount int64
var totalBytes int64
var wg sync.WaitGroup
for filename, url := range urls {
wg.Add(1)
go func(name, u string) {
defer wg.Done()
cfg := httpc.DefaultDownloadConfig()
cfg.FilePath = "/tmp/" + name
cfg.Overwrite = true
cfg.ProgressCallback = func(downloaded, total int64, speed float64) {
fmt.Printf("\r%s: %.1f%% (%s/s)", name,
float64(downloaded)/float64(total)*100,
float64(speed)/1024/1024)
}
result, err := client.Download(context.Background(), u, cfg)
if err != nil {
log.Printf("%s 下载失败: %v", name, err)
return
}
atomic.AddInt64(&successCount, 1)
atomic.AddInt64(&totalBytes, result.BytesWritten)
fmt.Printf("\n%s 完成: %d\n", name, result.BytesWritten)
}(filename, url)
}
wg.Wait()
fmt.Printf("\n下载完成:%d/%d, 总计 %d\n",
successCount, len(urls), totalBytes)
}并发请求:worker pool 与信号量
批量请求大量 URL 时,worker pool 把并发度钉在 worker 数上;信号量模式则允许任务动态伸缩但限制同时在途的上限。两者共享同一个 Client(HTTPC 的 Client 并发安全,连接池跨 goroutine 复用):
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"time"
"github.com/cybergodev/httpc"
)
func main() {
// 本地模拟服务器:每个请求耗时 20ms
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
cfg := httpc.DefaultConfig()
cfg.Security.AllowPrivateIPs = true // 允许 127.0.0.1 本地服务器
client, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
defer client.Close()
const (
numWorkers = 5
numJobs = 20
)
jobs := make(chan string, numJobs)
results := make(chan int, numJobs)
// 固定数量 worker 消费任务:并发度恒为 numWorkers
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for url := range jobs {
resp, err := client.Get(url)
if err != nil {
log.Printf("请求失败: %v", err)
results <- 0
continue
}
results <- resp.StatusCode()
}
}()
}
start := time.Now()
for i := 0; i < numJobs; i++ {
jobs <- fmt.Sprintf("%s/api/item/%d", server.URL, i)
}
close(jobs)
wg.Wait()
close(results)
var okCount int64
for status := range results {
if status >= 200 && status < 300 {
okCount++
}
}
fmt.Printf("worker pool:%d/%d 成功,耗时 %v(串行约需 %v)\n",
okCount, numJobs, time.Since(start), numJobs*20*time.Millisecond)
// 输出示例:worker pool:20/20 成功,耗时约 90ms(串行约需 400ms)
// 信号量模式:goroutine 数可以多,但在途请求最多 maxInFlight 个
const maxInFlight = 3
sem := make(chan struct{}, maxInFlight)
var wg2 sync.WaitGroup
var okCount2 int64
start = time.Now()
for i := 0; i < numJobs; i++ {
wg2.Add(1)
go func(id int) {
defer wg2.Done()
sem <- struct{}{}
defer func() { <-sem }()
resp, err := client.Get(fmt.Sprintf("%s/api/request/%d", server.URL, id))
if err != nil {
return
}
if resp.IsSuccess() {
atomic.AddInt64(&okCount2, 1)
}
}(i)
}
wg2.Wait()
fmt.Printf("信号量:%d/%d 成功,耗时 %v\n", okCount2, numJobs, time.Since(start))
// 输出示例:信号量:20/20 成功,耗时约 140ms
}并发度与连接池的配合
worker 数/信号量上限不要长期显著超过 Connection.MaxConnsPerHost(HTTP/1.1 场景),否则请求在传输层排队、总吞吐不再提升;HTTP/2(默认开启)下同一主机共享连接多路复用,受影响更小。详见性能优化。
结构化错误处理
ClientError 携带分类(Code()/Type)、重试性(IsRetryable())、请求上下文(URL/Method/Attempts/StatusCode),用 errors.As 提取后可以按类别走不同处理分支:
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
func main() {
client, err := httpc.NewDefault()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 1 纳秒超时:稳定触发超时错误用于演示
_, err = client.Get("https://httpbin.org/get",
httpc.WithTimeout(1*time.Nanosecond),
)
if err == nil {
log.Fatal("expected timeout error")
}
// errors.Is:哨兵错误判定(context 错误经 Unwrap 透传)
switch {
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("请求超时,考虑放宽超时或检查网络")
case errors.Is(err, context.Canceled):
fmt.Println("请求被取消")
}
// errors.As:提取结构化错误
var clientErr *httpc.ClientError
if errors.As(err, &clientErr) {
fmt.Println("Code:", clientErr.Code()) // 输出:Code: TIMEOUT
fmt.Println("Method:", clientErr.Method) // 输出:Method: GET
fmt.Println("Attempts:", clientErr.Attempts) // 输出:Attempts: 1
fmt.Println("Retryable:", clientErr.IsRetryable()) // 输出:Retryable: false
switch clientErr.Code() {
case "TIMEOUT":
fmt.Println("→ 分支:超时,可放宽预算后重试")
case "NETWORK_ERROR", "DNS_ERROR":
fmt.Println("→ 分支:网络/DNS 故障,检查连通性")
case "TLS_ERROR", "CERTIFICATE":
fmt.Println("→ 分支:证书问题,校验 CA 与系统时间")
case "RETRY_EXHAUSTED":
fmt.Println("→ 分支:重试耗尽,进入降级逻辑")
}
}
}HTTP 状态码错误(4xx/5xx 响应被归类为 HTTP_ERROR)同样携带 clientErr.StatusCode;Cause 字段保留底层错误供 errors.Unwrap 继续下钻。错误分类全表见错误处理与错误类型。
响应落盘:SaveToFile 与 Download
小响应体已在内存时 SaveToFile 一行落盘;大文件用 Download 流式写盘并支持进度/续传/校验:
package main
import (
"context"
"log"
"github.com/cybergodev/httpc"
)
func main() {
client, err := httpc.NewDefault()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 响应体已在内存:直接落盘(路径走与 Download 相同的安全校验)
result, err := client.Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
if err := result.SaveToFile("response.json"); err != nil {
log.Fatal(err)
}
// 大文件:Download 流式写盘 + SHA-256 校验(不匹配自动删文件)
cfg := httpc.DefaultDownloadConfig()
cfg.FilePath = "large-file.bin"
cfg.Overwrite = true
cfg.Checksum = "从发布清单等可信渠道获取的 SHA-256 hex"
if _, err := client.Download(
context.Background(), // 无需取消/超时时传 Background,勿传 nil
"https://example.com/large-file.bin",
cfg,
); err != nil {
log.Fatal(err)
}
log.Println("落盘完成")
}默认客户端管理
包级函数(httpc.Get 等)背后是一个懒初始化的共享默认客户端,可用 SetDefaultClient 换成自定义配置的实例(旧实例自动关闭),让全局调用统一走新配置:
package main
import (
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
func main() {
// 从默认配置出发,只改需要的字段
cfg := httpc.DefaultConfig()
cfg.Timeouts.Request = 5 * time.Second
cfg.Retry.MaxRetries = 0
customClient, err := httpc.New(cfg)
if err != nil {
log.Fatal(err)
}
// 设为默认客户端:此后包级函数全部走新配置(旧默认客户端自动关闭)
if err := httpc.SetDefaultClient(customClient); err != nil {
log.Fatal(err)
}
result, err := httpc.Get("https://httpbin.org/get") // 使用 5s 超时、0 重试
if err != nil {
log.Fatal(err)
}
fmt.Println(result.StatusCode()) // 输出:200
// 收尾:关闭默认客户端释放连接池
if err := httpc.CloseDefaultClient(); err != nil {
log.Fatal(err)
}
}适用场景:应用启动时统一定制全局行为、按环境(开发/生产)切换配置。注意 SetDefaultClient 只接受 httpc.New 创建的客户端,且不能传入已关闭的实例;替换会关闭旧默认客户端并中断其仍在途的请求(并发调用方可能观察到连接错误),应只在启动期或静默期调用,勿在活跃流量下替换。
自定义中间件:请求签名
package main
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"time"
"github.com/cybergodev/httpc"
)
func SigningMiddleware(secret string) httpc.MiddlewareFunc {
return func(next httpc.Handler) httpc.Handler {
return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
timestamp := time.Now().Unix()
message := fmt.Sprintf("%s%s%d", req.Method(), req.URL(), timestamp)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
signature := hex.EncodeToString(mac.Sum(nil))
req.SetHeader("X-Timestamp", fmt.Sprintf("%d", timestamp))
req.SetHeader("X-Signature", signature)
return next(ctx, req)
}
}
}
func main() {
cfg := httpc.DefaultConfig()
cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
httpc.RecoveryMiddleware(),
SigningMiddleware("my-secret-key"),
}
client, _ := httpc.New(cfg)
defer client.Close()
result, err := client.Get("https://api.example.com/protected")
if err != nil {
log.Fatal(err)
}
log.Println(result.StatusCode())
}