Tuesday, May 14, 2013

SharePoint 2013 Online App: 403 Response While Downloading Documents from a Document Library

Background

I've been building a lot of SharePoint 2013 Online Apps lately (both provider and SharePoint hosted).  Like most developers, I initially struggled with a few OAuth App Token challenges when trying to persist data between page loads.  And just when I thought I had a good methodology down, I ran into an issue downloading documents from a document library.

I kept receiving a 403 (forbidden) HTTP response code when I attempted to read the file from the document library.  I checked the document library permissions and they were correct.  So, I did some digging online and found others complaining about the issue.

In order to isolate the exact issue, first, I created a document library and uploaded a few PDF documents.  Then, I created a sample SharePoint 2013 App in visual studio.  If you are unfamiliar with the process, here is a good link to get you started: How to: Create a basic provider-hosted app for SharePoint.  In order to limit the scope of this post, I will not cover specifically how I obtained the Microsoft.SharePoint.Client.ClientContext instance in the sample code here.  Please reference the Sources Consulted section below for related reading.

The Original Code

Here was my original sample code I used to isolate the issue:

/// <summary>
/// Downloads a file from a document library and saves it to disk
/// </summary>
/// <param name="context"></param>
/// <param name="fileNameToSaveToDisk"></param>
public void DownloadFileTest(ClientContext context, string fileNameInDocumentLib)
{
    var list = context.Web.Lists.GetByTitle("DocumentLibraryName");
    context.Load(list);
    context.Load(list.RootFolder);
    context.ExecuteQuery();

    string serverRelativeUrl = list.RootFolder.ServerRelativeUrl;
    serverRelativeUrl += "/" + fileNameInDocumentLib;

    // Save file to a temporary location to prove all file contents were received
    string localFilePath = "C:\\Temp\\test.pdf";

    int position = 1;
    int bufferSize = 200000;

    // Open the file
    using (FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(context, serverRelativeUrl))
    {
        Byte[] readBuffer = new Byte[bufferSize];
        // code below causes 403 error
        using (System.IO.Stream stream = System.IO.File.Create(localFilePath))
        {
            while (position > 0)
            {
                position = fileInfo.Stream.Read(readBuffer, 0, bufferSize);
                stream.Write(readBuffer, 0, position);
                readBuffer = new Byte[bufferSize];

            }
            fileInfo.Stream.Flush();
            stream.Flush();
        }
    }
}

The problem was stemming from the use of the Microsoft.SharePoint.Client.File.OpenBinaryDirect() method. This is a static method that is supposed to grab the file without having to call the ExecuteQuery() Method. However, since I was using the oAuth App authentication model, this method apparently doesn't pass my app token along like the ExecuteQuery() method does.

After pouring over lots of MSDN documentation, various blogs and asking a friend or two, I modified the code above and it works as expected:

The Working Code

/// <summary>
/// Grab a document from my document library utilizing the Microsoft.SharePoint.Client.File.OpenBinaryStream() method
/// </summary>
/// <param name="context"></param>
/// <param name="fileNameInDocumentLib"></param>
public void DownloadFileTest2(ClientContext context, string fileNameInDocumentLib)
{
    // Find the file by name
    CamlQuery camlQuery = new CamlQuery();
    camlQuery.ViewXml = @"<View>
                            <Query>
                                <Where>
                                <Eq>
                                    <FieldRef Name=""FileLeafRef"" />
                                    <Value Type=""Text"">" + fileNameInDocumentLib + @"</Value>
                                </Eq>
                                </Where>
                                </Query>
                            </View>";

    List list = context.Web.Lists.GetByTitle("DocumentLibraryName");
    ListItemCollection listItems = list.GetItems(camlQuery);
    context.Load(listItems);
    context.ExecuteQuery();

    if (listItems.Count > 0)
    {
        var item = listItems.First();
        Microsoft.SharePoint.Client.File file = item.File;
        ClientResult<Stream> data = file.OpenBinaryStream();

        // Load the Stream data for the file
        context.Load(file);
        context.ExecuteQuery();

        // If data received, write it to disk so I can verify the contents
        if (data != null)
        {
            int position = 1;
            int bufferSize = 200000;
            Byte[] readBuffer = new Byte[bufferSize];
            string localFilePath = "C:\\Temp\\test.pdf";
            using (System.IO.Stream stream = System.IO.File.Create(localFilePath))
            {
                while (position > 0)
                {
                    // data.Value holds the Stream
                    position = data.Value.Read(readBuffer, 0, bufferSize);
                    stream.Write(readBuffer, 0, position);
                    readBuffer = new Byte[bufferSize];
                }
                stream.Flush();
            }
        }
    }
}

Concluding Remarks

Notice in the example above I load the resulting Microsoft.SharePoint.Client.File object's Stream data by calling the OpenBinaryStream() method and then loading the contents by calling the ExecuteQuery() method on the current context.  This approach allowed me to successfully return the contents of  the file.  The only reason I wrote the file to my local hard drive was to verify that the file contents were correct.  It's kinda hard to validate I'm getting an un-corrupted PDF file simply by viewing a byte array.

As an aside, since it's probably more useful to return the file contents as a byte array, you could replace the code that writes the file to disk with something like:

if (data != null)
{
    MemoryStream memStream = new MemoryStream();
    data.Value.CopyTo(memStream);

    // returns a byte[]
    return memStream.ToArray();
}

Like/Dislike what you see? Feel free to drop me a line.

Sources Consulted

Microsoft.SharePoint.Client.File.OpenBinaryDirect() Method

Inside Microsoft OAuth Context Tokens

Authorization and authentication for apps in SharePoint 2013

OAuth authentication and authorization flow for cloud-hosted apps in SharePoint 2013

How to: Create a basic provider-hosted app for SharePoint



6 comments:

  1. Excellent tutorial! It helped me. Thank you.

    ReplyDelete
  2. You saved my day,
    Thank you for sharing!
    Nick

    ReplyDelete
  3. Thanks a lot, you really saved us a lot of time, although we almost had the same code and it caused 401 exception, but using your code, it worked like charm

    ReplyDelete
  4. This is great, but doesn't work in Sharepoint 2010 unfortunately!
    There is no OpenBinaryStream in SP 2010. If anybody has a solution, please share.

    I need an alternate for the below line and I'll be done.
    ClientResult data = file.OpenBinaryStream();

    ReplyDelete
  5. Nice articel.. Save my day !!. Thanks !!

    ReplyDelete