Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ internal static class Constants

// Messages
public const string ApiResponseLimitExceeded = "higher API call volume";
public const string ApiResponseRateLimit = "rate limit";
public const string ApiResponseApiKeyInvalid = "apikey is invalid";
public const string ApiResponsePremiumEndpoint = "premium endpoint";
public const string ApiResponsePremiumEndpoint = "This is a premium endpoint";

#region Yahoo Finance

Expand Down
18 changes: 9 additions & 9 deletions src/Services/AlphaVantageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public AlphaVantageService(

var jsonResponse = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
ThrowIfRejected(jsonResponse);
if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded))
if (IsRateLimited(jsonResponse))
{
throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}");
}
Expand Down Expand Up @@ -128,7 +128,7 @@ public async Task<IEnumerable<Record>> GetRecordsAsync(string symbol, DateTime?
{
var jsonResponse = await Helper.FetchJsonDocumentAsync(httpClient, _logger, url, ct).ConfigureAwait(false);
ThrowIfRejected(jsonResponse);
if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded))
if (IsRateLimited(jsonResponse))
{
throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}");
}
Expand Down Expand Up @@ -200,7 +200,7 @@ private async Task<List<IntradayRecord>> GetIntradayRecordsByMonthAsync(string s

var jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
ThrowIfRejected(jsonResponse);
if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded))
if (IsRateLimited(jsonResponse))
{
throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {symbol}");
}
Expand Down Expand Up @@ -244,7 +244,7 @@ public async Task<IEnumerable<ForexRecord>> GetForexRecordsAsync(string currency
{
var jsonResponse = await Helper.FetchJsonDocumentAsync(httpClient, _logger, url, ct).ConfigureAwait(false);
ThrowIfRejected(jsonResponse);
if (jsonResponse.Contains(Constants.ApiResponseLimitExceeded))
if (IsRateLimited(jsonResponse))
{
throw new FinanceNetException($"{Constants.ApiResponseLimitExceeded} for {currency1} /{currency2}");
}
Expand All @@ -262,21 +262,21 @@ public async Task<IEnumerable<ForexRecord>> GetForexRecordsAsync(string currency
}
}

/// <summary>
/// Alpha Vantage answers a refused request with HTTP 200 and a JSON message instead of data.
/// A premium-only endpoint or a bad API key will not change on retry, so fail immediately.
/// </summary>
// Alpha Vantage refuses with HTTP 200 and a JSON message; a premium endpoint or a bad key will not change on retry.
private static void ThrowIfRejected(string jsonResponse)
{
if (jsonResponse.Contains(Constants.ApiResponsePremiumEndpoint))
{
throw new FinanceNetAccessDeniedException(
$"Alpha Vantage rejected the request: this is a {Constants.ApiResponsePremiumEndpoint} and needs a premium plan");
"Alpha Vantage rejected the request: this is a premium endpoint and needs a premium plan");
}
if (jsonResponse.Contains(Constants.ApiResponseApiKeyInvalid))
{
throw new FinanceNetAccessDeniedException(
$"Alpha Vantage rejected the request: {Constants.ApiResponseApiKeyInvalid} or missing");
}
}

private static bool IsRateLimited(string jsonResponse) =>
jsonResponse.Contains(Constants.ApiResponseRateLimit) || jsonResponse.Contains(Constants.ApiResponseLimitExceeded);
}
8 changes: 4 additions & 4 deletions tests/IntegrationTests/AlphaVantageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task GetOverviewAsync(string symbol, bool shouldHave)
}
else
{
Assert.ThrowsAsync<FinanceNetException>(async () => await _service.GetOverviewAsync(symbol));
Assert.CatchAsync<FinanceNetException>(async () => await _service.GetOverviewAsync(symbol));
}
}

Expand All @@ -61,7 +61,7 @@ public async Task GetRecordsAsync(string symbol, bool shouldHave)
}
else
{
Assert.ThrowsAsync<FinanceNetException>(async () => await _service.GetRecordsAsync(symbol, DateTime.UtcNow.AddDays(-7)));
Assert.CatchAsync<FinanceNetException>(async () => await _service.GetRecordsAsync(symbol, DateTime.UtcNow.AddDays(-7)));
}
}

Expand All @@ -85,7 +85,7 @@ public async Task GetIntradayRecordsAsync(string symbol, EInterval eInterval, bo
}
else
{
Assert.ThrowsAsync<FinanceNetException>(async () => await _service.GetIntradayRecordsAsync(symbol, startDay, endDay, eInterval));
Assert.CatchAsync<FinanceNetException>(async () => await _service.GetIntradayRecordsAsync(symbol, startDay, endDay, eInterval));
}
}

Expand All @@ -101,7 +101,7 @@ public async Task GetForexRecordsAsync(string currency1, string currency2, bool
}
else
{
Assert.ThrowsAsync<FinanceNetException>(async () => await _service.GetForexRecordsAsync(currency1, currency2, DateTime.UtcNow.AddDays(-3)));
Assert.CatchAsync<FinanceNetException>(async () => await _service.GetForexRecordsAsync(currency1, currency2, DateTime.UtcNow.AddDays(-3)));
}
}
}
15 changes: 8 additions & 7 deletions tests/Services/AlphaVantageRejectedRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,19 @@ public void InvalidApiKey_FailsFastWithoutRetrying(string name, Func<AlphaVantag

var exception = Assert.CatchAsync<FinanceNetAccessDeniedException>(async () => await call(_service), name);
Assert.That(exception.Message, Does.Contain("apikey is invalid"), name);
Assert.That(exception.InnerException, Is.Null, name);
Assert.That(_requestCount, Is.EqualTo(1), $"{name}: an invalid-API-key refusal was retried");
}

[Test]
public void ApiLimitExceeded_IsStillRetried()
[TestCaseSource(nameof(Calls))]
public void RateLimit_IsStillRetried(string name, Func<AlphaVantageService, Task> call)
{
// The per-minute limit clears on its own, so it stays a retryable failure.
SetUpResponse("{\"Information\": \"Please consider spreading out your free API requests more sparingly (1 request per second). You may subscribe to any of the premium plans at https://www.alphavantage.co/premium/ to lift the free key rate limit, or visit higher API call volume.\"}");
// The rate-limit notice advertises "premium endpoints" too, but clears on its own.
SetUpResponse("{\"Information\": \"Thank you for using Alpha Vantage! Please consider spreading out your free API requests more sparingly (1 request per second). You may subscribe to any of the premium plans at https://www.alphavantage.co/premium/ to lift the free key rate limit (25 requests per day), raise the per-second burst limit, and instantly unlock all premium endpoints\"}");

var exception = Assert.CatchAsync<FinanceNetException>(async () => await _service.GetRecordsAsync("IBM", StartDate));
Assert.That(exception, Is.Not.InstanceOf<FinanceNetAccessDeniedException>());
Assert.That(_requestCount, Is.EqualTo(RetryCount + 1));
var exception = Assert.CatchAsync<FinanceNetException>(async () => await call(_service), name);
Assert.That(exception, Is.Not.InstanceOf<FinanceNetAccessDeniedException>(), name);
Assert.That(_requestCount, Is.EqualTo(RetryCount + 1), $"{name}: a rate-limit notice was not retried");
}

[Test]
Expand Down
Loading