reactiveui / refit

The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.

Home Page:https://reactiveui.github.io/refit/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug]: POST request sent as GET

FelixZY opened this issue · comments

Describe the bug 🐞

We have an azure app service deployment where refit is used to communicate between two app services.

The same code was used to build and deploy to development, stage and production. Everything worked fine in development and stage but in production, Refit would appear to have sent a http GET rather than a http POST. I tried to rebuild and redeploy to our production environment, but the problem remained.

IService2Api.cs
using Refit;

public interface IService2Api
{
    [Post($"/my/endpoint/")]
    Task<IApiResponse<PartialResultDto<CustomId, CustomId>>> MyEndpointAsync(
        [Header("Authorization")] string authorization,
        [Body] IEnumerable<string> strings,
        CancellationToken cancellationToken
    );
}
AddRefitClient<IService2Api>
services
    .AddRefitClient<IService2Api>(new RefitSettings { CollectionFormat = CollectionFormat.Multi })
    .ConfigureHttpClient(
        (sp, client) =>
        {
            client.DefaultRequestHeaders.TryAddWithoutValidation(
                "User-Agent",
                $"Service1 (Service1/1.0.1.0)"
            );
            client.BaseAddress = new Uri("http://service2.azurewebsites.net");
            client.Timeout = TimeSpan.FromSeconds(10);
        }
    );
Service1 logs
...
2023-11-23 20:25:54.929 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.LogicalHandler: Start processing HTTP request POST http://service2.azurewebsites.net/my/endpoint/
2023-11-23 20:25:54.929 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.LogicalHandler: Request Headers:
Authorization: Bearer [Redacted]
User-Agent: Service1, (Service1/1.0.1.0)
Content-Type: application/json; charset=utf-8

2023-11-23 20:25:54.929 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Sending HTTP request POST http://service2.azurewebsites.net/my/endpoint/
2023-11-23 20:25:54.929 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Request Headers:
Authorization: Bearer [Redacted]
User-Agent: Service1, (Service1/1.0.1.0)
Content-Type: application/json; charset=utf-8

2023-11-23 20:25:54.972 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Received HTTP response headers after 43.1596ms - 405
2023-11-23 20:25:54.972 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Response Headers:
Date: Thu, 23 Nov 2023 20:25:54 GMT
Content-Length: 0
Allow: POST

2023-11-23 20:25:54.972 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.LogicalHandler: End processing HTTP request after 43.5574ms - 405
2023-11-23 20:25:54.972 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+[Redacted]IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.LogicalHandler: Response Headers:
Date: Thu, 23 Nov 2023 20:25:54 GMT
Content-Length: 0
Allow: POST
...
2023-11-23 20:25:55.030 +00:00 [Error] Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer: Connection ID "[Redacted]", Request ID "[Redacted]": An unhandled exception was thrown by the application.
Refit.ApiException: Response status code does not indicate success: 405 (Method Not Allowed).
...
Service2 logs
...
2023-11-23 20:25:54.959 +00:00 [Information] Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Request:
Protocol: HTTP/1.1
Method: GET
Scheme: https
PathBase: 
Path: /my/endpoint/
Cookie: [Redacted]
Host: service2.azurewebsites.net
Max-Forwards: 10
User-Agent: Service1 (Service1/1.0.1.0)
...

Step to reproduce

var apiResult = await service2Api.MyEndpointAsync(
    $"Bearer {accessToken}",
    strings,
    cancellationToken
);
if (!apiResult.IsSuccessStatusCode)
{
    throw apiResult.Error;
}

Reproduction repository

https://github.com/reactiveui/refit

Expected behavior

Refit reporting POST should result in a POST call.

Screenshots 🖼️

No response

IDE

No response

Operating system

Windows

Version

Dotnet - v7.0

Device

Azure App Service

Refit Version

7.0.0

Additional information ℹ️

I'm frankly stumped for now. If you have any ideas what might cause Refit to report POST but actually perform GET, I would be most interested.

This is almost certainly an Azure problem, Refit does not detect development vs production or Debug vs Release, it does the same thing in every environment. I would try to make a DelegatingHttpHandler and log the kind of request being sent down, but it'll probably report that it's sending a POST

Thank you for the suggestion. I wrapped things like this:

