13

In my .net core web api project I would like to hit an external API so that I get my response as expected.

The way I`m registering and using the HttpClient is as follows. In the startup, I'm adding the following code which is called named typed httpclient way.

 services.AddHttpClient<IRecipeService, RecipeService>(c => {
                c.BaseAddress = new Uri("https://sooome-api-endpoint.com");
                c.DefaultRequestHeaders.Add("x-raay-key", "123567890754545645gggg");
                c.DefaultRequestHeaders.Add("x-raay-host", "sooome-api-endpoint.com");
            });

In addition to this, I have 1 service in which I inject the HttpClient.


    public class RecipeService : IRecipeService
    {
        private readonly HttpClient _httpClient;

        public RecipeService(HttpClient httpClient)
        {
           _httpClient = httpClient;
        }

        public async Task<List<Core.Dtos.Recipes>> GetReBasedOnIngAsync(string endpoint)
        {
            
            using (var response = await _httpClient.GetAsync(recipeEndpoint))
            {
                // ...
            }
        }
    }

When the httpClient is created, if I hover over the object itself, the base URI/Headers are missing, and I don't understand why exactly this is happening. I would appreciate if someone could show some light :)

UPDATE 1ST

The service is being used in one of the Controllers shown below. The service is injected by the DI and then the relative path is parsed to the service ( I assumed I already have the base URL stored in the client ) Maybe I`m doing it wrong?


namespace ABC.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FridgeIngredientController : ControllerBase
    {
        private readonly IRecipeService _recipeService;
        private readonly IMapper _mapper;

        public FridgeIngredientController(IRecipeService recipeService, IMapper mapper)
        {
            _recipeService = recipeService;
            _mapper = mapper;
        }

        [HttpPost("myingredients")]
        public async Task<ActionResult> PostIngredients(IngredientsDto ingredientsDto)
        {
            var readyUrIngredientStr = string.Join("%2", ingredientsDto.ingredients);

            var urlEndpoint = $"recipes/findByIngredients?ingredients={readyUrIngredientStr}";
            var recipesResponse = await _recipeService.GetRecipeBasedOnIngredientsAsync(urlEndpoint);
            InMyFridgeRecipesDto recipesFoundList = new InMyFridgeRecipesDto
            {
                FoundRecipes = recipesResponse
            };

            return Ok(recipesFoundList);
        }
    }
}


Any suggestions?

Serge
  • 40,935
  • 4
  • 18
  • 45
Csibi Norbert
  • 780
  • 1
  • 11
  • 35
  • Ok cool. remove the leading slash from `urlEndpoint` in controller action. – Nkosi Jan 18 '21 at 17:15
  • And just to be clear. `IReService` is really meant to be `IRecipeService`? – Nkosi Jan 18 '21 at 17:17
  • Did you remove the `AddScoped` as suggested before and only used the `AddHttpClient`? – Nkosi Jan 18 '21 at 17:18
  • @Nkosi Okay, so I'm trying to respond to your 3 comments. In the first comment you made, I did that, and i got this error ```An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.``` Although the base should have been set... In the second comment, yes, they are the same, but I didn't stay to shorten things up anymore...it's the same – Csibi Norbert Jan 18 '21 at 17:25
  • @Nkosi I did remove the ```AddScoped``` and used only ```AddHttpClient``` as suggested, so there is 1-1 alike code – Csibi Norbert Jan 18 '21 at 17:26
  • Update the post with the suggested changes you made that are still not working. We'll move forward from that point. – Nkosi Jan 18 '21 at 17:31

6 Answers6

22

A simple, frustrating reason this may happen is due to the order of your service collection statements.

Assigning the dependant service after the HTTPClient will not work, it must come before:

// NOT WORKING - BaseAddress is null
services.AddTransient<Controller1>();
services.AddTransient<Controller2>();

services.AddHttpClient<HttpService>(client =>
{
    client.BaseAddress = new Uri(baseAdress);
});

services.AddTransient<HttpService>();


// WORKING - BaseAddress is not null
services.AddTransient<Controller1>();
services.AddTransient<Controller2>();
services.AddTransient<HttpService>();

services.AddHttpClient<HttpService>(client =>
{
    client.BaseAddress = new Uri(baseAdress);
});

EDIT

As LIFEfreedom rightfully pointed out in their answer: while the order of the statements has an effect here, it is not the reason for behaviour.

Both of the following statements create a transient service for the HttpService class:

services.AddTransient<HttpService>();
services.AddHttpClient<HttpService>();

However, when adding both of these statements only the latest one will be used, overwriting any statements before it. In my example, I only got the expected result when the AddHttpClient statement with the base address configuration came last.

Jack
  • 886
  • 7
  • 27
11

You configured your client as a typed client and not a named client. No need for the factory.

You should explicitly inject the http client in constructor instead, not the http client factory.

Change your code to this:

private readonly HttpClient _httpClient;

public ReService(HttpClient httpClient) {
    _httpClient = httpClient;
}

public async Task<List<Core.Dtos.Re>> GetReBasedOnIngAsync(string endpoint)
{

    ///Remove this from your code
    var client = _httpClientFactory.CreateClient(); <--- HERE the base URL/Headers are missing
    
    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Get,
        RequestUri = new Uri(endpoint)
    };
    //////

    using (var response = await _httpClient.GetAsync(endpoint))
    {
        // ...
    }
}

