mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-17 17:34:09 +02:00
rarbg: implement retry strategy with 429 response (#14000)
This commit is contained in:
27
src/Jackett.Common/Exceptions/TooManyRequestsException.cs
Normal file
27
src/Jackett.Common/Exceptions/TooManyRequestsException.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Jackett.Common.Utils.Clients;
|
||||||
|
|
||||||
|
namespace Jackett.Common.Exceptions
|
||||||
|
{
|
||||||
|
public class TooManyRequestsException : Exception
|
||||||
|
{
|
||||||
|
public TimeSpan RetryAfter { get; private set; }
|
||||||
|
|
||||||
|
public TooManyRequestsException(string message, TimeSpan retryWait)
|
||||||
|
: base(message) => RetryAfter = retryWait;
|
||||||
|
|
||||||
|
public TooManyRequestsException(string message, WebResult response)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
if (response.Headers.ContainsKey("Retry-After"))
|
||||||
|
{
|
||||||
|
var retryAfter = response.Headers["Retry-After"].ToString();
|
||||||
|
|
||||||
|
if (int.TryParse(retryAfter, out var seconds))
|
||||||
|
RetryAfter = TimeSpan.FromSeconds(seconds);
|
||||||
|
else if (DateTime.TryParse(retryAfter, out var date))
|
||||||
|
RetryAfter = date.ToUniversalTime() - DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,17 +7,17 @@ namespace Jackett.Common
|
|||||||
{
|
{
|
||||||
public IIndexer Indexer { get; protected set; }
|
public IIndexer Indexer { get; protected set; }
|
||||||
|
|
||||||
public IndexerException(IIndexer Indexer, string message, Exception innerException)
|
public IndexerException(IIndexer indexer, string message, Exception innerException)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
=> this.Indexer = Indexer;
|
=> this.Indexer = indexer;
|
||||||
|
|
||||||
public IndexerException(IIndexer Indexer, string message)
|
public IndexerException(IIndexer indexer, string message)
|
||||||
: this(Indexer, message, null)
|
: this(indexer, message, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public IndexerException(IIndexer Indexer, Exception innerException)
|
public IndexerException(IIndexer indexer, Exception innerException)
|
||||||
: this(Indexer, "Exception (" + Indexer.Id + "): " + innerException.GetBaseException().Message, innerException)
|
: this(indexer, "Exception (" + indexer.Id + "): " + innerException.GetBaseException().Message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jackett.Common.Exceptions;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
using Jackett.Common.Models.IndexerConfig;
|
using Jackett.Common.Models.IndexerConfig;
|
||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
@@ -315,6 +316,12 @@ namespace Jackett.Common.Indexers
|
|||||||
expireAt = DateTime.Now.Add(HealthyStatusValidity);
|
expireAt = DateTime.Now.Add(HealthyStatusValidity);
|
||||||
return new IndexerResult(this, results, false);
|
return new IndexerResult(this, results, false);
|
||||||
}
|
}
|
||||||
|
catch (TooManyRequestsException ex)
|
||||||
|
{
|
||||||
|
var delay = ex.RetryAfter.TotalSeconds;
|
||||||
|
expireAt = DateTime.Now.AddSeconds(delay);
|
||||||
|
throw new IndexerException(this, ex);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var delay = Math.Min(MaxStatusValidity.TotalSeconds, ErrorStatusValidity.TotalSeconds * Math.Pow(2, errorCount++));
|
var delay = Math.Min(MaxStatusValidity.TotalSeconds, ErrorStatusValidity.TotalSeconds * Math.Pow(2, errorCount++));
|
||||||
|
@@ -6,8 +6,8 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jackett.Common.Exceptions;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
using Jackett.Common.Models.IndexerConfig;
|
using Jackett.Common.Models.IndexerConfig;
|
||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
@@ -139,25 +139,20 @@ namespace Jackett.Common.Indexers
|
|||||||
await RenewalTokenAsync();
|
await RenewalTokenAsync();
|
||||||
|
|
||||||
var response = await RequestWithCookiesAsync(BuildSearchUrl(query));
|
var response = await RequestWithCookiesAsync(BuildSearchUrl(query));
|
||||||
if (response != null && response.ContentString.StartsWith("<"))
|
var responseCode = (int)response.Status;
|
||||||
|
|
||||||
|
switch (responseCode)
|
||||||
{
|
{
|
||||||
if (response.ContentString.Contains("torrentapi.org | 520:"))
|
case 429:
|
||||||
{
|
throw new TooManyRequestsException($"Rate limited with StatusCode {responseCode}, retry in 2 minutes", TimeSpan.FromMinutes(2));
|
||||||
if (retry)
|
case 520:
|
||||||
{
|
throw new TooManyRequestsException($"Rate limited with StatusCode {responseCode}, retry in 3 minutes", TimeSpan.FromMinutes(3));
|
||||||
logger.Warn("torrentapi.org returned Error 520, retrying after 5 secs");
|
case (int)HttpStatusCode.OK:
|
||||||
return await PerformQueryWithRetry(query, false);
|
break;
|
||||||
}
|
default:
|
||||||
else
|
throw new Exception($"Indexer API call returned an unexpected StatusCode [{responseCode}]");
|
||||||
{
|
|
||||||
logger.Warn("torrentapi.org returned Error 520");
|
|
||||||
return releases;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the response was not JSON, likely a HTML page for a server outage
|
|
||||||
logger.Warn(response.ContentString);
|
|
||||||
throw new Exception("The response was not JSON");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonContent = JObject.Parse(response.ContentString);
|
var jsonContent = JObject.Parse(response.ContentString);
|
||||||
var errorCode = jsonContent.Value<int>("error_code");
|
var errorCode = jsonContent.Value<int>("error_code");
|
||||||
switch (errorCode)
|
switch (errorCode)
|
||||||
@@ -176,11 +171,8 @@ namespace Jackett.Common.Indexers
|
|||||||
logger.Warn("torrentapi.org returned code 5 Too many requests per second, retrying after 5 secs");
|
logger.Warn("torrentapi.org returned code 5 Too many requests per second, retrying after 5 secs");
|
||||||
return await PerformQueryWithRetry(query, false);
|
return await PerformQueryWithRetry(query, false);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
throw new TooManyRequestsException("Rate limited, retry in 2 minutes", TimeSpan.FromMinutes(2));
|
||||||
logger.Warn("torrentapi.org returned code 5 Too many requests per second");
|
|
||||||
return releases;
|
|
||||||
}
|
|
||||||
case 8: // search_imdb not found, see issue #12466 (no longer used, has been replaced with error 10)
|
case 8: // search_imdb not found, see issue #12466 (no longer used, has been replaced with error 10)
|
||||||
case 9: // invalid imdb, see Radarr #1845
|
case 9: // invalid imdb, see Radarr #1845
|
||||||
case 13: // invalid tmdb, invalid tvdb
|
case 13: // invalid tmdb, invalid tvdb
|
||||||
@@ -188,32 +180,23 @@ namespace Jackett.Common.Indexers
|
|||||||
case 10: // imdb not found, see issue #1486
|
case 10: // imdb not found, see issue #1486
|
||||||
case 14: // tmdb not found (see Radarr #7625), thetvdb not found
|
case 14: // tmdb not found (see Radarr #7625), thetvdb not found
|
||||||
case 20: // no results found
|
case 20: // no results found
|
||||||
if (jsonContent.ContainsKey("rate_limit"))
|
if (jsonContent.Value<int>("rate_limit") is 1 && jsonContent.Value<JArray>("torrent_results") == null)
|
||||||
{
|
throw new TooManyRequestsException("Rate limited, retry in 5 minutes", TimeSpan.FromMinutes(5));
|
||||||
if (retry)
|
|
||||||
{
|
|
||||||
logger.Warn("torrentapi.org returned code 20 with Rate Limit exceeded. Retrying after 5 secs.");
|
|
||||||
return await PerformQueryWithRetry(query, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.Warn("torrentapi.org returned code 20 with Rate Limit exceeded.");
|
|
||||||
return releases;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the api returns "no results" in some valid queries. we do one retry on this case but we can't do more
|
// the api returns "no results" in some valid queries. we do one retry on this case but we can't do more
|
||||||
// because we can't distinguish between search without results and api malfunction
|
// because we can't distinguish between search without results and api malfunction
|
||||||
return retry ? await PerformQueryWithRetry(query, false) : releases;
|
return retry ? await PerformQueryWithRetry(query, false) : releases;
|
||||||
default:
|
default:
|
||||||
throw new Exception("Unknown error code: " + errorCode + " response: " + response.ContentString);
|
throw new Exception($"Unknown error code: {errorCode}. Response: {response.ContentString}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jsonContent.Value<JArray>("torrent_results") == null)
|
||||||
|
return releases;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var item in jsonContent.Value<JArray>("torrent_results"))
|
foreach (var item in jsonContent.Value<JArray>("torrent_results"))
|
||||||
{
|
{
|
||||||
var title = WebUtility.HtmlDecode(item.Value<string>("title"));
|
|
||||||
|
|
||||||
var magnetStr = item.Value<string>("download");
|
var magnetStr = item.Value<string>("download");
|
||||||
var magnetUri = new Uri(magnetStr);
|
var magnetUri = new Uri(magnetStr);
|
||||||
|
|
||||||
@@ -224,27 +207,21 @@ namespace Jackett.Common.Indexers
|
|||||||
// append app_id to prevent api server returning 403 forbidden
|
// append app_id to prevent api server returning 403 forbidden
|
||||||
var details = new Uri(item.Value<string>("info_page") + "&app_id=" + _appId);
|
var details = new Uri(item.Value<string>("info_page") + "&app_id=" + _appId);
|
||||||
|
|
||||||
// ex: 2015-08-16 21:25:08 +0000
|
|
||||||
var dateStr = item.Value<string>("pubdate").Replace(" +0000", "");
|
|
||||||
var dateTime = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
|
||||||
var publishDate = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc).ToLocalTime();
|
|
||||||
|
|
||||||
var size = item.Value<long>("size");
|
|
||||||
var seeders = item.Value<int>("seeders");
|
var seeders = item.Value<int>("seeders");
|
||||||
var leechers = item.Value<int>("leechers");
|
var leechers = item.Value<int>("leechers");
|
||||||
|
|
||||||
var release = new ReleaseInfo
|
var release = new ReleaseInfo
|
||||||
{
|
{
|
||||||
Title = title,
|
|
||||||
Category = MapTrackerCatDescToNewznab(item.Value<string>("category")),
|
|
||||||
MagnetUri = magnetUri,
|
|
||||||
InfoHash = infoHash,
|
|
||||||
Details = details,
|
|
||||||
PublishDate = publishDate,
|
|
||||||
Guid = guid,
|
Guid = guid,
|
||||||
|
Details = details,
|
||||||
|
MagnetUri = magnetUri,
|
||||||
|
Title = WebUtility.HtmlDecode(item.Value<string>("title")).Trim(),
|
||||||
|
Category = MapTrackerCatDescToNewznab(item.Value<string>("category")),
|
||||||
|
InfoHash = infoHash,
|
||||||
|
PublishDate = DateTime.Parse(item.Value<string>("pubdate"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||||
Seeders = seeders,
|
Seeders = seeders,
|
||||||
Peers = leechers + seeders,
|
Peers = leechers + seeders,
|
||||||
Size = size,
|
Size = item.Value<long>("size"),
|
||||||
DownloadVolumeFactor = 0,
|
DownloadVolumeFactor = 0,
|
||||||
UploadVolumeFactor = 1
|
UploadVolumeFactor = 1
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Jackett.Common;
|
using Jackett.Common;
|
||||||
|
using Jackett.Common.Exceptions;
|
||||||
using Jackett.Common.Indexers;
|
using Jackett.Common.Indexers;
|
||||||
using Jackett.Common.Indexers.Meta;
|
using Jackett.Common.Indexers.Meta;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
@@ -14,9 +15,11 @@ using Jackett.Common.Models.DTO;
|
|||||||
using Jackett.Common.Services.Interfaces;
|
using Jackett.Common.Services.Interfaces;
|
||||||
using Jackett.Common.Utils;
|
using Jackett.Common.Utils;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
|
||||||
namespace Jackett.Server.Controllers
|
namespace Jackett.Server.Controllers
|
||||||
@@ -432,6 +435,20 @@ namespace Jackett.Server.Controllers
|
|||||||
|
|
||||||
return Content(xml, "application/rss+xml", Encoding.UTF8);
|
return Content(xml, "application/rss+xml", Encoding.UTF8);
|
||||||
}
|
}
|
||||||
|
catch (IndexerException ex) when (ex.InnerException is TooManyRequestsException tooManyRequestsException)
|
||||||
|
{
|
||||||
|
logger.Error(ex);
|
||||||
|
|
||||||
|
if (!HttpContext.Response.Headers.ContainsKey("Retry-After"))
|
||||||
|
{
|
||||||
|
var retryAfter = Convert.ToInt32(tooManyRequestsException.RetryAfter.TotalSeconds);
|
||||||
|
|
||||||
|
if (retryAfter > 0)
|
||||||
|
HttpContext.Response.Headers.Add("Retry-After", $"{retryAfter}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetErrorXML(900, ex.Message, StatusCodes.Status429TooManyRequests);
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.Error(e);
|
logger.Error(e);
|
||||||
@@ -440,7 +457,18 @@ namespace Jackett.Server.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Route("[action]/{ignored?}")]
|
[Route("[action]/{ignored?}")]
|
||||||
public IActionResult GetErrorXML(int code, string description) => Content(CreateErrorXML(code, description), "application/xml", Encoding.UTF8);
|
public IActionResult GetErrorXML(int code, string description, int statusCode = StatusCodes.Status400BadRequest)
|
||||||
|
{
|
||||||
|
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse("application/xml");
|
||||||
|
mediaTypeHeaderValue.Encoding = Encoding.UTF8;
|
||||||
|
|
||||||
|
return new ContentResult
|
||||||
|
{
|
||||||
|
StatusCode = statusCode,
|
||||||
|
Content = CreateErrorXML(code, description),
|
||||||
|
ContentType = mediaTypeHeaderValue.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static string CreateErrorXML(int code, string description)
|
public static string CreateErrorXML(int code, string description)
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user