Search code examples
c#jqueryasp.net-mvcasp.net-web-apigetjson

405 error (Method Not Allowed) on Web API get


I'm having a frustrating problem when I try to make a call to my Web API controller method with $.getJSON : it always ends up with the following message being displayed in the console : "Failed to load resource: the server responded with a status of 405 (Method Not Allowed)"

Here's my controller :

using MyProject.Domain;
using MyProject.WebApp.Session;
using System;
using System.Linq;
using System.Web.Mvc;

namespace MyProject.WebApp.ApiControllers.Favorites
{
    public class FavoriteArticlesController : BaseController<FavoriteArticle, Guid>
    {
        [HttpGet]
        public object SetFavorite(Guid articleId, bool isFavorite)
        {
            try
            {
                if (isFavorite)
                {
                    var favorite = new FavoriteArticle
                    {
                        UserId = UserInfo.GetUserId(),
                        ArticleId = articleId
                    };
                    _repo.Upsert(favorite);
                }
                else
                {
                    var favorite = _repo.GetAll()
                        .First(fa => fa.ArticleId.CompareTo(articleId) == 0);
                    _repo.Delete(favorite.Id);
                }
                _repo.Commit();
                return new { Success = true, Error = (string)null };
            }
            catch (Exception ex)
            {
                return new { Success = false, Error = ex.Message };
            }
        }
    }
}

In case that is relevant in any way, BaseController naturally derives from ApiController. Here's the code if needed :

using MyProject.Data.Repository;
using MyProject.Data.Services;
using MyProject.Domain;
using System.Web.Http;

namespace MyProject.WebApp.ApiControllers
{
    public class BaseController<TEntity, TKey> : ApiController
        where TEntity : class, IEntity<TKey>, new()
    {
        protected UnitOfWork _unitOfWork;
        protected Repository<TEntity, TKey> _repo;

        protected BaseController()
        {
            _unitOfWork = new UnitOfWork();
            _repo = _unitOfWork.GetRepository<TEntity, TKey>();
        }
    }
}

And here's one of the functions that make the call :

$.fn.bindFavoriteArticle = function () {
    this.click(function () {
        var link = $(this);
        $.getJSON('/api/FavoriteArticles/SetFavorite', { ajax: true, articleId: link.attr('data-target-id'), isFavorite: true }, function (response) {
            if (response.Success === true) {
                link.children('i').removeClass('fa-heart-o')
                    .addClass('fa-heart');
                link.attr('data-toggle', 'unfavoriteArticle')
                    .unbind('click')
                    .bindUnfavoriteArticle();
            } else {
                // TODO : use bootstrap alert messages
                alert(response.Error);
            }
        });
    });
};

I saw here and there that the route configuration could be the source of the problem, so here's the content of RouteConfig.cs :

using System.Web.Mvc;
using System.Web.Routing;

namespace MyProject.WebApp
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "ApiDefault",
                url: "api/{controller}/{action}/{id}",
                defaults: new { controller = "SubscriptionsController", action = "GetSelectList", id = UrlParameter.Optional }
            );
        }
    }
}

Any idea of what's going on ? I feel like there's a lot I'm missing concerning how Web APIs work...


Solution

  • As mentioned in one of the other answers, the code configured the wrong routes. For web api configure the WebApiConfig

    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {
            // Web API routes
    
            //Enable Attribute routing is they are being used.
            config.MapHttpAttributeRoutes();
    
            //Convention based routes.
    
            //Matches GET /api/FavoriteArticles/SetFavorite
            config.Routes.MapHttpRoute(
                name: "DefaultActionApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            //Matches GET /api/FavoriteArticles
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
    

    Make sure that it is configured before MVC routes.

    protected void Application_Start() {
        // Pass a delegate to the Configure method.
        GlobalConfiguration.Configure(WebApiConfig.Register);
        //...other configurations
    }
    

    Some advice about refactoring the controller to be a little more restful. Try to return IHttpActionResult from actions. simplifies the framework and give you more control of how the response is returned.

    [RoutePrefix("api/favoritearticles"]
    public class FavoriteArticlesController : BaseController<FavoriteArticle, Guid> {
    
    
        [HttpPost]
        [Route("{articleId:guid}")] //Matches POST api/favoritearticles/{articleId:guid}
        public IHttpActionResult SetFavorite(Guid articleId) {            
            var favorite = new FavoriteArticle
            {
                UserId = UserInfo.GetUserId(),
                ArticleId = articleId
            };
            _repo.Upsert(favorite);
            _repo.Commit();
            return Ok(new { Success = true, Error = (string)null });
        }
    
        [HttpDelete]
        [Route("{articleId:guid}")] //Matches DELETE api/favoritearticles/{articleId:guid}
        public IHttpActionResult RemoveFavorite(Guid articleId) {            
            var favorite = _repo.GetAll()
                .First(fa => fa.ArticleId == articleId);
    
            if(favorite == null) return NotFound();
    
            _repo.Delete(favorite.Id);
            _repo.Commit();
            return Ok(new { Success = true, Error = (string)null });
        }
    }
    

    Controllers should be as lean as possible so even the above should be slimmed down even more via an injected service into the controller.

    Error handling is a cross-cutting concern and should also be extracted and handled via the framework's extensibility points.