Streaming Exceptions from ADO.NET Data Services

In the last post, I discussed what happened with regular exceptions, "unrecoverable" exceptions and WebDataServiceException.

In this post I'll tackle another aspect. The service will stream results, meaning that we don't require the data source to materialize all results at once.

This is an efficient way of doing things, but opens the door (wider) for problems taking place while we write out the response for a URL request.

In my last post, all errors were detected before we started to write out the response body. Bacause the response headers get written all out once right before starting on the body, we always had the chance to change the response status code and choose to write a different response body.

It's possible, however, that we run into problems while we're sending the results back. Unfortunately at that point we've already sent a 200 - OK response, so the strategy we take is to try to "break" the response and include error information.

For example, building on the example from the last post, let's say we have the following service operation.

[WebGet]
public IEnumerable<Model.Customers> CustomersThatThrow()
{
int count = 0;
foreach (var c in this.CurrentDataSource.Customers)
{
yield return c;
if (count++ == 2)
{
throw new InvalidOperationException(
"Something happened - imagine a database connection was dropped.");
}
}

}

For these error conditions, I like to use simple telnet. You'll need to use the port that the debugging server picks for you and have the service running before you can try this out.

From a console, run this command (assuming your server is listening on port 65314 - change as appropriate): telnet 127.0.0.1 65314

Then paste this, including a trailing empty line. Echo is turned off by default in telnet, so you won't see what you type or paste.
GET /WebDataService1.svc/CustomersThatThrow HTTP/1.0

The response looks something like this (reformatted for readability):

HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Wed, 26 Dec 2007 23:49:45 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: no-cache
Content-Type: application/atom+xml
Content-Length: 4259
Connection: Close
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xml:base="
https://localhost:65314/WebDataService1.svc/ " xmlns:ads=" https://schemas.microsoft.com/ado/2007/08/dataweb " xmlns:adsm=" https://schemas.microsoft.com/ado/2007/08/dataweb/metadata " xmlns="
https://www.w3.org/2005/Atom ">
<id>https://localhost:65314/WebDataService1.svc/CustomersThatThrow</id>
<updated />
<title>CustomersThatThrow</title>
<link rel="self" href="CustomersThatThrow" title="CustomersThatThrow" />
<entry adsm:type="Model.Customers">
<id>https://localhost:65314/WebDataService1.svc/Customers('ALFKI')</id>
...
</entry>
<entry adsm:type="Model.Customers">
<id>https://localhost:65314/WebDataService1.svc/Customers('ANATR')</id>
...
</entry>
<entry adsm:type="Model.Customers">
...
</entry>
<Exception xmlns="
https://schemas.microsoft.com/ado/2007/08/dataweb/exception ">
Something happend - imagine a database connection was dropped.&#xD;
at WebApplication3.WebDataService1.&lt;CustomersThatThrow&gt;d__2.MoveNext()&#xD;
at Microsoft.Data.Web.Serializers.AtomSerializer.WriteFeedElements(IEnumerator elements, Type expectedType, String title, String absoluteUri, String relativeUri, Boolean hasMoved)&#xD;
...
</Exception>

Note that the top-level <feed> element doesn't have a matching closing </feed> element. This means that if the client tries to parse the whole document as an XML document, it will fail - this is good, as we can't trust that the partial response makes sense on its own. If the client is also streaming (that is, processing each entry at a time), it will also have to deal with partially processed entities (but that's in the nature of the processing model).

Let's see what happens with other formats:
telnet 127.0.0.1 65314

GET /WebDataService1.svc/CustomersThatThrow HTTP/1.0
Accept: application/json

(reformatting the payload to show structure)
HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Wed, 26 Dec 2007 23:55:31 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: no-cache
Content-Type: application/json
Content-Length: 2093
Connection: Close
{
"d" : [ {
__metadata: {
uri: "
https://localhost:65314/WebDataService1.svc/Customers(\'ALFKI\' )", type: "Model.Customers"
}, CustomerID: "ALFKI", ...
}, {
__metadata: {
uri: "
https://localhost:65314/WebDataService1.svc/Customers(\'ANATR\' )", type: "Model.Customers"
}, CustomerID: "ANATR", ...
}, {
__metadata: {
uri: "
https://localhost:65314/WebDataService1.svc/Customers(\'ANTON\' )", type: "Model.Customers"
}, CustomerID: "ANTON", ...
}Something happend - imagine a database connection was dropped.
at WebApplication3.WebDataService1.<CustomersThatThrow>d__2.MoveNext()
at Microsoft.Data.Web.Serializers.JsonSerializer.WriteTopLevelElements(IEnumerator elements, Boolean hasMoved)
at Microsoft.Data.Web.Serializers.Serializer.WriteRequest(IEnumerator queryResults, Boolean hasMoved)
at Microsoft.Data.Web.ResponseBodyWriter.Write(Stream stream)

Again, notice that the error message is included in the payload in a way that produces invalid JSON (lexically incorrect in the array scope, and the square bracket and top-level curly brace are left unclosed).

The one place where we really can't do much is in MIME types that are unstructured or that we don't know anything about. In this case, we'll simply inject the error message into the response body and stop processing.

Please note that other than the part of 'we'll try to flow the status code out from a WebDataServiceException', everything in this post and the previous one is more an implementation note than a hard contract (until we have an official release and then it'll have the strength of "backward compatibility behavior", of course).

 

This post is part of the transparent design exercise in the Astoria Team. To understand how it works and how your feedback will be used please look at this post.