Exchange ActiveSync Building Blocks – Error Handling

Index for the series:
Exchange ActiveSync Building Blocks – Intro

Background reading (not a pre-requisite):
[MS-ASCMD] Common Status Codes
[MSDN] WebExceptionStatus Enumeration

If you’ve followed the past couple of articles you’ll no doubt have noticed I haven’t gone out of my way to handle exceptions and errors gracefully. If you run into a problem when running the sample code against an Exchange that for some reason is configured differently than mine, (also known as entirely wrong by definition), you might run into the very helpful “Something happened” message without further explanation.

One might be tempted to ask if it’s because I’m lazy, or simply do not know how to handle it. Well, ask no more, because I now intend to do something about it 🙂

There are two main things we need to be able to handle;
– Exceptions (conditions throwing errors).
– ActiveSync protocol errors (usually included as a status code in wbxml returned from the server).

Exceptions can be anything really – HTTP errors, SSL exceptions, network issues. Unless it’s something really obvious like the user entering an incorrect address to the Exchange Server it’s often something that needs to be resolved by the network and/or Exchange admin. If you as a programmer are not able to fix this, (let’s face it, if you’re implementing an ActiveSync client other people will be using with their servers odds are you can’t), you should make sure that the user receives an understandable feedback, and preferably something he can pass along to his helpdesk for resolving. Far too often I run into ActiveSync clients presenting entirely generic error messages that don’t provide me with anything I can work with. I hate it when that happens, and send evil thoughts in the general direction of the programmer who implemented this kind of error handling.

ActiveSync protocol errors are a different beast. These usually happen in the actual ActiveSync traffic, and this means that you are communicating with an Exchange Server, but not necessarily per protocol. This means that it has the potential for being more tricky to troubleshoot, and is more likely to be something the programmer needs to handle. Note that a status code isn’t by definition an error, it can be informational in its nature. Whether it’s an error condition or a generic feedback you should handle it in a proper manner.

I am of course not able to cover every possible error situation that may occur, but I have figured out a few of them that are relevant to troubleshooting 🙂

I’ve separated these into two methods, as it would just create too much work maintaining it across all the different tests I’ve implemented so far if I was inlining it in the test itself. (That these tests themselves might/should be collected in a smaller number of methods is an entirely different matter. All in due time.) Not all errors are valid for all tests, but they usually don’t indicate entirely different things so it shouldn’t matter. You can run into differences between Exchange Server versions – for instance Exchange 2007 will throw an exception (HTTP 449) when you should provision whereas Exchange 2010 returns this as a status code (141/142) instead. I’ve added a few checks to my code for this, but if you want to make sure your code works across Exchange versions you need to study the MSDN docs good and make note of all change log entries between the versions.

I’m reusing the UI from the FolderSync test app (EAS_BB_Part-02_03), but behind the scenes I have added the code required to provide the user with a little bit more information if something fails.

Please note that while I have previously included a code snippet in all apps so that all certificates are happily accepted, I have removed these lines specifically to be able to throw such SSL error conditions. (You have the source so modify it if you don’t like this behavior.)

The first method is for parsing the HTTP errors (throws WebExceptions). There’s lines that seem to be cut off below – it’s just for illustrative purposes. Use the code from the source file; don’t copy & paste from the box below.

