Created
February 6, 2025 08:28
-
-
Save TosinShada/c551877cd2105092fc2192e533d631e2 to your computer and use it in GitHub Desktop.
Calling multiple APIs in parallel and returning each of their results back to the caller even if one of the APIs throws an exception
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text.Json; | |
namespace ConsoleApp; | |
public class ApiCaller | |
{ | |
private readonly HttpClient _httpClient; | |
public ApiCaller(HttpClient httpClient) | |
{ | |
_httpClient = httpClient; | |
_httpClient.Timeout = TimeSpan.FromSeconds(5); // Set a default timeout for all requests | |
} | |
public async Task<List<(string RequestUrl, HttpResponseMessage Response, string? Content)>> | |
ProcessMultipleApiCallsAsync(List<string> endpoints) | |
{ | |
List<(string RequestUrl, HttpResponseMessage Response, string? Content)> results = new(); | |
// Create a list of tasks, one for each API call | |
var apiCallTasks = | |
endpoints.Select(endpoint => _httpClient.GetAsync(endpoint)).ToList(); | |
try | |
{ | |
// Await all the tasks concurrently | |
var responses = await Task.WhenAll(apiCallTasks); | |
// Process the responses *in the same order* as the original requests. This is crucial for correlating requests and responses. | |
for (var i = 0; i < responses.Length; i++) | |
{ | |
var requestUrl = endpoints[i]; // Get the original request URL. | |
var response = responses[i]; | |
string? content = null; | |
if (response.IsSuccessStatusCode) | |
{ | |
content = await response.Content.ReadAsStringAsync(); | |
} | |
// Even on failure, add to the result list so we have complete tracking. | |
results.Add((requestUrl, response, content)); | |
} | |
} | |
catch (Exception ex) | |
{ | |
// Handle exceptions that might occur during *any* of the API calls. Task.WhenAll will throw an exception if ANY of the tasks throw. | |
// Important: The original exception from Task.WhenAll is an AggregateException. You often need to inspect its InnerExceptions. | |
Console.WriteLine($"An error occurred during API calls: {ex.Message}"); | |
// Iterate through the original tasks to find the specific failed requests and their original endpoints. | |
for (var i = 0; i < apiCallTasks.Count; i++) | |
{ | |
switch (apiCallTasks[i].Status) | |
{ | |
case TaskStatus.RanToCompletion: | |
{ | |
var requestUrl = endpoints[i]; // Get the original request URL. | |
var response = apiCallTasks[i].Result; | |
string? content = null; | |
if (response.IsSuccessStatusCode) | |
{ | |
content = await response.Content.ReadAsStringAsync(); | |
} | |
// Even on failure, add to the result list so we have complete tracking. | |
results.Add((requestUrl, response, content)); | |
break; | |
} | |
case TaskStatus.Faulted: | |
{ | |
// Get the original request endpoint from the endpoints list | |
var requestUrl = endpoints[i]; | |
//If the exception is not an HttpRequestException, use the generic Exception type, otherwise access the InnerException directly | |
var innerException = apiCallTasks[i].Exception?.InnerException; | |
if (innerException is HttpRequestException httpEx) | |
{ | |
Console.WriteLine( | |
$"Request to {requestUrl} failed: {httpEx.StatusCode} - {httpEx.Message}"); | |
results.Add((requestUrl, | |
new HttpResponseMessage(httpEx.StatusCode ?? | |
System.Net.HttpStatusCode.ServiceUnavailable) | |
{ RequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl) }, null)); | |
} | |
else | |
{ | |
Console.WriteLine($"Request to {requestUrl} failed: {innerException?.Message}"); | |
results.Add((requestUrl, | |
new HttpResponseMessage(System.Net.HttpStatusCode.ServiceUnavailable) | |
{ RequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl) }, null)); | |
} | |
break; | |
} | |
case TaskStatus.Canceled: | |
{ | |
// Get the original request endpoint from the endpoints list | |
var requestUrl = endpoints[i]; | |
Console.WriteLine($"Request to {requestUrl} was canceled."); | |
results.Add((requestUrl, | |
new HttpResponseMessage(System.Net.HttpStatusCode.RequestTimeout) | |
{ RequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl) }, | |
null)); // Or another appropriate status code | |
break; | |
} | |
case TaskStatus.Created: | |
case TaskStatus.WaitingForActivation: | |
case TaskStatus.WaitingToRun: | |
case TaskStatus.Running: | |
case TaskStatus.WaitingForChildrenToComplete: | |
default: | |
{ | |
// Get the original request endpoint from the endpoints list | |
var requestUrl = endpoints[i]; | |
// Handle other unexpected exceptions | |
Console.WriteLine($"A general error occurred: {ex.Message}"); | |
// In this case, you don't know which requests failed, or you may have some partial results. Add entries with 500 for all endpoints | |
results.Add((requestUrl, | |
new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError), null)); | |
break; | |
} | |
} | |
} | |
} | |
return results; | |
} | |
//Helper function for pretty output | |
public static void PrintResults( | |
List<(string RequestUrl, HttpResponseMessage Response, string? Content)> results) | |
{ | |
foreach (var result in results) | |
{ | |
Console.WriteLine($"Request URL: {result.RequestUrl}"); | |
Console.WriteLine($"Status Code: {result.Response.StatusCode}"); | |
if (result.Response.IsSuccessStatusCode) | |
{ | |
try | |
{ | |
// Attempt to deserialize as JSON. If it fails, just print the raw content. | |
var jsonDocument = JsonDocument.Parse(result.Content!); | |
var prettyJson = JsonSerializer.Serialize(jsonDocument, | |
new JsonSerializerOptions { WriteIndented = true }); | |
Console.WriteLine($"Content: {prettyJson}"); | |
} | |
catch (JsonException) | |
{ | |
// Not valid JSON, just print the raw content. | |
Console.WriteLine($"Content (Raw): {result.Content}"); | |
} | |
catch (Exception e) | |
{ | |
// Other exceptions that might happen on reading the result. | |
Console.WriteLine($"Error processing content: {e.Message}"); | |
} | |
} | |
else | |
{ | |
Console.WriteLine($"Reason Phrase: {result.Response.ReasonPhrase}"); // More detailed error reason. | |
} | |
Console.WriteLine("---"); | |
} | |
} | |
} | |
public static class Program | |
{ | |
public static async Task Main(string[] args) | |
{ | |
// Create an HttpClient instance (usually done with dependency injection in real applications) | |
using HttpClient httpClient = new(); | |
ApiCaller apiCaller = new(httpClient); | |
// Example list of API endpoints | |
List<string> endpoints = | |
[ | |
"https://api-accessgive.aff.ng/health", // Valid endpoint | |
"https://api-accessgive.aff.ng/invalid", // Invalid endpoint (likely 404) | |
"https://esbprod.accessbankplc.com/timeout", // Unreachable endpoint (timeout) | |
"https://api-accessgive.aff.ng/api/v1/Campaign/id/unknown" // Invalid endpoint (likely 500) | |
]; | |
// Call the API for multiple endpoints | |
var results = await apiCaller.ProcessMultipleApiCallsAsync(endpoints); | |
// Process the results (e.g., log, display, etc.) | |
ApiCaller.PrintResults(results); | |
// Example of how to use the results for further processing: | |
Console.WriteLine("\nProcessing successful results:"); | |
foreach (var result in results) | |
{ | |
if (result.Response.IsSuccessStatusCode) | |
{ | |
// Do something with successful responses, e.g. deserialize JSON, store in database, etc. | |
Console.WriteLine( | |
$"Successfully fetched data from {result.RequestUrl}: {result.Content?.Substring(0, Math.Min(result.Content.Length, 50))}..."); // Just a snippet | |
} | |
} | |
Console.WriteLine("\nProcessing unsuccessful results:"); | |
foreach (var result in results) | |
{ | |
if (!result.Response.IsSuccessStatusCode) | |
{ | |
// Handle failed responses. For example, you could retry, log errors, or alert someone. | |
Console.WriteLine( | |
$"Failed to fetch data from {result.RequestUrl}. Status Code: {result.Response.StatusCode}"); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment