openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG][C-Sharp] HTTP subpath is ignored in the configured `BaseAddress` of the `HttpClient`

Open Dragemil opened this issue 1 year ago • 2 comments

Bug Report Checklist

  • [x] Have you provided a full/minimal spec to reproduce the issue?
  • [x] Have you validated the input using an OpenAPI validator (example)?
  • [ ] Have you tested with the latest master to confirm the issue still exists?
  • [x] Have you searched for related issues/PRs?
  • [x] What's the actual output vs expected output?
  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

Let's assume we've configured the BaseAddress of the HttpClient used in the generated C# client from the OpenAPI description like so:

client.BaseAddress = new Uri("http://localhost/subpath/");

Then, using the generated client, the /subpath/ part of the BaseAddress is completely ignored when sending HTTP requests.

openapi-generator version

7.7.0

OpenAPI declaration file content or url

https://github.com/ory/kratos/blob/master/.schema/openapi.json

Generation Details

openapi-generator-cli generate --config <...> https://github.com/leancodepl/dotnet-kratos-client/blob/master/generate.sh

Steps to reproduce
  1. Generate the C# OpenAPI client
  2. Configure HttpClient providing BaseAddress with a subpath
  3. Attempt to send a request using generated client
Related issues/PRs

Somehow related issue, where generated client ignored provided HTTP scheme: https://github.com/OpenAPITools/openapi-generator/issues/14682

Suggest a fix

Instead of building the base address ourselves, maybe we should use the one already configured in the HttpClient?

Dragemil avatar Aug 26 '24 08:08 Dragemil

Is your subpath not in ClientUtils.CONTEXT_PATH? image

devhl-labs avatar Aug 26 '24 23:08 devhl-labs

@devhl-labs It is not, since I have generated OpenAPI client with no particular base address to server in mind, having expected that users of that generated client will provide the base address to server by configuring HttpClient.BaseAddress. So in my case, the ClientUtils.CONTEXT_PATH is just const "", and the actual subpath of the configured HttpClient.BaseAddress is ignored.

Dragemil avatar Aug 27 '24 11:08 Dragemil

This is still an issue on openapi-generator version 7.12.0 using the generichost output

I am also not able to rely on servers in the OAS and I ran into this where the API I am consuming has multiple environments, but some of those environments have no subpath, so the solution with CONTEXT_PATH would not work and break the others: e.g:

  • https://some-api-environment.com: Works now
  • https://environment.proxy.com/some-api: Doesn't work. If I change OpenAPI spec to make use of CONTEXT_PATH, it will break the other one.

I ended up solving this with a DelegatingHandler to rewrite the Uri, since I was already using one.

DI

builder.ConfigureApi((context, services, options) =>
        {
            // ...
            options.AddApiHttpClients(client =>
            {
                var config = context.Configuration.GetSection(AccountsConfiguration.SectionName).Get<AccountsConfiguration>()!;
                client.BaseAddress = new Uri(config.Domain); // Domain here is the full string "https://example.com/subpath"
            },
            builder =>
            {
                builder
                    .AddHttpMessageHandler<AccountsDelegatingHandler>()
            });
        });

And then the handler itself

public class AccountsDelegatingHandler(IOptions<AccountsConfiguration> options) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri?.AbsoluteUri.StartsWith(options.Value.Domain) == false) // Domain here is the full string "https://example.com/subpath"
        {
            request.RequestUri = new Uri(options.Value.Domain + request.RequestUri!.PathAndQuery);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

It seems like an easy change to be done here: generichost/api.mustache that I would be glad to contribute to, but I do not understand the reason why BaseAddress is disassembled into parts and then reassembled again.

BartoszUrban avatar Mar 14 '25 11:03 BartoszUrban

I do not understand the reason why BaseAddress is disassembled into parts and then reassembled again.

I will investigate but i vaguely remember an issue about the port not being set. If i remeber wrong and this can be fixed by simpler code that would be great.

devhl-labs avatar Mar 14 '25 11:03 devhl-labs

@Dragemil @BartoszUrban Please see if this works for you https://github.com/OpenAPITools/openapi-generator/pull/20896

devhl-labs avatar Mar 15 '25 19:03 devhl-labs

@devhl-labs, yes, thanks, works great!

BartoszUrban avatar Mar 15 '25 19:03 BartoszUrban

Looks good, thanks!

Dragemil avatar Mar 24 '25 08:03 Dragemil

Hello @Dragemil and @BartoszUrban , I would like to reopen this issue. The new solution causes a bug when you don't set a subpath. For example when we use the address http://localhost:8080/ with the client and have a path like /getFoo then string.Concat(HttpClient.BaseAddress.AbsolutePath, "{{{path}}}"); generates the path //getFoo.

The behaviour can be reproduced by executing the generated tests.

cavus700 avatar Apr 07 '25 08:04 cavus700

I am experiencing the same issue.
All my microservices have their api without a subpath.
I worked around that by patching every use with find src -name "*.cs" -type f -exec sed -i 's/HttpClient\.BaseAddress\.AbsolutePath/""/g' {} \; in my build script

Ekwav avatar Apr 09 '25 16:04 Ekwav

I will send a pr thanks for the reports

devhl-labs avatar Apr 09 '25 16:04 devhl-labs

Please check https://github.com/OpenAPITools/openapi-generator/pull/21081 @cavus700 @Ekwav @BartoszUrban

devhl-labs avatar Apr 12 '25 21:04 devhl-labs

@devhl-labs Thank you for the PR. For me it is working again.

There is one case, which still could cause some trouble:

var client = new HttpClient();
client.BaseAddress = new Uri("http://localhost/foo/");
var path = client.BaseAddress.AbsolutePath == "/"
    ? "/bar"
    : string.Concat(client.BaseAddress.AbsolutePath, "/bar");

The path in this example looks like this /foo//bar. It could be avoided by omitting the trailing slash in the base path but as a User this not obvious.

I would suggest a slightly different solution:

[Theory]
    [InlineData("http://localhost", "/bar")]
    [InlineData("http://localhost/", "/bar")]
    [InlineData("http://localhost/api", "/api/bar")]
    [InlineData("http://localhost/api/", "/api/bar")]
    public void Test(string baseAddress, string expected)
    {
        var client = new HttpClient();
        client.BaseAddress = new Uri(baseAddress);
        var actualPath =  string.Concat(client.BaseAddress.AbsolutePath.TrimEnd('/'), "/bar") ;
        Assert.Equal(expected, actualPath);
    }

cavus700 avatar Apr 14 '25 05:04 cavus700