ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.26k stars 745 forks source link

Add possibility to add headers in Generated HttpClient #6426

Open Lukeuke opened 1 year ago

Lukeuke commented 1 year ago

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

danny-zegel-zocdoc commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

Lukeuke commented 1 year ago

@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 commented 1 year ago

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

twogood commented 1 year ago

@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?

mikeries commented 7 months ago

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?

Socolin commented 7 months ago

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

mikeries commented 7 months ago

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: @.***>

wtfuii commented 4 months ago

@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.