Friday, May 7, 2010

Silverlight + Azure Shared Access Policy Issue

We’ve been working with Shared Access Policies in Azure for the last week or so, and for the most part it was working.  But it would only work for a while, then it would stop.  Then it would start working again.   It took some help from Steve Marx and Jai Haridas on the Azure Team to figure out what was going on.

This first piece of code is part of our storage manager that retrieves the Uri for the blob from Azure Blob Storage, and creates a Shared Access Policy for that blob so that our Silverlight Video Player can directly access the blob without going through a very slow web service call.

   1: /// <summary>



   2:         /// Get the Uri for a specified video blob



   3:         /// </summary>



   4:         /// <param name="videoId">The unique identifier of the video</param>



   5:         /// <returns>A Uri ponting to the Video</returns>



   6:         public Uri GetVideoBlobUri(Guid videoId)



   7:         {



   8:             var log = Log4NetHelper.GetLogger();



   9:             log.DebugFormat("Getting Video for video id {0}", videoId);



  10:  



  11:             try



  12:             {



  13:                 CloudBlobContainer container = GetContainer("VideoContainerName");



  14:  



  15:                 CloudBlockBlob cloudBlockBlob = container.GetBlockBlobReference(string.Format(CultureInfo.InvariantCulture, "{0}", videoId));



  16:                 



  17:                 var readPolicy = new SharedAccessPolicy



  18:                 {



  19:                     Permissions = SharedAccessPermissions.Read,



  20:                     SharedAccessExpiryTime = DateTime.UtcNow + TimeSpan.FromMinutes(10)



  21:                 };



  22:  



  23:                 var blobUri = new Uri(cloudBlockBlob.Uri.AbsoluteUri + cloudBlockBlob.GetSharedAccessSignature(readPolicy));



  24:  



  25:                 log.DebugFormat("GetVideoBlobUri successfully retrieved for video id {0}", videoId);



  26:  



  27:                 return blobUri;



  28:             }



  29:             catch (StorageClientException ex)



  30:             {



  31:                 // If the blob was not found, return null



  32:                 if (ex.ErrorCode == StorageErrorCode.BlobNotFound)



  33:                     return null;



  34:                 // Rethrow the exception in all other cases



  35:                 throw;



  36:             }



  37:             catch (Exception ex)



  38:             {



  39:                 log.ErrorFormat("Error while retrieving video blob {0}", ex.Message);



  40:                 throw;



  41:             }            



  42:         }




In our Silverlight Web Service, we call this manager class and return the Uri embedded in an XElement.  Originally we did this to allow us to return other information with the Uri, but at this point, the Uri is all we are passing through.





   1: /// <summary>



   2: /// Get video URI.



   3: /// </summary>



   4: /// <param name="videoId">ID of the given video.</param>



   5: /// <returns>Status</returns>



   6: public XElement DemoVideoUri(string videoId)



   7: {



   8:     var blobManager = UnityFactory.Current.Resolve<IBlobManager>();



   9:     var blobUri = blobManager.GetVideoBlobUri(new Guid(videoId));



  10:  



  11:     string xml = string.Format("<VideoUri>{0}</VideoUri>", HttpUtility.HtmlEncode(blobUri.AbsoluteUri));



  12:     var sr = new StringReader(xml);



  13:     return XElement.Load(sr);



  14: }




On the Silverlight side, we take make a call to this web service, extract the Url from the XElement, and pass the Url into the MediaPlayer like so





   1: var client = new WebClient();



   2:  



   3: var videoUri = GetVideoUri();



   4:  



   5: client.DownloadStringCompleted += (x, y) =>



   6:                                Dispatcher.BeginInvoke(



   7:                                    () =>



   8:                                    {



   9:                                        var xdoc = XDocument.Parse(y.Result);



  10:                                        var query = from b in xdoc.Descendants()



  11:                                                    select b.Value;



  12:                                        HostedVideoUri = new Uri(HttpUtility.HtmlDecode(query.First()), UriKind.Absolute);



  13:                                        mediaPlayer.Source = HostedVideoUri;



  14:                                        Log(string.Format("HostedVideoUrl = {0}", HostedVideoUri.AbsoluteUri));



  15:                                    }



  16:                                    );



  17:  



  18: client.DownloadStringAsync(new Uri(videoUri, UriKind.Absolute));



  19:  



  20:  



  21: private static string GetDemoVideoUri()



  22: {



  23:     var host = HtmlPage.Window.Eval("window.location.hostname;") as string;



  24:     var path = HtmlPage.Window.Eval("window.location.pathname;") as string;



  25:     var refererUri = host + path;



  26:     var url = string.Format("http://{0}/xxxx.Svc/DemoVideoUri/{1}", GetHost(), VideoId);



  27:     return url;



  28: }






This is all pretty simple code (and the above code works, by the way).



Where we ran into problems was in the Web Service, when populating the xml string.  Originally, we used blobUri.ToString() instead of blobUri.AbsoluteUri.   This caused big issues (403 errors returned from the Azure Storage Service), when the Video Player tried to retrieve the blob because the Url returned from the Shared Access Policy generator can have spaces in it.  And Uri.ToString and Uri.AbsoluteUri work very differently when handling spaces.  I did not know this until last night.  Uri.ToString unescapes the Uri before returning it.







So why did it work sometimes, and not all the time?  Simple.  Sometimes the Uri from the Shared Access Policy Generator has spaces, and sometimes it does not.  The former did not work, while the latter did.  We were looking for a pattern in the number of times we called the web service, or the interval between calls, and the size of the video blob.  But it was as simple as a single space.



I think we spent at least 8 hours over the last two days trying to track down this bug.  Hopefully this saves someone else some grief.



This post is cross posted on http://www.palador.com/blog/Blog/Silverlight--Azure-Shared-Access-Policy-Issue intentionally.