Monday, March 17, 2008

WebServices, 302 Object moved and "Client found response content type of 'text/html', but expected 'text/xml'" exception

The issue

This article focuses on the issue of WebServices intentionally redirecting requests to other locations. Please first make sure that the exception is not caused unintentionally, for example by incorrect authorization policy in web.config.

In general, Web Browsers handle redirects with no issues. If "302 Object moved" is sent to indicate a redirect, the web browser happily checks new location and retrieves the data.

However, while web browsers automatically handle redirects regarding requests to WebServices, it seems that client proxy classes do not automatically handle such redirects. This is quite annoying - my WebBrowser automatically redirects 302s, the HttpWebRequest also does but the WebService proxy inheriting from SoapHttpClientProtocol does not!

I am not going to answer a general question "why do you need to redirect a web service call". I just assume you need, just as I do.

General solution

My first attempt to find the solution was to use Google. I've came upon nice article by Matt Powell, Using ASP.NET Session State in a Web Service (look for the "Cookieless Sessions" section).

In short, Matt's solution is to wrap each invocation in a try-catch clause which tries to remap original WebService's Uri to the new one. This solves the issue, however is not practical since you'd end up with wrapping each single WebService method with a bloating try-catch clause.

The nice thing in Matt's approach is that it's general - if any subsequent WebService call causes 302 to be sent to the client, it is handled on the client side.

Specific solution - single redirect

The case of my application is not as general as to require the remapping to be handled for every request. I need to remap the url just once during the first call, however an additional cookie is appended to this first request. Instead of wrapping each single method, I've just wrote a simple proxy class which inherit from the SoapHttpClientProtocol class.

using System.Diagnostics;
using System.Web.Services;
using System.ComponentModel;
using System.Web.Services.Protocols;
using System;
using System.Xml.Serialization;
using System.Net;
using System.IO;
 
namespace Vulcan.Web.WebServices
{
    public class SoapHttpClientProtocol302 : 
        System.Web.Services.Protocols.SoapHttpClientProtocol
    {
        protected override System.Net.WebRequest GetWebRequest( Uri uri )
        {
            if ( !_302Fixed )
            {
                Fix302( new Uri( this.Url ) );
                _302Fixed = true;
 
                return base.GetWebRequest( new Uri( this.Url ) );
            }
 
            return base.GetWebRequest( uri );
        }
 
        private bool _302Fixed    = false;
        private void Fix302( Uri uri )
        {
            HttpWebRequest request    = (HttpWebRequest)HttpWebRequest.Create( uri );
            request.CookieContainer   = new CookieContainer();
            request.AllowAutoRedirect = false;
            HttpWebResponse response  = (HttpWebResponse)request.GetResponse();
 
            if ( response.StatusCode == HttpStatusCode.Redirect )
            {
                this.Url = new Uri( uri, response.Headers["Location"] ).ToString();
                this.CookieContainer.Add( response.Cookies );
            }
        }
    }
}

As you can see, the first request is handled in a special way: a new request is made to the application server and if the response code is 302 the Url of the proxy class is remapped to the new location returned by the appliaction server.


What I then need to do is to move my client proxy classes in the class hierarchy from SoapHttpClientProtocol to SoapHttpClientProtocol302.

3 comments:

Anonymous said...

Nice Fix.

There is an issue with Fix302 though. You need to add

request.Proxy = this.Proxy

prior to the

HttpWebResponse response = (HttpWebResponse)request.GetResponse();

There is another nice trick. Since the generated code for the client is a partial class, instead of generating a new class just add a new file that implements your functions in that partial class. Thus no code changes to existing code that consumes the generated client.

Wiktor Zychla said...

thanks for sharing these remarks!

regards,
Wiktor

Daniel Clarke said...

Hmm, you should really be closinging your response after use. This has just bitten me on the bum - I made lots of requests and ran out of free connections.



void RedirectFrom30xResponse(Uri uri)
{
var request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.CookieContainer = new CookieContainer();
request.AllowAutoRedirect = false;
request.Proxy = Proxy;
HttpWebResponse response = null;
try
{
response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode == HttpStatusCode.MovedPermanently // 301
||
response.StatusCode == HttpStatusCode.Redirect // 302
||
response.StatusCode == HttpStatusCode.RedirectMethod // 303
||
response.StatusCode == HttpStatusCode.RedirectKeepVerb // 307
)
{
Url = new Uri(uri, response.Headers["Location"]).ToString();
if (CookieContainer == null)
{
CookieContainer = new CookieContainer();
}
CookieContainer.Add(response.Cookies);
}
}
finally
{
response.Close();
}
}