graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Add possibility to add headers in Generated HttpClient

Open Lukeuke opened this issue 2 years ago • 10 comments

Product

Strawberry Shake

Is your feature request related to a problem?

I've been following the docs: chillicream.com/docs/strawberryshake/v13/get-started and I ran into an issue, because there's nowhere I can add request headers to the Query.

I have configured client in Program.cs

builder.Services.AddHttpClient(
    CryptoClient.ClientName,
    client => client.BaseAddress =
        client.BaseAddress = new Uri("http://localhost:5106/graphql"));

builder.Services.AddCryptoClient();

And I have this simple Query that I want to execute:

query GetMessages{
    messages{
        nodes {
            content
        }
    }
}

I'm executing it like this:

var token = await LocalStorage.GetItemAsStringAsync("token");

token = token.Replace("\"", "");


var result = await CryptoClient.GetMessages.ExecuteAsync();
var messages = result.Data.Messages.Nodes;

But the endpoint that I'm hitting is Authorized: image

I've been searching the docs but the only solution that I found is that I can do it like this:

services
    .AddConferenceClient()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress =
            new Uri("https://workshop.chillicream.com/graphql/");
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", "Your Oauth token");
    });

But the thing is that my token is dynamic and its from the LocalStorage so I can't just assign it to the Program.cs

The solution you'd like

I would like to have access to something like:

CryptoClient.Headers.Authorization =
    new AuthenticationHeaderValue("Bearer", your_token);

var result = await CryptoClient.GetMessages.ExecuteAsync();
var messages = result.Data.Messages.Nodes;

Note: CryptoClient is HttpClient added to Program.cs as GraphQL client

Lukeuke avatar Aug 09 '23 18:08 Lukeuke

I also have a similar need, the GQL server I'm hitting requires some custom headers for some queries. I need to be able to supply custom headers to a subset of requests and with dynamic values.

Just to preempt the obvious point that this may not be an ideal design, as the API consumer, the design is not in my control and I just need to consume the API as it is.

I'm not a fan of supplying headers via static methods but I think there are some options for very nice solutions.

Solution proposal 1

ExecuteAsync can unconditionally accept an optional headers parameter. Given that enabling this on all queries could be a breaking change unless the new parameter is added after the cancellationToken parameter, which would be awkward, this feature could be enabled for a given query based on some annotation in the .graphql file, something like this:

#[ACCEPT_HEADERS]
query GetMessages {
    ...
}

Solution proposal 2

Queries defined in .graphql files can specify some annotation to inform the framework to accept specific header params for the request, something like this:

#[HEADER(x-required-header: String!)]
#[HEADER(x-optional-header: String)]
query GetMessages($param1 String, $param2: Int) {
    ...
}

so that the query accepts the header parameters as it does other arguments like this GetMessages.ExecuteAsync(param1: "whatever", param2: 10, xRequiredHeader: "some value" )

(granted this approach would require handling of kabab cased headers and

danny-zegel-zocdoc avatar Aug 28 '23 12:08 danny-zegel-zocdoc

after looking around a bit I found https://github.com/ChilliCream/graphql-platform/issues/5204 and https://github.com/ChilliCream/graphql-platform/issues/3467 which seem to overlap this this issue.

danny-zegel-zocdoc avatar Aug 28 '23 12:08 danny-zegel-zocdoc

the proposal in https://github.com/ChilliCream/graphql-platform/issues/3467 seems similar to my annotation concept except that its using directives (a feature I'm not familiar with) which seems better being that its code and not just comments

danny-zegel-zocdoc avatar Aug 29 '23 11:08 danny-zegel-zocdoc

@danny-zegel-zocdoc I got around the problem by just implemeting the TokenStore which stores the token in Services as Singleton

public class TokenStore
{
    public string Jwt { get; set; } = null!;
}

In Program.cs:

builder.Services.AddSingleton<TokenStore>();

...

builder.Services.AddHttpClient(
    CryptoClient.ClientName,
    (service, client) =>
    {
        var store = service.GetRequiredService<TokenStore>();
        
        client.BaseAddress =
            client.BaseAddress = new Uri("http://localhost:5106/graphql");
        
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", store.Jwt); // Here you can add the header with any value you want and then 
         // just modify on runtime, Example below
    });

Example: When loggin the user

    private async Task<bool> LoginUser()
    {
        var result = IdentityRepository.LoginUser(User);

        await result;
        
        if (!result.Result.Item1)
        {
            LoginMessage = result.Result.Item2.ToString();
            return false;
        }

        var token = result.Result.Item2 as SignInResponseDto;

        await LocalStorage.SetItemAsync("token", token.Token);
        await AuthStateProvider.GetAuthenticationStateAsync();

        TokenStore.Jwt = token.Token; // Inject the TokenStore as Service and then change the value

       // Now you can make requests with HttpClient to GraphQL server with the changed value
        
        NavigationManager.NavigateTo("/");
        return await Task.FromResult(true);
    }

Lukeuke avatar Aug 29 '23 14:08 Lukeuke

@danny-zegel-zocdoc Your solution proposal 2 is the best IMO

Lukeuke avatar Aug 29 '23 14:08 Lukeuke

@Lukeuke I'm facing exactly the same issue, but I think your solution will still only have one CryptoClient with the first JWT you set and then that one will be reused by all subsequent requests?

twogood avatar Sep 14 '23 12:09 twogood

We're facing a similar problem. In our case, the token is user-specific and can only be provided from a scoped context. I've read many issues similar to this one and have yet to find a workaround.

I believe I saw a reference that things were being redone to address this in version 14, but I don't know when that will be and I'm not sure it will fix my scenario.

I didn't think our use-case was unusual. We have an application with multiple tenants and we don't want customers in one accessing data from another. Anyone have a workaround to suggest?

mikeries avatar Apr 04 '24 18:04 mikeries

@mikeries You can check this https://github.com/ChilliCream/graphql-platform/issues/3467#issuecomment-1962226640

Socolin avatar Apr 04 '24 18:04 Socolin

Thanks! That worked great!

On Thu, Apr 4, 2024 at 1:19 PM Socolin @.***> wrote:

@mikeries https://github.com/mikeries You can check this #3467 (comment) https://github.com/ChilliCream/graphql-platform/issues/3467#issuecomment-1962226640

— Reply to this email directly, view it on GitHub https://github.com/ChilliCream/graphql-platform/issues/6426#issuecomment-2037887800, or unsubscribe https://github.com/notifications/unsubscribe-auth/AECETTRMFPFSPJIPALOJ7TDY3WKRXAVCNFSM6AAAAAA3KLZGBGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMZXHA4DOOBQGA . You are receiving this because you were mentioned.Message ID: @.***>

mikeries avatar Apr 04 '24 21:04 mikeries

@Lukeuke I'm facing exactly the same issue, but I think your solution will still only have one CryptoClient with the first JWT you set and then that one will be reused by all subsequent requests?

If this is still relevant for you: I can confirm that the client is getting regenerated for every request. To quote the documentation:

Strawberry Shake uses the HttpClientFactory to generate a HttpClient on every request.

wtfuii avatar Jul 22 '24 22:07 wtfuii