mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 10:46:51 +03:00
ratelimit
This commit is contained in:
@@ -17,6 +17,20 @@ import (
|
||||
"github.com/sartoopjj/thefeed/internal/protocol"
|
||||
)
|
||||
|
||||
// ghRateLimitError is returned by fetchGitHubRaw when GitHub rejects
|
||||
// with 403 + X-RateLimit-Remaining: 0. Surfacing it as a typed error
|
||||
// lets the caller render a specific 429 to the browser so the UI can
|
||||
// show a "rate limited, try again at HH:MM" popup instead of a generic
|
||||
// relay failure.
|
||||
type ghRateLimitError struct {
|
||||
ResetUnix int64
|
||||
Body string
|
||||
}
|
||||
|
||||
func (e *ghRateLimitError) Error() string {
|
||||
return fmt.Sprintf("github rate limit (reset=%d): %s", e.ResetUnix, e.Body)
|
||||
}
|
||||
|
||||
// relayHTTPClient is the single shared HTTP client for the GitHub relay
|
||||
// path. Reusing one client (and its underlying *http.Transport) gives us
|
||||
// connection pooling and DNS-result caching for free across the many
|
||||
@@ -161,7 +175,23 @@ func (s *Server) serveFromGitHubRelay(w http.ResponseWriter, r *http.Request, si
|
||||
encBody, _, err := fetchGitHubRaw(ctx, relayHTTPClient, url, size+int64(aeadOverhead))
|
||||
if err != nil {
|
||||
s.addLog(fmt.Sprintf("relay: fetch %s: %v", url, err))
|
||||
|
||||
// Rate-limit gets surfaced specifically so the UI can show a
|
||||
// "try again at HH:MM" popup. Other errors fall through to the
|
||||
// caller's 502 → existing slow-DNS fallback prompt.
|
||||
var rl *ghRateLimitError
|
||||
if errors.As(err, &rl) {
|
||||
minutes := int64(1)
|
||||
if rl.ResetUnix > 0 {
|
||||
secs := rl.ResetUnix - time.Now().Unix()
|
||||
if secs > 0 {
|
||||
minutes = (secs + 59) / 60
|
||||
}
|
||||
}
|
||||
w.Header().Set("X-Relay-Reset", strconv.FormatInt(rl.ResetUnix, 10))
|
||||
w.Header().Set("X-Relay-Reset-Min", strconv.FormatInt(minutes, 10))
|
||||
http.Error(w, "github rate limit", http.StatusTooManyRequests)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
body, err := protocol.DecryptRelayBlob(relayKey, encBody)
|
||||
@@ -205,13 +235,20 @@ func fetchGitHubRaw(ctx context.Context, hc *http.Client, url string, expectedSi
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
// Surface enough context to tell rate-limit, abuse-detection,
|
||||
// and plain auth errors apart. GitHub puts a JSON message in
|
||||
// the body and rate-limit counters in the response headers.
|
||||
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||||
rl := resp.Header.Get("X-RateLimit-Remaining")
|
||||
reset := resp.Header.Get("X-RateLimit-Reset")
|
||||
retry := resp.Header.Get("Retry-After")
|
||||
// Distinct typed error for the rate-limit case so the caller
|
||||
// can render a 429 with the reset time instead of a generic
|
||||
// 502 / fallback error.
|
||||
if resp.StatusCode == http.StatusForbidden && rl == "0" {
|
||||
resetUnix, _ := strconv.ParseInt(reset, 10, 64)
|
||||
return nil, "", &ghRateLimitError{
|
||||
ResetUnix: resetUnix,
|
||||
Body: strings.TrimSpace(string(errBody)),
|
||||
}
|
||||
}
|
||||
hdr := ""
|
||||
if rl != "" {
|
||||
hdr += " rl-remaining=" + rl
|
||||
|
||||
@@ -2593,6 +2593,8 @@
|
||||
auto_update_off: 'بهروزرسانی خودکار برای @{name} خاموش شد',
|
||||
media_relay_fallback: 'دانلود از مسیر سریع نشد. از مسیر کند (DNS) امتحان کنیم؟ توجه: ممکن است خیلی کند باشد.',
|
||||
media_slow_only: 'رله گیتهاب در دسترس نیست. دانلود از مسیر DNS خیلی کند است. ادامه میدهی؟',
|
||||
media_rate_limited: 'به محدودیت تعداد درخواست گیتهاب رسیدی. حدود {n} دقیقه دیگه دوباره فعال میشه. حالا از مسیر کند (DNS) دانلود کنیم؟',
|
||||
media_rate_limited_short: 'محدودیت تعداد درخواست گیتهاب',
|
||||
media_size_mismatch: 'سایز فایل با چیزی که سرور گفته بود نمیخونه',
|
||||
clear_cache: 'پاک کردن کش', clear_cache_btn: '🗑 پاک کردن کش', cache_cleared: 'کش پاک شد!',
|
||||
saved_resolvers_title: 'شروع سریع',
|
||||
@@ -2760,6 +2762,8 @@
|
||||
auto_update_off: 'Auto-update is now OFF for @{name}',
|
||||
media_relay_fallback: 'Fast relay failed. Try the slow DNS path? Note: this can be very slow.',
|
||||
media_slow_only: 'GitHub relay is unavailable for this file. The DNS path is very slow. Download anyway?',
|
||||
media_rate_limited: 'GitHub rate limit hit. Resets in about {n} min. Try the slow DNS path now?',
|
||||
media_rate_limited_short: 'GitHub rate limit',
|
||||
media_size_mismatch: 'Downloaded size doesn\'t match the manifest',
|
||||
clear_cache: 'Clear Cache', clear_cache_btn: '🗑 Clear Cache', cache_cleared: 'Cache cleared!',
|
||||
saved_resolvers_title: 'Quick Start',
|
||||
@@ -4509,10 +4513,29 @@
|
||||
mediaShowBlob(card, blobURL);
|
||||
mediaPersistBlob(msgID, card, xhr.response, mediaBlobs[msgID].mime);
|
||||
} else {
|
||||
// GitHub rate limit gets its own popup with the reset time so
|
||||
// the user knows it's temporary and how long to wait.
|
||||
if (source === 'fast' && xhr.status === 429) {
|
||||
var resetMin = parseInt(xhr.getResponseHeader('X-Relay-Reset-Min') || '0', 10);
|
||||
await handleRateLimit(resetMin);
|
||||
return;
|
||||
}
|
||||
if (source === 'fast') { handleFastFailure(xhr.statusText || ('HTTP ' + xhr.status)); return; }
|
||||
mediaShowError(card, xhr.statusText || ('HTTP ' + xhr.status));
|
||||
}
|
||||
}
|
||||
async function handleRateLimit(resetMin) {
|
||||
var minutes = (resetMin && resetMin > 0) ? resetMin : '?';
|
||||
var msg = (t('media_rate_limited') || 'GitHub rate limit hit. Reset in {n} min. Try the slow DNS path?').replace('{n}', minutes);
|
||||
if (dnsAvail) {
|
||||
var ok = await showConfirmDialog(msg, t('yes') || 'Yes', t('no') || 'No');
|
||||
if (ok) { restartWith('slow'); return; }
|
||||
} else {
|
||||
await showInfoDialog(msg, t('ok') || 'OK');
|
||||
}
|
||||
mediaShowError(card, t('media_rate_limited_short') || 'Rate limited');
|
||||
finishSlot();
|
||||
}
|
||||
function finishSlot() {
|
||||
mediaActiveCount = Math.max(0, mediaActiveCount - 1);
|
||||
mediaPumpQueue();
|
||||
|
||||
Reference in New Issue
Block a user