A SerializationException is being thrown from
System.Web.OData.Formatter.ODataMediaTypeFormatter.GetSerializer(...) when
SingleResult.Create(...) takes in a query that returns an empty result.
This happens when an invalid key is specified by the user. A 404 Not Found
should be returned instead of a 500 Internal Server Error.
This issue is related to:
https://aspnetwebstack.codeplex.com/workitem/1021
https://aspnetwebstack.codeplex.com/workitem/1040
** To Reproduce **
[EnableQuery]
public SingleResult<Product> GetProduct([FromODataUri] int key)
{
return SingleResult.Create(Enumerable.Empty<Product>().AsQueryable());
}
** Exception Details **
{
"message": "An error has occurred.",
"innererror": {
"message": "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata.metadata=minimal'.",
"type": "System.InvalidOperationException",
"internalexception": {
"message": "'SingleResult`1' cannot be serialized using the ODataMediaTypeFormatter.",
"type": "System.Runtime.Serialization.SerializationException",
"stacktrace": " at System.Web.OData.Formatter.ODataMediaTypeFormatter.GetSerializer(Type type, Object value, IEdmModel model, ODataSerializerProvider serializerProvider)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()"
}
}
}
** Workaround **
public class NullSerializerProvider : DefaultODataSerializerProvider
{
private readonly NullEntityTypeSerializer _nullEntityTypeSerializer;
public NullSerializerProvider()
{
_nullEntityTypeSerializer = new NullEntityTypeSerializer(this);
}
public override ODataSerializer GetODataPayloadSerializer(IEdmModel model, Type type, HttpRequestMessage request)
{
var serializer = base.GetODataPayloadSerializer(model, type, request);
if (serializer == null)
{
var response = request.GetOwinContext().Response;
response.OnSendingHeaders(state =>
{
((IOwinResponse)state).StatusCode = (int)HttpStatusCode.NotFound;
}, response);
return _nullEntityTypeSerializer;
}
return serializer;
}
}
public class NullEntityTypeSerializer : ODataEntityTypeSerializer
{
public NullEntityTypeSerializer(ODataSerializerProvider serializerProvider)
: base(serializerProvider)
{ }
public override void WriteObjectInline(object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
{
if (graph != null)
{
base.WriteObjectInline(graph, expectedType, writer, writeContext);
}
}
}
var odataFormatters = ODataMediaTypeFormatters.Create(new NullSerializerProvider(), new DefaultODataDeserializerProvider());
config.Formatters.InsertRange(0, odataFormatters);
Original CodePlex Issue: Issue 2190
Status: Proposed
Reason Closed: Unassigned
Assigned to: lianw
Reported on: Nov 20, 2014 at 3:00 PM
Reported by: ryansimmen
Updated on: Tue at 2:28 PM
Updated by: timmhagen
On _2014-12-11 17:46:11 UTC_, cysu commented:
Assign to Liang for an investigation.
On _2015-01-06 04:28:54 UTC_, timmhagen commented:
I have attempted to use the workaround, while I don't get a 500 anymore, I now get 200 instead of a 404. The HttpStatusCode.OK is hard coded in the System.Web.Http.Controllers.ValueResultConverter class during the return in Convert(). Do you have a modification
to the work around to avoid a 200 and instead return a 404?
On _2015-01-06 04:30:27 UTC_, timmhagen commented:
This line to be exact....
return controllerContext.Request.CreateResponse
On _2015-01-06 05:51:47 UTC_, ryansimmen commented:
Yes, I updated the issue for you. See the modification I made to GetODataPayloadSerializer(...)
On _2015-01-06 22:28:37 UTC_, timmhagen commented:
Thanks that worked.
For completeness / users using this Workaround:
if you are using Microsoft.OData.Client at client side, you have also to configure it for handling 404:
C#
container.IgnoreResourceNotFoundException = true;
An old blog about it: http://blogs.msdn.com/b/peter_qian/archive/2009/03/20/safe-resource-not-found-404-exception-handling-in-ado-net-data-service-client.aspx
One issue with the workaround is, that for odata function calls we also get a 404 because
base.GetODataPayloadSerializer(model, type, request) is null too in this case. Here is my version of dealing with this issue:
``` c#
public override ODataSerializer GetODataPayloadSerializer(IEdmModel model, Type type, HttpRequestMessage request)
{
var serializer = base.GetODataPayloadSerializer(model, type, request);
if (serializer == null)
{
// get the defined functions
var functions = model.SchemaElements.Where(s => s.SchemaElementKind == EdmSchemaElementKind.Function
|| s.SchemaElementKind == EdmSchemaElementKind.Action);
var isFunctionCall = false;
// Do we have a function call in the request
foreach (var f in functions)
{
var fname = string.Format("{0}.{1}", f.Namespace, f.Name);
if (request.RequestUri.OriginalString.Contains(fname))
{
isFunctionCall = true;
break;
}
}
// only, if it is not a function call
if (!isFunctionCall)
{
var response = request.GetOwinContext().Response;
response.OnSendingHeaders(state =>
{
((IOwinResponse)state).StatusCode = (int)HttpStatusCode.NotFound;
}, response);
}
return _nullEntityTypeSerializer;
}
return serializer;
}
```
@devdorn Thanks for your solution, would you help to write a github page for us about your work around?
Send a pull request in https://github.com/OData/WebApi/tree/gh-pages/_posts
It will post in http://odata.github.io/WebApi/
Wonderful code! where do I put it? I added to WebApiConfig and it did not like being in a static class
@Ofer-Gal There is a page for this http://odata.github.io/WebApi/#10-02-work-around-for-singleresult-create, check that
var odataFormatters = ODataMediaTypeFormatters.Create(new NullSerializerProvider(), new DefaultODataDeserializerProvider());
config.Formatters.InsertRange(0, odataFormatters);
Thank! I am a step closer but I still have a problem when the key is not found.
In the line var response = request.GetOwinContext().Response; in the GetODataPayloadSerializer class.
the value of request.GetOwinContext() is null so the status "Not found" can't be sent.
Can this be because I use All System.Web.OData.... rather than System.Web.Http.OData.... ?
No, there is no problem with v4 that you are using, you may need to go check why this is null or provide more detail :) http://stackoverflow.com/questions/22598567/asp-net-webapi-cant-find-request-getowincontext
having a similar issue as @Ofer-Gal. the request.GetOwinContext() is returning null, due to the request.Properties not containing a "MS_OwinContext" key.
Figured out my issue, at least. needed to add app.UseWebApi(GlobalConfiguration.Configuration); to the Startup.cs to make OWIN handle the webAPI calls. works like a charm now.
Can you send some more? like which method did you add it to etc..
I've just hit the same issue. Is the workaround the definitive solution ?
It doesn't feel right that a serializer ends up setting a status code.
I'm also a bit confused because https://aspnetwebstack.codeplex.com/workitem/1040 clearly states the bug as fixed. Is this a regression ?
Edit: after some tinkering and I found what feels like a more appropriate solution.
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "api/v1",
model: builder.GetEdmModel(),
defaultHandler: new ODataNullValueMessageHandler() {InnerHandler = new HttpControllerDispatcher(config)});
System.Web.OData.ODataNullValueMessageHandler is a HttpMessageHandler and is apparently made specifically for this purpose. It will set the status code to an appropriate value depending on the type of route and the type of response.
My initial tests show it works but I cannot guarantee there isn't any corner cases not covered.
I too bumped into this. Kinda bothersome since the SingleResult documentation states "Represents an System.Linq.IQueryable containing zero or one entities". Seems happy enough with @timoch's suggested response, and seems to work fine enough for now, though I second the call for some more official response.
@andrensairr There is a page for this http://odata.github.io/WebApi/#10-02-work-around-for-singleresult-create
Thanks @VikingsFan, works a treat.
for me, the request doesn't have a method GetOwinContext(). Am I missing an assembly? I have added Microsoft.Owin
Ok, I have added the correct assembly, however it doesn't look like the code with the NotFound is getting executed, I am getting a 200 response with the word null. Does anyone have any idea?
Thank you
I bumped into this issue as well.
@VikingsFan Can you please also add the following where you have request.GetOwinContext().Response?
//// in case you are NOT using Owin, uncomment the following and comment everything above
// HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
My project wasn't using Owin and I was building OData prototype.
Updated Thanks, you can give us a PR next time ;)
This doesn't seem to be fixed in OData 4 though it works as expected in OData 3 - surely this should be in the core V4 code by now, having to add your own serializer seems a bit odd
The posted fix works, but should be in core...
Yeah why has this not been rolled into the standard package ??
also to use the workaround do we need to modify the odata source code and re-build or cann we add this to a project ??
I am not sure how we are supposed to apply this fix ?
@figuerres The solution posted by timoch seems the best, as its just part of your OData configuration in the project
Not I would recommend modifying the OnSendingHeaders to contain:
IOwinResponse owinResponse = (IOwinResponse)state;
owinResponse.StatusCode = (int)HttpStatusCode.NotFound;
owinResponse.ReasonPhrase = "Not Found";
The original referenced code returns a 404 error but a reason phrase of OK.
Verified this is not an issue any more on WebApi/master branch.
This issue can be closed.
So slow, I don't even use OData anymore :D
@dr-noise sorry that you feel it that way. Apparently this issue just came back to our radar screen during recent triage. If you have any other similar issues related to WebApi, please ping us via github to trigger proper triage.
Most helpful comment
This doesn't seem to be fixed in OData 4 though it works as expected in OData 3 - surely this should be in the core V4 code by now, having to add your own serializer seems a bit odd