feat: Revamp HTTP Client: Multi-client support, Streaming & Optimizations
Description
This PR replaces the single-client singleton (previously initialized via sync.Once) with a Factory Pattern. This enables support for Multiple HTTP Clients while significantly refactoring the engine for performance and safety.
[!Note] We will need to update TestRequest to support testing these HTTP client changes. I’ll create a separate PR for that, as the current PR is already quite large.
Critical: Configuration Update (Breaking Change)
The config/http.go structure has changed to support multiple clients. You must update your configuration file.
Old Config (Single Client)
Previously, the configuration only allowed one global client under the "client" key.
// config/http.go
"client": map[string]any{
"base_url": config.Env("HTTP_CLIENT_BASE_URL"),
"timeout": config.Env("HTTP_CLIENT_TIMEOUT", 30),
"max_idle_conns": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS", 100),
"max_idle_conns_per_host": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST", 2),
"max_conns_per_host": config.Env("HTTP_CLIENT_MAX_CONN_PER_HOST", 10),
"idle_conn_timeout": config.Env("HTTP_CLIENT_IDLE_CONN_TIMEOUT", 90),
},
New Config (Multi-Client)
We replace the single "client" key with default_client and a clients map. This avoids conflicts with the HTTP Server configuration (which often uses keys like host or port at the root).
// config/http.go
// The default client to use when facades.Http() is called without a name.
"default_client": config.Env("HTTP_CLIENT_DEFAULT", "default"),
// specific client configurations
"clients": map[string]any{
"default": map[string]any{
"base_url": config.Env("HTTP_CLIENT_BASE_URL"),
"timeout": config.Env("HTTP_CLIENT_TIMEOUT", 30),
"max_idle_conns": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS", 100),
"max_idle_conns_per_host": config.Env("HTTP_CLIENT_MAX_IDLE_CONNS_PER_HOST", 2),
"max_conns_per_host": config.Env("HTTP_CLIENT_MAX_CONN_PER_HOST", 10),
"idle_conn_timeout": config.Env("HTTP_CLIENT_IDLE_CONN_TIMEOUT", 90),
},
// You can add more clients here (e.g., "stripe", "aws")
},
Key Features
1. Multiple Clients Support
Access specific API configurations on the fly using the new Factory.
facades.Http().Get("/users") // Uses "default" driver
facades.Http().Client("stripe").Get("/") // Uses "stripe" driver
2. Streaming (Low Memory Usage)
Introduced Stream() to handle large file downloads efficiently without loading the entire response into RAM.
resp, err := facades.Http().Get("https://example.com/huge-file.zip")
stream, _ := resp.Stream() // Stream directly to disk
defer stream.Close()
io.Copy(file, stream)
3. Fail-Fast & Lazy Loading
Replaces the strict initialization with a "Lazy Error" pattern. Missing configurations no longer panic at boot; they return a safe error only when a request is attempted.
Usage & Migration
Dependency Injection (Best Practice)
Inject specific client.Client instances into your services instead of the entire Factory.
// In your ServiceProvider:
// app.Bind("PaymentService", func() {
// return NewPaymentService(facades.Http().Client("stripe"))
// })
type PaymentService struct {
request client.Request
}
func (s *PaymentService) Charge() {
// Already configured with Stripe host/timeout
s.request.Post("/charges", data)
}
Response Parsing
We are moving to Response.Bind() for safer error handling.
Old Way (Deprecated):
facades.Http().Bind(&user).Get("/")
// Unsafe. Parsed before status check.
New Way (Recommended):
resp, err := facades.Http().Get("/")
assert(err)
if resp.Failed() {
panic()
}
resp.Bind(&user)
// Safe. Check `resp.Failed()` first.
Breaking Changes
-
Configuration:
config/http.gostructure must be updated. -
Interface (Mocks):
client.Responsenow includesStream()andBind(). If you mock this interface in tests, you must implement these methods.
✅ Checks
- [x] Added test cases for the Factory and Client isolation.
- [x] Verified thread safety with concurrent request tests.
- [ ] Updated documentation to reflect the new API.