summaryrefslogtreecommitdiff
path: root/app/httpclient.go
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-14 20:23:47 -0500
committerChristian Cleberg <[email protected]>2026-07-14 20:23:47 -0500
commit5756a37bc14bab81cef2e305c504a396995dcf18 (patch)
treee8d99d6cc87a17e83f90be8c30ee3a73003f04be /app/httpclient.go
parentf4afe364c1885b55d3bd92d1aa2f00c4bb7c8ee2 (diff)
parent369fd17e696699b9b1aa1ac8293e0122c7e3a41e (diff)
downloadskunky-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.go68
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),
+ }
+}