OData/AspNetCoreOData

getting 405 when trying to $batch

Opened this issue · 18 comments

$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
Invoke-WebRequest -UseBasicParsing -Uri "http://localhost:5021/odata/`$batch" -Method "POST"
-WebSession $session -Headers @{ "Accept"="application/json, text/plain, */*" "Accept-Encoding"="gzip, deflate, br, zstd" "Accept-Language"="en-US,en;q=0.9" "Origin"="http://localhost:4200" "Referer"="http://localhost:4200/" "Sec-Fetch-Dest"="empty" "Sec-Fetch-Mode"="cors" "Sec-Fetch-Site"="same-site" "sec-ch-ua"=""Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"" "sec-ch-ua-mobile"="?0" "sec-ch-ua-platform"=""Windows"" }
-ContentType "multipart/mixed; boundary=----MyCoolBatchBoundary" `
-Body ([System.Text.Encoding]::UTF8.GetBytes("------MyCoolBatchBoundary$([char]13)$([char]10)Content-Type: application/http; msg-http=GET /odata/Customers$([char]13)$([char]10)$([char]13)$([char]10)------MyCoolBatchBoundary$([char]13)$([char]10)Content-Type: application/http; msg-http=GET /odata/Features$([char]13)$([char]10)$([char]13)$([char]10)------MyCoolBatchBoundary--$([char]13)$([char]10)"))

The URI you are passing there, seems to have an extra character in it. Can you double check?
http://localhost:5021/odata/`$batch

should be
http://localhost:5021/odata/$batch

sorry, that is just a remnant of copy to curl from devtools ... assured that i am calling http://localhost:5021/odata/$batch ...
i have set up CORS to allow any method, and pre-flight seems ok ... but strangely the call to $batch has only Allow: GET in the response headers

here is my Program.cs

// Program.cs
using gofer.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Batch;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntityType<Comment>();
modelBuilder.EntityType<Order>();
modelBuilder.EntitySet<Customer>("Customers");
modelBuilder.EntitySet<Feature>("Features");

builder.Services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null)
              .AddRouteComponents("odata", modelBuilder.GetEdmModel()));

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors("AllowAll");

app.UseODataRouteDebug();
app.UseODataQueryRequest();
app.UseODataBatching();

app.UseRouting();
app.UseEndpoints( endpoints => endpoints.MapControllers() );

app.Run();

// using the default model from the modelbuilder
static IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder();
    builder.EntitySet<Customer>("Customers");
    builder.EntitySet<Feature>("Features");
    return builder.GetEdmModel();
}

and a contoller:

using gofer.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace gofer.Controllers
{
    public class FeaturesController : ODataController
    {
        private static List<Feature> features= new List<Feature>([
            new Feature{ Id = 111111,Name = "Food Stuffs"},
            new Feature{ Id = 111112,Name = "Garage Shelving"},
            new Feature{ Id = 111113,Name = "Quantum Decoherence"},
            new Feature{ Id = 111114,Name = "Outboard Motors"},
            new Feature{ Id = 111115,Name = "Cancellations"},
            new Feature{ Id = 111116,Name = "Remunerations"},
            new Feature{ Id = 111117,Name = "Feedback Focus"}
            ]);

        [EnableQuery]
        public ActionResult<IEnumerable<Feature>> Get()
        {
            return Ok(features);
        }

        [EnableQuery]
        public ActionResult<Feature> Get([FromRoute] int key)
        {
            var item = features.SingleOrDefault(d => d.Id.Equals(key));

            if (item == null)
            {
                return NotFound();
            }

            return Ok(item);
        }
    }
}

and here is the curl from postman:

curl --location 'http://localhost:5021/odata/$batch' \
--header 'Content-Type: multipart/mixed; boundary=----MyCoolBatchBoundary' \
--header 'Accept: application/json, text/plain, */*' \
--header 'Accept-Encoding: gzip, deflate, br, zstd' \
--data '------MyCoolBatchBoundary
Content-Type: application/http; msg-http=GET /odata/Customers

------MyCoolBatchBoundary
Content-Type: application/http; msg-http=GET /odata/Features

------MyCoolBatchBoundary--'

app.UseODataRouteDebug();
app.UseODataQueryRequest();
app.UseODataBatching();

app.UseRouting();
app.UseEndpoints( endpoints => endpoints.MapControllers() );

Can you try moving your calls to after UseRouting and see if that has any effect?

app.UseRouting();

app.UseODataRouteDebug();
app.UseODataQueryRequest();
app.UseODataBatching();

app.UseEndpoints( endpoints => endpoints.MapControllers() );

EDIT:
Ignore my suggestion. The docs explicitly ask one to add the middleware before the routing middleware here:

Batch middlware should add before app.UseRouting();

When you try to make your individual requests directly, do they all work?

Can you also try using JSON batch format for your request and see if that makes any difference?
https://learn.microsoft.com/en-us/odata/odatalib/json-batch

i gave it a try but same 405 ...
odatamiddleware,md in the github repo has them all before, thats why i had them before

yes, ../odata/$metadata ../odata/Features and .../odata/Customers all working fine

oh cool i didnt yet know about json batching ... i will give that a try

get 405 with json batch ... i think the problem is in server configuration ... why it is returning 405 and only Allow GET

test POST to a controller works fine:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using gofer.Models;

namespace gofer.Controllers
{
    public class TestController : ODataController
    {
        private static List<Test> tests = new List<Test>([ new Test { Id = 10001, Text = "Barney Rubble" } ]);

        public ActionResult<IEnumerable<Test>> Post()
        {
            return Ok(tests);
        }
    }
}

404 ...
expected, no ? :)

@jeffschulzusc could you create a small sample repo and post the link here?

@jeffschulzusc apparently, you have to explicitly provide a batch handler. Just calling UseODataBatching is not enough.

On your AddRouteComponents call, try this:

-    .AddRouteComponents("odata", modelBuilder.GetEdmModel()));
+    .AddRouteComponents("odata", modelBuilder.GetEdmModel(), new DefaultODataBatchHandler()));

Then you should be able to POST data into http://localhost:5021/odata/$batch

Hope this helps.


@xuzhg @habbes I think this user experience is really not great here... at the very least, if the middleware detects no batch handler is present, it should just throw some exception instead of just returning method now allowed. This is not intuitive at all to fix.

Additionally, the fact that the $batch endpoint doesn't show up in the $odata route debugger makes this even worse. We should fix that IMHO. It is an OData route anyways, so it should definitely be showing up there.

ah thanks, i added that ... my json batch is almost working now:

curl --location 'http://localhost:5021/odata/$batch' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
  "requests": [
    {
      "id": "json-batch-test-get-features-1",
      "method": "GET",
      "url": "http://localhost:5021/odata/Features",
      "headers": {
        "content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
        "odata-version": "4.0"
      }
    }
  ]
}'

but 404:

{
    "responses": [
        {
            "id": "json-batch-test-get-features-1",
            "status": 404,
            "headers": {}
        }
    ]
}

from
https://learn.microsoft.com/en-us/odata/odatalib/json-batch
https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc499716905

i don't really know what i should have for id and atomicityGroup ...
i will check the sample code in the repo see if that clarifies things