zeroclaw / pkg /utils /http.go
personalbotai
feat: implement Telegram proxy support for Hugging Face Spaces
f5fca4e
package utils
import (
"context"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// NewHTTPClientWithGoogleDNS creates an HTTP client that forces DNS resolution via 8.8.8.8
func NewHTTPClientWithGoogleDNS() *http.Client {
dialer := &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 10 * time.Second,
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
},
}
transport := &http.Transport{
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return &http.Client{
Transport: transport,
}
}
// NewIPv4OnlyHTTPClient creates an HTTP client that forces IPv4 only (disables IPv6)
func NewIPv4OnlyHTTPClient() *http.Client {
dialer := &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
// Force IPv4 by dialing 8.8.8.8:53 (Google DNS)
d := net.Dialer{
Timeout: 10 * time.Second,
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
},
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
// Force IPv4 by replacing any IPv6 addresses with IPv4 lookup
// If address contains colon (IPv6), resolve hostname separately and pick IPv4
if strings.Contains(address, ":") {
// Extract host from "host:port" or "[ipv6]:port"
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
// Resolve host to IPv4 addresses only
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
// Find first IPv4 address
for _, addr := range addrs {
if addr.IP.To4() != nil {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(addr.IP.String(), port))
}
}
return nil, &net.DNSError{Err: "no IPv4 address found for " + host}
}
// Already IPv4 or hostname, use default dialer
return dialer.DialContext(ctx, "tcp", address)
},
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return &http.Client{
Transport: transport,
}
}
// NewProxiedHTTPClient creates an HTTP client that uses a proxy if configured
// It respects standard proxy environment variables and adds proxy support for HTTPS
func NewProxiedHTTPClient() *http.Client {
// Check for proxy configuration
proxyURL := os.Getenv("TELEGRAM_PROXY_URL")
if proxyURL == "" {
proxyURL = os.Getenv("HTTPS_PROXY")
if proxyURL == "" {
proxyURL = os.Getenv("HTTP_PROXY")
}
}
transport := &http.Transport{
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// If proxy is configured, set it up
if proxyURL != "" {
u, err := url.Parse(proxyURL)
if err != nil {
// If proxy URL is invalid, log and continue without proxy
// In production, we'd use a logger but this is a low-level utils function
// So we'll just proceed without proxy
} else {
transport.Proxy = http.ProxyURL(u)
}
}
return &http.Client{
Transport: transport,
}
}
// IsProxyOrNetworkError checks if an error indicates proxy or network policy block
func IsProxyOrNetworkError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// Common network/block indicators
indicators := []string{
"operation not permitted",
"permission denied",
"network is unreachable",
"connection refused",
"timeout",
"no route to host",
"i/o timeout",
"dial tcp",
"connect:",
"proxy",
}
for _, indicator := range indicators {
if strings.Contains(strings.ToLower(errStr), indicator) {
return true
}
}
return false
}