And according to last MS documentation only the typed client registration is needed in this case. Fix your startup to this:

// services.AddScoped<IReService, ReService>(); //<-- REMOVE. NOT NEEDED
services.AddHttpClient<IReService, ReService>(c => ...

But you still can try to add you base address, please add trailing slash (and let us know if it still works):

services.AddHttpClient<IReService, ReService>(c => {
                c.BaseAddress = new Uri("https://sooome-api-endpoint.com/");
                 });

if problem still persists I recommend you to try named http clients.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
Serge
  • 40,935
  • 4
  • 18
  • 45
  • I did exactly how you said, but it still doesn't work. However, I have a question, I won't be better to use the factory of the httpclient? – Csibi Norbert Jan 18 '21 at 16:20
  • Sorry, I meant to say that even if I debug, there is no base address in the _httpClient nor headers. And the error is the following ```Invalid URI: The format of the URI could not be determined.``` because in baseAddress = null. Does this make sense? – Csibi Norbert Jan 18 '21 at 16:25
  • Microsoft has problems with typed clients and changes everything constantly. Look at the last changes made @Nkosi at my code. Try this. – Serge Jan 18 '21 at 16:28
  • @CsibiNorbert - Try to add trailing slash to your url. See my answer. – Serge Jan 18 '21 at 16:54
  • @Sergey So if I provide the full url to the ```GetAsync``` it is working but in the end I get a 401 because the Headers are not present in the httpClient. But still, I would like to be able to add those if possible in the Startup or I think I need the HttpRequestMesage to add those :/ – Csibi Norbert Jan 18 '21 at 17:01
  • @Nkosi Please see the updated part from my question.... So this is how I use it, I'm not sure if i use it wrong, or I'm missing something? Bare in mind that the baseAddress is null ( the one we set in the startup is not coming) however, if I add the full path to ```GetAsynt``` is working but it throws that I'm unauthorised due to the headers missing from the httpClient again... So in the end I wanted to achieve ```https://sooome-api-endpoint.com/something/something?ingredients=tomatoe2%potatoe``` So the endpoint parameter holds only this ```something/something?ingredients=tomatoe2%potatoe``` – Csibi Norbert Jan 18 '21 at 17:08
  • Did you get a solution for this? Seems like a straight forward use-case but not working – Rob Oct 15 '22 at 21:35
5

Okay so I will answer my post because with the suggested TYPED way of doing it was causing problems with the values not being set inside the httpClient, E.G BaseAddress was always null.

In the startup I was trying to go with typed httpclient e.g

services.AddHttpClient<IReService, ReService>(c => ...

But instead of doing that, I choose the to go with the Named client. Which means that in the startup we need to register the httpclient like this

services.AddHttpClient("recipeService", c => {
....

And then in the service itself I used HttpClientFactory like below.

 private readonly IHttpClientFactory _httpClientFactory;

        public RecipeService(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public async Task<List<Core.Dtos.Recipes>> GetRecipeBasedOnIngredientsAsync(string recipeEndpoint)
        {
            var client = _httpClientFactory.CreateClient("recipeService");

            using (var response = await client.GetAsync(client.BaseAddress + recipeEndpoint))
            {
                response.EnsureSuccessStatusCode();

                var responseRecipies = await response.Content.ReadAsStringAsync();
                var recipeObj = ConvertResponseToObjectList<Core.Dtos.Recipes>(responseRecipies);

                return recipeObj ?? null;
            }
        }
Csibi Norbert
  • 780
  • 1
  • 11
  • 35
4

@jack wrote a comment and several guys supported him that this is the right decision, but it is the wrong decision.

AddHttpClient creates a TService service as a Transient service, to which it passes an HttpClient created only for it

Calling first AddTransient, and then AddHttpClient<>, you add 2 implementations of one dependency and only the last added one will be returned

// Create first dependency
services.AddTransient<HttpService>();

// Create second and last dependency
services.AddHttpClient<HttpService>(client =>
{
    client.BaseAddress = new Uri(baseAdress);
});
LIFEfreedom
  • 53
  • 1
  • 5
1

As @LIFEfreedom has pointed out. Order of registration will show some effect. But why? If we register the type client after the service we are injecting it will work. The reason type client is registering the service once again. So multiple services are getting injected. But as we know DI provides you last registered service that's why it is working if the type client is registered at the end.

The simple solution to fix the actual issue is to not register the service separately. AddHttpClient will do it for you. As I'm aware type client register services as transient so be sure before using it.

// Do not register this
// services.AddTransient<HttpService>();

// This will automatically register HttpService.
services.AddHttpClient<HttpService>(client =>
{
    client.BaseAddress = new Uri(baseAdress);
});```


akshay_zz
  • 57
  • 1
  • 10
0

I experienced this and what worked for me was using the interface and concrete implementation of my service like this instead of using just the concrete class:

services.AddHttpClient<**ICategoryManager, CategoryManager**>(httpClient => 
{ 
        httpClient.BaseAddress = new Uri("https://sooome-api-endpoint.com/categories"); 
}); 
Psalm23
  • 1
  • 2