Search code examples
c#reactjsasp.net-core-webapiasp.net-core-8

Why does [FromBody] cause CORS and AmbiguousMatchException exceptions between React and ASP.NET Core 8 Web API?


I have a web app that has React as front-end and .NET core 8 as back-end. The app has a navigation with Meals button. Upon clicking it a sub-navigation with two more buttons appears. One of them is All meals and the other one is Add meal. Everything was working perfectly fine with both until I decided that the front-end should send the current date when fetching for all the meals. When I then added in the back-end controller [FromBody] string date it started throwing two errors that have not been thrown and it would not get fixed until I remove the [FromBody] string date and rebuild the project.

Here are the errors, first is from the back-end and the second is from the front-end: 1 - `Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request. Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:

  Fitness_Tracker.Controllers.MealController.AllMeals (Fitness-Tracker)
  Fitness_Tracker.Controllers.MealController.AllMeals (Fitness-Tracker)
     at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(Span1 candidateState)
     at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, Span1 candidateState)
     at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.Select(HttpContext httpContext, Span1 candidateState)
     at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
     at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
     at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)`

2- Access to fetch at 'https://localhost:7009/api/meal/all' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

import React, { useEffect, useState } from 'react';
import '../../css/AllMealsPage.css';

const AllMealsPage = () => {
    const [meals, setMeals] = useState([]);
    const [calories, setCalories] = useState(0);
    const [errorMessage, setErrorMessage] = useState('');
    const [selectedDate, setSelectedDate] = useState(new Date());

    useEffect(() => {
        const fetchMeals = async () => {
            try {
                const response = await fetch('https://localhost:7009/api/meal/all', {
                    method: 'POST',
                    credentials: 'include',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ date: selectedDate.toISOString() })
                });

                if (!response.ok) {
                    throw new Error('Displaying meals failed');
                }

                const data = await response.json();
                setMeals(data);
                setErrorMessage('');
            } catch (err) {
                setErrorMessage(err.message);
            }
        };

        fetchMeals();
    }, [selectedDate]);

    useEffect(() => {
        const fetchCalories = async () => {
            try {
                const response = await fetch('https://localhost:7009/api/meal/calories', {
                    method: 'POST',
                    credentials: 'include'
                });

                if (!response.ok) {
                    throw new Error('Displaying calories failed');
                }

                const data = await response.json();
                setCalories(data);
                setErrorMessage('');
            } catch (err) {
                setErrorMessage(err.message);
            }
        };

        fetchCalories();
    }, [selectedDate]);

    const handlePreviousDay = () => {
        setSelectedDate(prevDate => {
            const newDate = new Date(prevDate);
            newDate.setDate(prevDate.getDate() - 1);
            return newDate;
        });
    };

    const handleNextDay = () => {
        setSelectedDate(prevDate => {
            const newDate = new Date(prevDate);
            const today = new Date();
            if (newDate.toDateString() !== today.toDateString()) {
                newDate.setDate(prevDate.getDate() + 1);
            }
            return newDate;
        });
    };

    return (
        <div className="all-meals-container">
            {errorMessage && <p className="error-message">{errorMessage}</p>}

            <div className="date-navigation">
                <button onClick={handlePreviousDay}>←</button>
                <span>{selectedDate.toDateString()}</span>
                <button onClick={handleNextDay} disabled={selectedDate.toDateString() === new Date().toDateString()}>→</button>
            </div>

            <div className="table-wrapper">
                <table className="meals-table">
                    <thead>
                        <tr>
                            <th>Meal Name</th>
                            <th>Meal of the Day</th>
                            <th>Calories</th>
                        </tr>
                    </thead>
                    <tbody>
                        {meals.map((meal) => (
                            <tr key={meal.id}>
                                <td>{meal.name}</td>
                                <td>{MealOfTheDayLabel(meal.mealOfTheDay)}</td>
                                <td>{meal.calories}</td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>

            <h1>Total calories: {calories}</h1>
        </div>
    );
};

const MealOfTheDayLabel = (mealOfTheDay) => {
    switch (mealOfTheDay) {
        case 0:
            return 'Breakfast';
        case 1:
            return 'Lunch';
        case 2:
            return 'Dinner';
        case 3:
            return 'Snack';
        default:
            return 'Unknown';
    }
};

export default AllMealsPage;

And here is my back-end controller(when not working):

namespace Fitness_Tracker.Controllers
{
    using Fitness_Tracker.Data.Models;
    using Fitness_Tracker.Models.Meals;
    using Fitness_Tracker.Services.Meals;
    using Microsoft.AspNetCore.Mvc;
    using System.Security.Claims;

    public class MealController : BaseApiController
    {
        private readonly IMealService _mealService;

        public MealController(IMealService mealService)
        {
            this._mealService = mealService;
        }

        [HttpPost("add")]
        public async Task<IActionResult> AddMeal([FromBody] AddMealModel model)
        {
            if(!ModelState.IsValid)
            {
                return BadRequest();
            }

            string userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            if(userId == null) { 
                return BadRequest();
            }

            await _mealService.CreateMealAsync(userId, model);

            return Ok();
        }

        [HttpPost("all")]
        public async Task<IActionResult> AllMeals(string date)// Remove [FromBody] string date and it works
        {
            string userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            if(userId == null)
            {
                return BadRequest();
            }

            List<Meal> result = await _mealService.GetAllUserMealsAsync(userId);

            return Ok(result);
        }

        [HttpPost("calories")]
        public async Task<IActionResult> AllMealsCalories()
        {
            string userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

            if (userId == null)
            {
                return BadRequest();
            }

            int result = await _mealService.GetTotalUserMealCaloriesAsync(userId);

            return Ok(result);
        }
    }
}

I played around with it and basically whatever I add to catch as a parameter, such as [FromQuery] string date, simply string date and etc. - the errors occur. I can't seem to figure out what could the issue be, since the errors I receive have nothing to do with the actual issue, because otherwise everything is perfectly fine.

I expected to simply do [FromBody] string date and catch the date being sent from the front-end, but it went into nightmare mode.


Solution

  • I recommend creating a model for the All action like this:

    [HttpPost("all")]
    public async Task<IActionResult> AllMeals([FromBody] AllMealsRequest allMealsRequest)
    {
      [...]
    }
    
    // With a class similar to this: 
    public class AllMealsRequest
    {
       public string Date { get; set; }
    }
    

    The reason for this is that json has become defacto standard, and the api by default expects a json when you send a payload. Making an object just makes it play nice together with the api. Furthermore, it is more easy at a later time to add properties to the request body without breaking the api in the future.

    Elaborating a bit more, if it is a purely get information api call, you should change your api to be HttpGet with a FromQuery attribute on the parameter. When you call the endpoint you should instead use a query parameter in the url such as this: https://localhost:7009:/api/meal/all?date=[your date here]

    // https://localhost:7009:/api/meal/all?date=[your date here] 
    [HttpGet("all")]
    public async Task<IActionResult> AllMeals([FromQuery] string date)
    {
         [...]
    }