diff options
| author | Christian Cleberg <[email protected]> | 2026-07-14 20:23:47 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-14 20:23:47 -0500 |
| commit | 5756a37bc14bab81cef2e305c504a396995dcf18 (patch) | |
| tree | e8d99d6cc87a17e83f90be8c30ee3a73003f04be /app/httpclient.go | |
| parent | f4afe364c1885b55d3bd92d1aa2f00c4bb7c8ee2 (diff) | |
| parent | 369fd17e696699b9b1aa1ac8293e0122c7e3a41e (diff) | |
| download | skunky-art-5756a37bc14bab81cef2e305c504a396995dcf18.tar.gz skunky-art-5756a37bc14bab81cef2e305c504a396995dcf18.tar.bz2 skunky-art-5756a37bc14bab81cef2e305c504a396995dcf18.zip | |
Merge dev: devianter v0.3.1 migration, DA throttle, fixes
Migrate to github.com/zerolabsco/devianter v0.3.1 and adapt to its
reordered return values, throttle outbound DeviantArt requests, harden
the random-art retry loop, and fix the darwin build.
Diffstat (limited to 'app/httpclient.go')
| -rw-r--r-- | app/httpclient.go | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/app/httpclient.go b/app/httpclient.go new file mode 100644 index 0000000..d3e5d50 --- /dev/null +++ b/app/httpclient.go @@ -0,0 +1,68 @@ +package app + +import ( + "net/http" + "strings" + "sync" + "time" +) + +// DeviantArt fronts its API with AWS CloudFront + WAF, which bans egress IPs that +// hit it too hard. Under a bot flood, unbounded concurrent handlers each fetch +// ~150-200 KB of DA JSON, which both hammers that IP (risking a ban) and can OOM +// the process. devianter makes its requests with a bare &http.Client{}, so they go +// through http.DefaultTransport — we wrap it here to bound the rate and concurrency +// of calls to deviantart.com and to add timeouts. Requests to other hosts (e.g. +// wixmp image CDN) are passed straight through, so media stays fast. +// +// http.ProxyFromEnvironment is preserved, so HTTPS_PROXY (VPN egress) still applies. + +// Tunables (kept in source; safe defaults). Lower is gentler on the DA IP. +var ( + daMinInterval = 400 * time.Millisecond // minimum gap between DA request starts + daMaxConcurrent = 2 // max simultaneous in-flight DA requests +) + +type daThrottle struct { + base http.RoundTripper + sem chan struct{} + mu sync.Mutex + last time.Time +} + +func (t *daThrottle) RoundTrip(req *http.Request) (*http.Response, error) { + // Only throttle DeviantArt's WAF-protected API host; let everything else fly. + if !strings.Contains(req.URL.Hostname(), "deviantart.com") { + return t.base.RoundTrip(req) + } + + // Concurrency cap: block until a slot frees up (backpressure under floods). + t.sem <- struct{}{} + defer func() { <-t.sem }() + + // Rate cap: enforce a minimum interval between request starts. + t.mu.Lock() + if wait := daMinInterval - time.Since(t.last); wait > 0 { + time.Sleep(wait) + } + t.last = time.Now() + t.mu.Unlock() + + return t.base.RoundTrip(req) +} + +// InstallDAThrottle wraps http.DefaultTransport with the rate/concurrency limits and +// timeouts above. Call once at startup, before any DeviantArt request is made. +func InstallDAThrottle() { + // Clone the default transport so we keep its Proxy (ProxyFromEnvironment) and + // connection-pool defaults, then tighten timeouts to bound hung connections. + base := http.DefaultTransport.(*http.Transport).Clone() + base.TLSHandshakeTimeout = 10 * time.Second + base.ResponseHeaderTimeout = 20 * time.Second + base.ExpectContinueTimeout = 2 * time.Second + + http.DefaultTransport = &daThrottle{ + base: base, + sem: make(chan struct{}, daMaxConcurrent), + } +} |
