An example of the risk of not versioning REST

A while back I was contacted to solve a quasi-nasty issue. An existing REST implementation had been updated with an extra field being added to the response. This worked fine for 99% of the consumers, but for one consumer it broke. This consumer wrote a package that they sold on to others and their customers were screaming.

The reason it broke was that it was not coded to handled additional fields. Technically, REST is an architectural pattern without standards. Robustness in handling extra fields and data is what most developers would expect -- but that is not a requirement of REST. It is hopeful thinking.

If the REST was well versioned, this issue would not have arisen. It did arise.

While this consumer can patch their code, getting the patch to all of their customers was problematic hence there was a need to do an immediate fix, somehow. Fortunately, their package allows the REST Url to be specified and that allow a simple quick solution. Create a "Relay Website" up on Azure that relays the data from the customers and remove this extra field in the response. All of the data was in JSON which reduced the scope of the issue.

The code was actually trivial (using Newtonsoft.Json.Linq;). As you can see, it is easy to eliminate as many fields as desired by just adding case statements:


   public class CorgiController : ApiController
    {
        [HttpPost]
        public JObject Get()
        {
            var jresponse = RestEcho.EchoPost();
            foreach (JToken item in jresponse.Children())
                WalkChildrenAndRemove(item);
            return jresponse;
        }


        [HttpPost]
        public JObject Cancel()
        {
            return RestEcho.EchoPost();
        }

        private void WalkChildrenAndRemove(JToken jitem)
        {
            if (jitem is JProperty)
            {
                var prop = jitem as JProperty;
                switch (prop.Name)
                {
                    case "Foobar": jitem.Remove(); break;
                    default:
                        foreach (JToken item in jitem.Children())
                            WalkChildrenAndRemove(item);
                        break;
                }
            }
            else if (jitem is JArray)
            {
                var arr = (JArray)jitem;
                foreach (JToken item in arr)
                    WalkChildrenAndRemove(item);
            }
            else
            {
                foreach (JToken item in jitem.Children().ToArray())
                    WalkChildrenAndRemove(item);
            }
        }
    }
}
With the RestEcho class being also trivial,

  public static class RestEcho
    {
        public static JObject EchoPost()
        {
            var url = GetServer() + HttpContext.Current.Request.Path;
            var stream = new StreamReader(HttpContext.Current.Request.InputStream);
            var body = stream.ReadToEnd();
            var value = JObject.Parse(body);
            // Get the login and password sent
            HttpContext httpContext = HttpContext.Current;
            NameValueCollection headerList = httpContext.Request.Headers;
            var testHeader = headerList.Get("Authorization");
            if (testHeader == null || testHeader.Length < 7)
            {
                HttpContext.Current.Response.StatusCode = 401;
                HttpContext.Current.Response.StatusDescription = "Basic Authentication Is Required";
                HttpContext.Current.Response.Write("Failed to Authenticate");
                HttpContext.Current.Response.End();
            }
            // remove "BASIC " from field
            var authorizationField = headerList.Get("Authorization").Substring(6);
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorizationField);
            var response = client.PostAsJsonAsync(url, value).Result;
            var jresponse = new JObject();
            try
            {
                jresponse = (JObject)response.Content.ReadAsAsync().Result;
            }
            catch
            {

            }
            return jresponse;
        }
  
This pattern can also be used to reduce a version X to version 0 with likely less coding than alternative approaches -- after all, you just have to add case statements if the structure is the same.

This was tossed up onto Azure with running costs being well less than $10/month. 

Happy customer. Happy customer's customers.

Comments

Popular posts from this blog

Simple WP7 Mango App for Background Tasks, Toast, and Tiles: Code Explanation

Yet once more into the breech (of altered programming logic)

Error : /ScriptResource.axd : Invalid viewstate.