Resty
Simple HTTP and REST client for Go. Maintains performance through internal use of net/http while providing more user-friendly API. Built-in automatic JSON/XML parsing, OAuth/Bearer authentication, retry functionality, debugging, and middleware support.
GitHub Overview
go-resty/resty
Simple HTTP, REST, and SSE client library for Go
Topics
Star History
Library
Resty
Overview
Resty is a "Simple HTTP and REST client library for Go" that has been widely adopted as a native HTTP client in the Go ecosystem, designed as a high-level HTTP library that emphasizes "balance between simplicity and rich functionality." Built as a powerful abstraction on top of the net/http package, it provides chainable APIs, automatic retry functionality, middleware systems, diverse authentication options, and automatic JSON/XML processing. It enables intuitive and efficient HTTP communication for Go developers and is utilized across a wide range of scenarios from REST API consumption to web service development.
Details
Resty 2025 edition (v3 series) continues to evolve as a mature solution for Go HTTP clients. Designed around three main components - Client, Request, and Response - it comprehensively manages the entire HTTP request lifecycle. With thread-safe implementation (using sync.RWMutex) ensuring safe usage from multiple goroutines, it provides enterprise-grade HTTP communication requirements including a rich middleware system, automatic retry functionality, OAuth/Basic/JWT authentication, Server-Sent Events (SSE) support, tracing functionality, and custom DNS resolvers. Compared to net/http, Resty offers improved development efficiency through high-level APIs and rich built-in functionality.
Key Features
- Chainable API: Intuitive and fluent interface for concise code writing
- Powerful Middleware System: Customization of request/response processing
- Automatic Retry Functionality: Configurable retry conditions and backoff strategies
- Comprehensive Authentication Support: Built-in support for Basic, Bearer, Digest, and OAuth authentication
- Automatic JSON/XML Processing: Automatic serialization/deserialization of structs
- Thread-Safe Design: Safe usage in concurrent processing environments
Pros and Cons
Pros
- Significantly more concise and readable code writing compared to net/http
- High development efficiency and productivity improvement through rich built-in functionality
- High customizability through powerful middleware system
- Robustness through automatic retry and error handling
- Enterprise-level support through comprehensive authentication options
- Safety in concurrent processing through thread-safe design
Cons
- Slightly higher learning cost and abstraction level compared to net/http
- Minor increase in binary size due to additional external dependencies
- Low-level control may be limited for very specific HTTP requirements
- May be overkill when net/http is sufficient for simple use cases
- Migration cost due to API changes during version upgrades
- May require understanding of internal implementation during debugging
Reference Pages
Code Examples
Installation and Basic Setup
// Add to go.mod
require resty.dev/v3
// Or install with Go 1.17+
go get resty.dev/v3
package main
import (
"fmt"
"resty.dev/v3"
)
// Basic client initialization
func main() {
// Create client (always use defer Close for resource management)
client := resty.New()
defer client.Close()
// Basic configuration
client.SetBaseURL("https://api.example.com").
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(1 * time.Second).
SetHeader("User-Agent", "MyApp/1.0").
SetHeader("Accept", "application/json")
}
// Advanced client configuration
func createAdvancedClient() *resty.Client {
client := resty.New()
// Enable debug mode
client.SetDebug(true)
// Proxy configuration
client.SetProxy("http://proxy.example.com:8080")
// SSL/TLS configuration
client.SetRootCertificate("/path/to/root/cert.pem").
SetClientCertificates("/path/to/client.crt", "/path/to/client.key")
// Redirect configuration
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(5))
return client
}
Basic Requests (GET/POST/PUT/DELETE)
package main
import (
"fmt"
"resty.dev/v3"
"time"
)
// User data structures
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
UserID int `json:"user_id"`
Message string `json:"message"`
}
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Details string `json:"details"`
}
func main() {
client := resty.New()
defer client.Close()
client.SetBaseURL("https://api.example.com")
// GET request (simple)
resp, err := client.R().Get("users")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Status: %s\n", resp.Status())
fmt.Printf("Body: %s\n", resp.String())
// GET request with structured response
var users []User
resp, err = client.R().
SetResult(&users).
Get("users")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if resp.IsSuccess() {
fmt.Printf("Users retrieved: %d items\n", len(users))
for _, user := range users {
fmt.Printf("User: %s (%s)\n", user.Name, user.Email)
}
}
// GET request with query parameters
var paginatedUsers []User
resp, err = client.R().
SetQueryParams(map[string]string{
"page": "1",
"limit": "10",
"sort": "created_at",
"order": "desc",
}).
SetResult(&paginatedUsers).
Get("users")
// POST request (login example)
loginReq := LoginRequest{
Username: "testuser",
Password: "secret123",
}
var loginResp LoginResponse
var errResp ErrorResponse
resp, err = client.R().
SetBody(loginReq).
SetResult(&loginResp).
SetError(&errResp).
Post("https://api.example.com/login")
if err != nil {
fmt.Printf("Network error: %v\n", err)
return
}
if resp.IsSuccess() {
fmt.Printf("Login successful: %s\n", loginResp.Token)
fmt.Printf("User ID: %d\n", loginResp.UserID)
} else {
fmt.Printf("Login failed: %s (Code: %d)\n", errResp.Error, errResp.Code)
}
// POST request (create user)
newUser := User{
Name: "John Doe",
Email: "[email protected]",
Age: 30,
}
var createdUser User
resp, err = client.R().
SetHeader("Authorization", "Bearer "+loginResp.Token).
SetBody(newUser).
SetResult(&createdUser).
SetError(&errResp).
Post("users")
if resp.IsSuccess() {
fmt.Printf("User created: ID=%d, Name=%s\n", createdUser.ID, createdUser.Name)
}
// PUT request (update user)
updatedData := User{
ID: createdUser.ID,
Name: "John Smith",
Email: "[email protected]",
Age: 31,
}
resp, err = client.R().
SetHeader("Authorization", "Bearer "+loginResp.Token).
SetBody(updatedData).
SetResult(&createdUser).
Put(fmt.Sprintf("users/%d", createdUser.ID))
// DELETE request
resp, err = client.R().
SetHeader("Authorization", "Bearer "+loginResp.Token).
Delete(fmt.Sprintf("users/%d", createdUser.ID))
if resp.IsSuccess() {
fmt.Println("User deleted successfully")
}
}
// Form data and file operations
func formAndFileExamples() {
client := resty.New()
defer client.Close()
// Form data submission
resp, err := client.R().
SetFormData(map[string]string{
"username": "testuser",
"email": "[email protected]",
"category": "premium",
}).
Post("https://api.example.com/register")
// File upload
resp, err = client.R().
SetFile("document", "/path/to/document.pdf").
SetFormData(map[string]string{
"description": "Important document",
"category": "legal",
}).
SetHeader("Authorization", "Bearer your-token").
Post("https://api.example.com/upload")
// Multiple file upload
resp, err = client.R().
SetFiles(map[string]string{
"document": "/path/to/document.pdf",
"image": "/path/to/image.jpg",
"data": "/path/to/data.json",
}).
SetFormData(map[string]string{
"title": "Multiple files",
"description": "Batch upload",
}).
Post("https://api.example.com/upload/batch")
if err != nil {
fmt.Printf("Upload error: %v\n", err)
return
}
if resp.IsSuccess() {
fmt.Println("Files uploaded successfully")
}
}
Authentication and Security Configuration
package main
import (
"fmt"
"resty.dev/v3"
"crypto/tls"
)
// Basic authentication
func basicAuthExample() {
client := resty.New()
defer client.Close()
// Client-wide Basic authentication
client.SetBasicAuth("username", "password")
resp, err := client.R().Get("https://api.example.com/protected")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Response: %s\n", resp.String())
// Per-request Basic authentication
resp, err = client.R().
SetBasicAuth("user", "pass").
Get("https://api.example.com/secure")
}
// Bearer Token authentication
func bearerTokenExample() {
client := resty.New()
defer client.Close()
// Client-wide Bearer token
client.SetAuthToken("your-jwt-token")
resp, err := client.R().Get("https://api.example.com/user/profile")
// Per-request Bearer token
resp, err = client.R().
SetAuthToken("specific-token").
Get("https://api.example.com/admin/data")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
}
// Custom authentication scheme
func customAuthExample() {
client := resty.New()
defer client.Close()
// API Key authentication
client.SetHeader("X-API-Key", "your-api-key")
// Custom authentication header
resp, err := client.R().
SetHeader("X-Custom-Auth", "custom-token-value").
SetHeader("X-Client-ID", "your-client-id").
Get("https://api.example.com/data")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
}
// OAuth 2.0 implementation example
type OAuthTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
func oauth2Example() {
client := resty.New()
defer client.Close()
// OAuth 2.0 token acquisition
var tokenResp OAuthTokenResponse
resp, err := client.R().
SetBasicAuth("client_id", "client_secret").
SetFormData(map[string]string{
"grant_type": "client_credentials",
"scope": "read write",
}).
SetResult(&tokenResp).
Post("https://auth.example.com/oauth2/token")
if err != nil || !resp.IsSuccess() {
fmt.Printf("OAuth token acquisition failed: %v\n", err)
return
}
// Use acquired token
accessToken := tokenResp.AccessToken
var userData interface{}
resp, err = client.R().
SetAuthToken(accessToken).
SetResult(&userData).
Get("https://api.example.com/user/profile")
if resp.IsSuccess() {
fmt.Printf("User data: %+v\n", userData)
}
}
// TLS/SSL detailed configuration
func tlsConfigExample() {
client := resty.New()
defer client.Close()
// Custom TLS configuration
client.SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: false, // Enable certificate verification
MinVersion: tls.VersionTLS12, // Minimum TLS version
MaxVersion: tls.VersionTLS13, // Maximum TLS version
ServerName: "api.example.com", // Server name verification
})
// Client certificate configuration
client.SetClientRootCertificate("/path/to/root/ca.crt")
client.SetClientCertificates("/path/to/client.crt", "/path/to/client.key")
// Disable certificate verification (for development only)
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
resp, err := client.R().Get("https://secure-api.example.com/data")
if err != nil {
fmt.Printf("TLS connection error: %v\n", err)
return
}
fmt.Printf("Secure response: %s\n", resp.String())
}
Middleware System and Advanced Features
package main
import (
"fmt"
"time"
"resty.dev/v3"
)
// Request middleware
func requestMiddlewareExample() {
client := resty.New()
defer client.Close()
// Request logging middleware
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
fmt.Printf("Request: %s %s\n", req.Method, req.URL)
fmt.Printf("Headers: %+v\n", req.Header)
return nil
})
// Request timestamp middleware
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
req.SetHeader("X-Request-Time", time.Now().Format(time.RFC3339))
req.SetHeader("X-Request-ID", generateRequestID())
return nil
})
// Response middleware
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
fmt.Printf("Response: %s (%s)\n", resp.Status(), resp.Time())
fmt.Printf("Response size: %d bytes\n", len(resp.Body()))
return nil
})
// Error handling middleware
client.OnError(func(req *resty.Request, err error) {
fmt.Printf("Request error: %s %s - %v\n", req.Method, req.URL, err)
})
resp, err := client.R().Get("https://api.example.com/data")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
// Authentication middleware
func authMiddlewareExample() {
client := resty.New()
defer client.Close()
var currentToken string
// Automatic token attachment middleware
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
if currentToken != "" {
req.SetAuthToken(currentToken)
}
return nil
})
// Token refresh middleware
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
if resp.StatusCode() == 401 {
fmt.Println("Token expired, refreshing...")
// Token refresh logic
newToken, err := refreshAuthToken(c)
if err != nil {
return err
}
currentToken = newToken
fmt.Println("Token refreshed successfully")
}
return nil
})
resp, err := client.R().Get("https://api.example.com/protected/data")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
// Rate limiting middleware
func rateLimitingExample() {
client := resty.New()
defer client.Close()
// Rate limiting middleware
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
if resp.StatusCode() == 429 { // Too Many Requests
retryAfter := resp.Header().Get("Retry-After")
if retryAfter != "" {
fmt.Printf("Rate limited, waiting %s seconds\n", retryAfter)
// Implement wait logic
time.Sleep(5 * time.Second)
}
}
return nil
})
// Request counter middleware
requestCount := 0
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
requestCount++
req.SetHeader("X-Request-Count", fmt.Sprintf("%d", requestCount))
return nil
})
}
// Utility functions
func generateRequestID() string {
return fmt.Sprintf("req-%d", time.Now().UnixNano())
}
func refreshAuthToken(client *resty.Client) (string, error) {
var tokenResp struct {
Token string `json:"access_token"`
}
resp, err := client.R().
SetBasicAuth("client_id", "client_secret").
SetFormData(map[string]string{
"grant_type": "refresh_token",
"refresh_token": "stored_refresh_token",
}).
SetResult(&tokenResp).
Post("https://auth.example.com/token")
if err != nil || !resp.IsSuccess() {
return "", fmt.Errorf("token refresh failed: %v", err)
}
return tokenResp.Token, nil
}
Error Handling and Retry Functionality
package main
import (
"fmt"
"time"
"resty.dev/v3"
)
// Comprehensive error handling
func errorHandlingExample() {
client := resty.New()
defer client.Close()
// Global retry configuration
client.SetRetryCount(3).
SetRetryWaitTime(1 * time.Second).
SetRetryMaxWaitTime(5 * time.Second).
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
// Custom backoff strategy
return time.Duration(resp.Request.Attempt) * time.Second, nil
})
// Retry condition configuration
client.AddRetryCondition(func(r *resty.Response, err error) bool {
// Retry on network errors
if err != nil {
return true
}
// Retry on server errors
if r.StatusCode() >= 500 {
return true
}
// Retry on rate limiting
if r.StatusCode() == 429 {
return true
}
return false
})
var result interface{}
var errorResp struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
}
resp, err := client.R().
SetResult(&result).
SetError(&errorResp).
Get("https://api.example.com/unreliable-endpoint")
if err != nil {
fmt.Printf("Network error: %v\n", err)
return
}
switch resp.StatusCode() {
case 200:
fmt.Printf("Success: %+v\n", result)
case 400:
fmt.Printf("Bad request: %s\n", errorResp.Error)
case 401:
fmt.Printf("Unauthorized: %s\n", errorResp.Message)
case 403:
fmt.Printf("Forbidden: %s\n", errorResp.Message)
case 404:
fmt.Printf("Not found: %s\n", errorResp.Error)
case 429:
fmt.Printf("Rate limited: %s\n", errorResp.Message)
case 500:
fmt.Printf("Server error: %s\n", errorResp.Error)
default:
fmt.Printf("Unexpected status: %d - %s\n", resp.StatusCode(), resp.String())
}
}
// Custom error handling with detailed information
type APIError struct {
StatusCode int `json:"status_code"`
Error string `json:"error"`
Message string `json:"message"`
Details string `json:"details"`
RequestID string `json:"request_id"`
}
func (e APIError) String() string {
return fmt.Sprintf("API Error %d: %s - %s (Request ID: %s)",
e.StatusCode, e.Error, e.Message, e.RequestID)
}
func advancedErrorHandling() {
client := resty.New()
defer client.Close()
client.SetBaseURL("https://api.example.com")
// Error response processing middleware
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
if !resp.IsSuccess() {
var apiError APIError
if err := resp.Unmarshal(&apiError); err == nil {
return fmt.Errorf("API error: %s", apiError.String())
}
return fmt.Errorf("HTTP error: %d - %s", resp.StatusCode(), resp.String())
}
return nil
})
var users []User
resp, err := client.R().
SetResult(&users).
Get("users")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
fmt.Printf("Retrieved %d users\n", len(users))
}
// Circuit breaker implementation
type CircuitBreaker struct {
failureCount int
threshold int
timeout time.Duration
lastFailure time.Time
state string // "closed", "open", "half-open"
}
func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
threshold: threshold,
timeout: timeout,
state: "closed",
}
}
func (cb *CircuitBreaker) Call(fn func() (*resty.Response, error)) (*resty.Response, error) {
if cb.state == "open" {
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = "half-open"
fmt.Println("Circuit breaker: half-open state")
} else {
return nil, fmt.Errorf("circuit breaker is open")
}
}
resp, err := fn()
if err != nil || (resp != nil && resp.StatusCode() >= 500) {
cb.failureCount++
cb.lastFailure = time.Now()
if cb.failureCount >= cb.threshold {
cb.state = "open"
fmt.Printf("Circuit breaker opened after %d failures\n", cb.failureCount)
}
return resp, err
}
// Success - reset circuit breaker
cb.failureCount = 0
cb.state = "closed"
return resp, nil
}
func circuitBreakerExample() {
client := resty.New()
defer client.Close()
cb := NewCircuitBreaker(3, 30*time.Second)
for i := 0; i < 10; i++ {
resp, err := cb.Call(func() (*resty.Response, error) {
return client.R().Get("https://api.example.com/unreliable")
})
if err != nil {
fmt.Printf("Attempt %d failed: %v\n", i+1, err)
} else {
fmt.Printf("Attempt %d succeeded: %s\n", i+1, resp.Status())
}
time.Sleep(1 * time.Second)
}
}
Performance Optimization and Concurrent Processing
package main
import (
"fmt"
"sync"
"context"
"time"
"resty.dev/v3"
)
// Concurrent request processing
func concurrentRequests() {
client := resty.New()
defer client.Close()
client.SetBaseURL("https://api.example.com")
urls := []string{
"users",
"posts",
"comments",
"categories",
"tags",
}
// Using sync.WaitGroup for concurrent processing
var wg sync.WaitGroup
results := make(chan string, len(urls))
for _, url := range urls {
wg.Add(1)
go func(endpoint string) {
defer wg.Done()
resp, err := client.R().Get(endpoint)
if err != nil {
results <- fmt.Sprintf("Error %s: %v", endpoint, err)
return
}
results <- fmt.Sprintf("Success %s: %d bytes", endpoint, len(resp.Body()))
}(url)
}
// Wait for all requests to complete
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Println(result)
}
}
// Controlled concurrent processing with semaphore
func controlledConcurrency() {
client := resty.New()
defer client.Close()
client.SetBaseURL("https://api.example.com")
userIDs := make([]int, 50)
for i := range userIDs {
userIDs[i] = i + 1
}
// Limit concurrent requests to 5
semaphore := make(chan struct{}, 5)
var wg sync.WaitGroup
for _, userID := range userIDs {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Acquire semaphore
semaphore <- struct{}{}
defer func() { <-semaphore }()
var user User
resp, err := client.R().
SetResult(&user).
Get(fmt.Sprintf("users/%d", id))
if err != nil {
fmt.Printf("Error getting user %d: %v\n", id, err)
return
}
if resp.IsSuccess() {
fmt.Printf("User %d: %s\n", id, user.Name)
}
}(userID)
}
wg.Wait()
fmt.Println("All requests completed")
}
// Batch processing with rate limiting
func batchProcessing() {
client := resty.New()
defer client.Close()
client.SetBaseURL("https://api.example.com")
userIDs := make([]int, 100)
for i := range userIDs {
userIDs[i] = i + 1
}
batchSize := 10
delayBetweenBatches := 1 * time.Second
for i := 0; i < len(userIDs); i += batchSize {
end := i + batchSize
if end > len(userIDs) {
end = len(userIDs)
}
batch := userIDs[i:end]
processBatch(client, batch)
// Wait between batches to avoid rate limiting
if end < len(userIDs) {
time.Sleep(delayBetweenBatches)
}
fmt.Printf("Batch %d completed: processed %d users\n", i/batchSize+1, len(batch))
}
}
func processBatch(client *resty.Client, userIDs []int) {
var wg sync.WaitGroup
for _, userID := range userIDs {
wg.Add(1)
go func(id int) {
defer wg.Done()
var user User
resp, err := client.R().
SetResult(&user).
Get(fmt.Sprintf("users/%d", id))
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if resp.IsSuccess() {
fmt.Printf("Processed user %d: %s\n", id, user.Name)
}
}(userID)
}
wg.Wait()
}
// Context-based timeout control
func contextTimeoutExample() {
client := resty.New()
defer client.Close()
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var result interface{}
resp, err := client.R().
SetContext(ctx).
SetResult(&result).
Get("https://api.example.com/slow-endpoint")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Request timed out")
} else {
fmt.Printf("Request error: %v\n", err)
}
return
}
fmt.Printf("Request completed: %s\n", resp.Status())
}
// Connection pool optimization
func optimizedClientConfiguration() *resty.Client {
client := resty.New()
// Connection pool settings
client.SetTimeout(30 * time.Second)
client.SetTransport(&http.Transport{
MaxIdleConns: 100, // Maximum idle connections
MaxIdleConnsPerHost: 20, // Maximum idle connections per host
IdleConnTimeout: 90 * time.Second, // Idle connection timeout
TLSHandshakeTimeout: 10 * time.Second, // TLS handshake timeout
DisableCompression: false, // Enable compression
ForceAttemptHTTP2: true, // Prefer HTTP/2
})
// Keep-alive settings
client.SetHeader("Connection", "keep-alive")
return client
}