I have an ASP.NET Core Web API (TargetFramework is net5.0) where I accept 2 media types:
application/jsonapplication/vnd.my.customtype+json
I have a basic controller as follows:
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("")]
public class RootController : ControllerBase
{
[HttpGet]
[HttpHead]
[Route("", Name = "get_root")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
public IActionResult Get()
{
return this.Ok(new { Item1 = "item 1" });
}
}
If I make a GET request with the header Accept: application/json and return a 200 OK with a result, everything is fine:
GET https://localhost:5001/ HTTP/1.1
Accept: application/json
Host: localhost:5001
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Content-Length: 18
{"item1":"item 1"}
If I make a GET request with the header Accept: application/vnd.my.customtype+json and return a 200 OK with a result, everything is also fine:
GET https://localhost:5001/ HTTP/1.1
Accept: application/vnd.my.customtype+json
Host: localhost:5001
HTTP/1.1 200 OK
Content-Type: application/vnd.my.customtype+json; charset=utf-8
Server: Kestrel
Content-Length: 18
{"item1":"item 1"}
The same controller returning a 404 (e.g. if a resource is not found)
[ApiController]
[ApiConventionType(typeof(DefaultApiConventions))]
[Route("")]
public class RootController : ControllerBase
{
[HttpGet]
[HttpHead]
[Route("", Name = "get_root")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
public IActionResult Get()
{
return this.NotFound();
}
}
Now, if I make a GET request with the header Accept: application/json, everything is fine (I get a proper 404 as well as ProblemDetails in the response body):
GET https://localhost:5001/ HTTP/1.1
Accept: application/json
Host: localhost:5001
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
Server: Kestrel
Content-Length: 161
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title":"Not Found",
"status":404,
"traceId":"00-54d50dfe6a27674ba83a73324fcf2721-d4286734f860c94a-00"
}
If I make a GET request with the header Accept: vnd.my.customtype+json, I get a 406 Not Acceptable response:
GET https://localhost:5001/ HTTP/1.1
Accept: application/vnd.my.customtype+json
Host: localhost:5001
HTTP/1.1 406 Not Acceptable
Server: Kestrel
Content-Length: 0
And in the logs, I get this:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET https://localhost:5001/ - -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI)'
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
Route matched with {action = "Get", controller = "Root"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Get() on controller Hooli.WebAPI.Controllers.RootController (Hooli.WebAPI).
warn: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
No output formatter was found for content types 'application/problem+json, application/problem+xml' to write the response.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
Executed action Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI) in 0.8058ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'Hooli.WebAPI.Controllers.RootController.Get (Hooli.WebAPI)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET https://localhost:5001/ - - - 406 0 - 1.1703ms
Note the warn line: No output formatter was found for content types 'application/problem+json, application/problem+xml' to write the response.
I cannot figure out why this will not return a proper 404 with problem details.
My Startup.cs is as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(
options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddNewtonsoftJson(
options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy(true, true)
};
});
services.Configure<MvcOptions>(
options =>
{
NewtonsoftJsonOutputFormatter newtonsoftJsonOutputFormatter =
options.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>().FirstOrDefault() ??
throw new ArgumentNullException(nameof(newtonsoftJsonOutputFormatter));
// add media types that we want to support by default
newtonsoftJsonOutputFormatter
.SupportedMediaTypes
.Add(MediaTypeHeaderValue.Parse("application/vnd.my.customtype+json"));
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
And just for completeness, my Program.cs file:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}
I have stripped my API down to this most basic setup to rule out any additional middleware / configurations. It doesn't matter what status code I return, if it is any of the 4xx responses I always get a 406 when the Accept header is the custom media type.
e.g.
- this.BadRequest();
- this.NotFound();
- this.Unauthorized();
- etc.
If I change options.ReturnHttpNotAcceptable = true to options.ReturnHttpNotAcceptable = false, the responses come back fine for everything, but then the API doesn't return a 406 when an invalid media type is passed in the Accept header.
How can I get the API to return the proper response when the custom media type is in the Accept header?