private string parseHTTPException
(WebException httpEx, string strEASVersion, bool bProvisionable)
{
   string parsedEx = null;

   switch (httpEx.Status.ToString())
   {

      case ("NameResolutionFailure"):
         parsedEx += httpEx.Message.ToString() + "\r\n";
         parsedEx += "Possible reason: DNS lookup failed, or server is not reachable by host name.\r\n"; parsedEx += "Status: FAIL";
         break;

      case ("ConnectFailure"):
         parsedEx += httpEx.Message.ToString() + "\r\n";
         parsedEx += "Possible reason: The server is not reachable at the specified address, or port is not open.\r\n"; parsedEx += "Check that necessary services are responding, and firewall rules aren't blocking connections.\r\n"; parsedEx += "Status: FAIL";
         break;

      case ("SendFailure"):
         parsedEx += httpEx.Message.ToString() + "\r\n";
         parsedEx += "There does not seem to be a server responding at this address. \r\n"; parsedEx += "Verify that the URL should respond and run test again.\r\n"; parsedEx += "Status: Further action required";
         break;

      case ("ProtocolError"):
         parsedEx += httpEx.Message.ToString() + "\r\n";
         HttpWebResponse resEx = (HttpWebResponse) httpEx.Response;
         int resCode = (int)resEx.StatusCode;
         if (resCode == 302)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "Server issued a redirect to a new URL. You should issue a new Autodiscover (and change FQDN).\r\n"; parsedEx += "Status: Further action required";
         }

         if(resCode == 400)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "Possibly a protocol mismatch, for example using version 14.0 against
                an Exchange 2007 server.\r\n"; parsedEx += "Choose a different protocol version to emulate, and try to run test again.\r\n"; parsedEx += "Status: FAIL";
         }
         if (resCode == 401)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "Wrong username/password. May also occur if you're using a reverse proxy which performs authentication.\r\n"; parsedEx += "Could also be caused by authenticating with user@domain.com if Active Directory doesn't accept this.\r\n"; parsedEx += "Status: FAIL";
         }
         if (resCode == 403)
         {
            if (strEASVersion == "12.0" || strEASVersion == "12.1")
            {
               parsedEx += "Explanation:\r\n";
               parsedEx += "You are either running a non-provisionable device, or a provisionable device that
                   haven't been provisioned yet.\r\n"; parsedEx += "First check: Tick off \"Provisionable device\" and run test again.\r\n";
               parsedEx += "Second check Tick off \"Support security policies\" and run test again.\r\n";
               parsedEx += "Status: Further action required";
            }
            else {
               parsedEx += "Explanation:\r\n";
               parsedEx += "The server requires SSL and will not let you connect over HTTP.\r\n"; parsedEx += "(For instance trying to connect over HTTP while IIS requires SSL.)\r\n"; parsedEx += "Status: Further action required";
            }
         }
         if (resCode == 404)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "File not found shouldn't occur... check IIS that files are not missing.\r\n"; parsedEx += "Status: FAIL";
         }
         if (resCode == 449)
         {
            if (bProvisionable == false)
            {
               parsedEx += "Explanation:\r\n";
               parsedEx += "You seem to be using a non-provisionable device.\r\n"; parsedEx += "Check \"Provisionable device\" and run test again.\r\n"; parsedEx += "Status: Further action required";
            }
            else {
               parsedEx += "Explanation:\r\n";
               parsedEx += "You seem to be running a provisionable device, but will need to apply policies before sync is allowed.\r\n"; parsedEx += "Check \"Support security policies\" and run test again.\r\n"; parsedEx += "Status: Further action required";
            }
         }
         if (resCode == 451)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "Redirect request. Your mailbox is located on a different server.\r\n"; parsedEx += "Address returned by Exchange: " + httpEx.Response.Headers.Get("X-MS-Location") + "\r\n";
            parsedEx += "Please change the FQDN part in the \"Server Address\" field.\r\n";
            parsedEx += "Status: Further action required";
         }
         if (resCode == 501 || resCode == 505)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "This is correct behaviour, and means your Exchange server is responding!\r\n"; parsedEx += "Status: PASS";
         }
         if (resCode == 502)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "There is a server available at this FQDN and this port, but it's not responding to your request.\r\n"; parsedEx += "Could for instance happen if http://contoso.com does not redirect to an Exchange server.\r\n"; parsedEx += "Status: FAIL";
         }
         if (resCode == 503)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "You server is either experiencing too much load, or in a maintenance mode.\r\n"; parsedEx += "Check again in a short while, and if problem persists check that all services are running on your
                Exchange server.\r\n"; parsedEx += "Status: FAIL";
         }
         if (resCode == 504)
         {
            parsedEx += "Explanation:\r\n";
            parsedEx += "Seems your Exchange server might have problems on the back end connecting to other servers
                (possibly Active Directory).\r\n"; parsedEx += "Check network connectivity on your Exchange server. (Problem most likely not between ActiveSync client
                and Exchange.)\r\n"; parsedEx += "Status: FAIL";
         }                    

         break;

      case ("TrustFailure"):
         parsedEx += httpEx.Message.ToString() + "\r\n";
         parsedEx += "The SSL certificate presented is not trusted. \r\n";
         parsedEx += "Check \"Trust all certificates\" and run test again.\r\n";
         parsedEx += "Status: Further action required";
         break;

      default:
         break;
   }

   return parsedEx;
}

For parsing the status codes out of the returned wbxml we actually cheat a little bit. We just check if the wbxml contains these codes statically. Which means that if you have a folder called for instance “142” this could be misinterpreted as an error. Where we are presently with our little program this doesn’t really matter, but it will make sense to implement it properly if you’re doing actual synchronization. (We haven’t covered wbxml yet either, so it wouldn’t make sense at any rate now.)