AddRefitClient call
services
    .AddRefitClient<IService2Api>(
        (sp) =>
            new RefitSettings
            {
                CollectionFormat = CollectionFormat.Multi,
                HttpMessageHandlerFactory = () =>
                    new LoggingHandler(
                        sp.GetRequiredService<ILoggerFactory>()
                            .CreateLogger<LoggingHandler>(),
                        new HttpClientHandler() { AllowAutoRedirect = true }
                    )
            }
    )
    .ConfigureHttpClient(
        // ...
LoggingHandler
private class LoggingHandler : DelegatingHandler
{
    private readonly ILogger<LoggingHandler> logger;

    public LoggingHandler(ILogger<LoggingHandler> logger, HttpMessageHandler innerHandler)
        : base(innerHandler)
    {
        this.logger = logger;
    }

    protected override HttpResponseMessage Send(
        HttpRequestMessage request,
        CancellationToken cancellationToken
    )
    {
        logger.LogDebug("Before [{Method}] to {Url}", request.Method, request.RequestUri);
        var response = base.Send(request, cancellationToken);
        logger.LogDebug("After [{Method}] to {Url}", request.Method, request.RequestUri);
        return response;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken
    )
    {
        logger.LogDebug("Before [{Method}] to {Url}", request.Method, request.RequestUri);
        var response = await base.SendAsync(request, cancellationToken);
        logger.LogDebug("After [{Method}] to {Url}", request.Method, request.RequestUri);
        return response;
    }
}

Which resulted in this output:

2023-11-24 23:34:11.315 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Sending HTTP request POST http://service2.azurewebsites.net/my/endpoint/
2023-11-24 23:34:11.315 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Request Headers:
Authorization: Bearer [Redacted]
User-Agent: Service1, (Service1/1.0.1.0)
Content-Type: application/json; charset=utf-8

2023-11-24 23:34:11.315 +00:00 [Debug] LoggingHandler: Before [POST] to http://service2.azurewebsites.net/my/endpoint/
2023-11-24 23:34:11.400 +00:00 [Debug] LoggingHandler: After [GET] to https://service2.azurewebsites.net/my/endpoint/
2023-11-24 23:34:11.401 +00:00 [Information] System.Net.Http.HttpClient.Refit.Implementation.Generated+IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Received HTTP response headers after 85.2956ms - 405
2023-11-24 23:34:11.401 +00:00 [Trace] System.Net.Http.HttpClient.Refit.Implementation.Generated+IService2Api, Common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null.ClientHandler: Response Headers:
Date: Fri, 24 Nov 2023 23:34:11 GMT
Set-Cookie: [Redacted]
Content-Length: 0
Allow: POST

From this we can see that the request was indeed altered. However, this seems to have happened inside the innermost HttpMessageHandler which I assume is deeper than Refit is supposed to operate?

Any ideas on what may have gone wrong or what might be worth testing are still most welcome! I will continue my own debugging next week.


Aside: It would be nice if refit could include a trace print of the http method as it is listed after calling base.Send, similar to what I did, to make similar issues easier to debug in the future:

var response = base.Send(request, cancellationToken);
logger.LogDebug("After [{Method}] to {Url}", request.Method, request.RequestUri);

Any ideas on what may have gone wrong or what might be worth testing are still most welcome! I will continue my own debugging next week.

I'm not super familiar with Azure app development, sorry :( There are several issues on the tracker similar to this which is why I brought it up, searching the closed issues might give you some hints

I looked over other issues but didn't really see anything similar - do you have examples @anaisbetts ?

Currently, I'm not willing to lock this down to Azure specifically - we do have an identical environment where this is working fine. Rather, I'm currently interested in "what circumstances would cause .NET to convert a POST request to a GET request internally?".

I created https://stackoverflow.com/questions/77548462/what-could-cause-a-post-request-to-be-sent-as-a-get-request-in-c-asp-net-core-7

Stack Overflow
I have two ASP.NET core 7 applications deployed on Azure app service. For certain requests, service1 needs to make a POST request on a private vnet to service2. This works fine in our staging envir...

In ConfigureHttpClient, try disabling redirects. (Sorry, I forget exactly how, but there's a property on the client handler you set to false. Maybe AllowAutoRedirect?)

Then log the response status and headers.

I think you have a 301/302 redirect in the mix somehow and the default behaviour of the http client is to automatically follow it.

Maybe you've configured your base URL in one environment to start with http:// and the server is configured to redirect to https://?

@FelixZY They weren't specifically similar in the "POST turns into GET" but more general like, "My app works locally but not on Azure" - they always figured out that something in the Azure environment was messing with them :-/

I have now had time to test and verify. Indeed, service1 would encounter a 301 redirect from service2, likely related to upgrading from http to https. This would result in the POST being converted into a GET.

Relevant sections from RFC 2616, section 10.3.2 (credit to Poul Bak on Stack Overflow):

If the 301 status code is received in response to a request other
than GET or HEAD, the user agent MUST NOT automatically redirect the
request unless it can be confirmed by the user, since this might
change the conditions under which the request was issued.

Note: When automatically redirecting a POST request after
receiving a 301 status code, some existing HTTP/1.0 user agents
will erroneously change it into a GET request.

https://datatracker.ietf.org/doc/html/rfc2616#section-10.3.2

IETF Datatracker
HTTP has been in use by the World-Wide Web global information initiative since 1990. This specification defines the protocol referred to as "HTTP/1.1", and is an update to RFC 2068. [STANDARDS-TRACK]

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.