Web Disservices: Microsoft's Misstep
by Mark Pilgrim | Pages: 1, 2
An alternate approach
First, let me say that I have no problem with the actual data that Microsoft is returning from these web services calls. XML is a fine way of serializing complex data structures, and several of Microsoft's public web services methods return data structures much more complex than a single string. The WSDL file contains an XML schema that defines these data structures and datatypes and allows WSDL-aware tools to produce wrapper classes to translate these serializations into native data structures.
So +1 on XML, +1 on WSDL, +1 on XML Schemas. These are all good things, have sizable benefits, and are worth their cost in complexity. I am also in favor of HTTP, which is a fine way of requesting data and receiving responses, even XML responses. It's well understood, widely supported, and mature. +1 on HTTP.
But let's think about how we could provide the same set of services as Microsoft has offered but in such a way that we take advantage of the Web more directly. First, Microsoft is overloading a single URL, choosing to dispatch their methods on a custom HTTP header (
SOAPAction). This method name could be put into the URL instead:
http://ws.microsoft.com/mscomservice/GetVersion http://ws.microsoft.com/mscomservice/GetCultures http://ws.microsoft.com/mscomservice/GetTopDownloads
And so forth. This would accomplish the same thing, but it would make logging more useful, since a standard IIS or Apache access log would contain the method name.
Second, those method arguments... None of these methods seem to take complex data structures; the most complex datatype they take is a GUID, which is simply a long string in a particular format. And as you can see from the sample requests, the server does not require declaring the datatype of each argument on each request; both the client and server are deemed to know them from the schema within the WSDL file. Each parameter is named and order doesn't matter. So, basically, we have a bunch of unordered, named key-value pairs that we're passing to a URL. So instead of
<soap:Body> <GetTopDownloads xmlns="http://www.microsoft.com"> <topType>Recent</topType> <topN>25</topN> <cultureID>en-US</cultureID> </GetTopDownloads> </soap:Body>
Third, let's consider that insecure approach to authentication. It is susceptible to a number of attack types, and it requires both client and server to store sensitive information in plain text. It is not widely implemented in toolkits. There is also no programmatic way for a client to determine what authentication mechanism the server requires; it is documented in human-readable form, of course, but there is no way to write a generic client that speaks to this and to other web services without specific knowledge of each.
It appears to have been chosen solely as a way to show off a new standard which Microsoft had a hand in creating. I would replace it altogether HTTP digest authentication, which is more mature. Not only does digest authentication solve several security problems, it's more widely supported, and there is a standard, documented way for the server to announce that digest authentication is required (if a client tries to call a web services method without the proper credentials or without any credentials at all).
Given all this, what would our web service look like? Well, taking the
GetVersion example above and making the necessary modifications, it would look like this:
GET /mscomservice/GetVersion HTTP/1.0
Authorization: Digest Username="DEVELOPERTOKEN", realm="Microsoft.com web services", nonce="NONCE", uri="/mscomservice/GetVersion", qop="auth", nc="00000001", cnonce="CNONCE", response="RESPONSE", opaque="OPAQUE"
(The developer token is the same as the original example, and the other values are generated from the Microsoft-supplied PIN and other constants defined in RFC 2617.)
And the response would look like this:
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
<GetVersionResult xmlns="http://www.microsoft.com">Microsoft.Com Platform Services 1.0 Beta</GetVersionResult>
Note that this approach accomplishes exactly the same thing: we are calling a method with no parameters, getting a string back, and identifying ourselves appropriately to the server, just like Microsoft's SOAP service. The differences are where things go:
The method name is now part of the URL instead of being in a custom HTTP header and in the
<soap:Body>. This makes logging more useful.
The method arguments are now query parameters in the URL instead of being stored as elements within the
The authentication information is now transmitted in the standard
AuthorizationHTTP header used by digest authentication.
There are further subtle benefits. If you look closely you'll see we're using GET instead of POST. This opens up all kinds of possibilities for controlled caching, both server-side and client-side. For example, if I call
GetVersion the server could return an
Last-Modified HTTP header along with the response. The next time I call
GetVersion, I could pass along this etag, and if the data hasn't changed, the web services server could simply return an HTTP status code 304 "Not Modified" rather than returning the same response over and over. This is an ordinary and clever part of the Web.
Ah, but I know what you're thinking. You're thinking "well, I don't care about what goes over the wire because I'll never see it. I'll just point my Web-Services-Enabled IDE to a WSDL file and call a method in a wrapper class that takes no arguments and returns a string." Okay, you can still do that. WSDL supports HTTP GET bindings, so you could create a WSDL file that described this URL-and-query-parameters web service, just as easily as you can describe an everything-in-the-SOAP-envelope web service. Here is such a WSDL file. Try it in Microsoft's Visual Studio .NET; you'll find that it generates wrappers for the methods and classes with native data structures, which are virtually identical to the wrapper classes generated by Microsoft's original WSDL file. (In fact they're slightly simpler because there's slightly less cruft in the serialized XML responses. Other than that, they're identical.)
Microsoft wouldn't do this, but you should
Realistically, would Microsoft consider doing this? No, probably not. As you can see, the hypothetical version is much simpler over the wire, so there is some benefit from reduced bandwidth. But for developers using Microsoft tools (i.e. Microsoft's primary customers here), there is no other benefit since WSDL hides the complexities of either approach equally well. Everything is simple when you have perfect tools, and Microsoft makes a lot of money selling those tools. Microsoft has implemented its web services in as complex a manner as possible, and then it proceeds to sell developer tools that make calling those web services trivially easy.
However, you should consider the second approach, assuming you're trying to make money from your actual web services instead of from selling developer tools. The primary advantage of the second approach is that it is significantly easier to use when you don't have perfect tools -- in this case, that means non-Microsoft tools that haven't caught up to Microsoft's latest standards yet. Digest authentication is more widely supported than the UsernameToken scheme Microsoft is using; it's more secure and more extensible too. Query strings are easier to produce than SOAP bodies; even if you have a good SOAP library, they're still easier to debug. Developers win with reduced debugging costs, and everybody wins with reduced bandwidth costs.
Further reading: I've put together a live Microsoft.com Web Services gateway that acts as a proxy between Microsoft.com's SOAP-based web services and a hypothetical second approach similar to what I've described here. Live, clickable web services calls are provided; the gateway script takes the URL-and-query-parameters and translates them into the SOAP body that Microsoft's server requires, and sends it along, then takes the response and strips out all unnecessary SOAP envelope and returns just the XML the client needs. Sample client code is available to call the gateway from both C# and Python, along with a WSDL file, and the gateway script itself.