private string parseResponseStatus
(string responseBody, string strEASVersion)
{
   string parsedStatus = null;

   if (responseBody.Contains("108") == true)
   {
      parsedStatus += "The device ID is invalid.";
      parsedStatus += "\r\nAre you using any special characters? Only plain characters and numerals recommended.)"; parsedStatus += "\r\n";
   }

   if (responseBody.Contains("126") == true)
   {
      parsedStatus += "The user account does not have permission to synchronize."; parsedStatus += "\r\nThis could also be due to using an admin account. (Domain admin is not allowed to sync.)"; parsedStatus += "\r\n";
   }

   //if EAS >= 12.1 (Exchange 2007 SP1) we need to parse the //returned wbxml for '142' to find out if we need to provision //EAS 12.0 indicates the same status through HTTP 449 if (strEASVersion == "12.1" || strEASVersion == "14.0" || strEASVersion == "14.1")
   {
      if (responseBody.Contains("142") == true)
      {
         parsedStatus += "The response is ok, but you need to provision";
         parsedStatus += "\r\n";
      }
   }
   if (responseBody.Contains("141") == true)
   {
      parsedStatus += "Only provisionable devices are allowed to sync, and you seem to be non-provisionable.\r\n"; parsedStatus += "Check the \"Provisionable device\" and run the test again.\r\n"; }
   if (responseBody.Contains("140") == true)
   {
      parsedStatus += "A remote wipe has been issued for your device.\r\n";
      parsedStatus += "Choose \"Remote Wipe (Emulated)\" to simulate a wipe.\r\n"; }
   if (responseBody.Contains("144") == true)
   {
      parsedStatus += "The device's policy key is invalid. The policy has probably changed on the server.
        The device needs to re-provision.\r\n"; parsedStatus += "This may happen if you have synced a \"device\" with security policies and then removed the support
        for all policies.\r\n"; }

   else if (responseBody.Length <= 15)
   {
      parsedStatus += "More status codes can be looked up here:\r\n";
      parsedStatus +=
        "http://msdn.microsoft.com/en-us/library/ee218647(v=EXCHG.80).aspx\r\n";
   }
   return parsedStatus;
}

We should then proceed to replace this catch:

catch (WebException ex)
{
   txtOutput.Text += "Something went wrong!\r\n";
   txtOutput.Text += ex.Message.ToString() + "\r\n\r\n";
}

With the more suitable:

catch (WebException ex)
{
   txtOutput.Text += "Response: " +
    parseHTTPException(ex,strASVersion,false) + "\r\n";
}

To handle status codes in an actual response from the server we should add a line to the output code to parse out the status.

Before:

txtOutput.Text += "Response body \r\n";
string responseBody = sr.ReadToEnd().Trim();
Byte[] byteResp = utf8Encoding.GetBytes(responseBody);

//to be able to represent the response in a //textbox we need to parse the individual chars char charResp = '0';
for (int i = 0; i < byteResp.Length; i++)
{
    charResp = (char)byteResp[i];
    txtOutput.Text += charResp;
}
txtOutput.Text += "\r\n";

After:

txtOutput.Text += "Response body \r\n";
string responseBody = sr.ReadToEnd().Trim();
Byte[] byteResp = utf8Encoding.GetBytes(responseBody);

txtOutput.Text += parseResponseStatus(responseBody, strASVersion);

//to be able to represent the response in a //textbox we need to parse the individual chars char charResp = '0';
for (int i = 0; i < byteResp.Length; i++)
{
    charResp = (char)byteResp[i];
    txtOutput.Text += charResp;
}
txtOutput.Text += "\r\n";

To be sure I would get an error I made a small edit to my ActiveSync mailbox policy. Last time we made sure to allow non-provisionable devices, but this time around we’ll make sure of the opposite:

image

Since we’re not doing any provisioning related activities this should fail.

Lo and behold – we do get an indication as to what our sync problem is (status code 141):

image

The tip given that I should check “Provisionable device” is a reference to the fact that I’ve copied this code from my EAS MD utility which has such a check box Smile We don’t have such a box yet, but will of course look into that later on.

Certainly not an exciting post this time around, but it was inevitable that I had to implement something more advanced than empty try-catch clauses sooner rather than later. And it makes it easier for you to debug your development environment as well Smile

Downloads:
C# Source – ErrorHandling.cs
C# Source – EAS_BB_Part-03_01.zip

One thought on “Exchange ActiveSync Building Blocks – Error Handling”

Leave a Reply

Your email address will not be published. Required fields are marked *

*