A Fetch-based CORS Wrapper for SharePoint REST

In this post, I’m going to create a better CORS Wrapper for SharePoint REST operations, and demonstrate using it for CRUD operations on a Picture library. First, I want to remove the dependency of jQuery, using fetch instead. As I covered in a previous post, Ugly SPA Revisited using Fetch and REST, fetch is new enough and implementations are spotty enough, even in evergreen browsers, that I will need to polyfill fetch and ES6 promise in order to support a reasonable cross-section of browsers.
 
By implementing the full range of CRUD operations on document libraries, we’ll have an opportunity to see if there are other issues that need to be addressed in our wrapper. My last post really only did one simple REST operation across CORS boundaries.


This post is part of a series on SharePoint and CORS including:
 
  1. REST Calls Across HNSCs (CORS)
  2. A Light-weight CORS Wrapper for SharePoint REST
  3. A Fetch-based CORS Wrapper for SharePoint REST

I’m going to build the same ugly SPA I’ve built before in some of my REST posts. In particular, I’m going to rewrite the code from the post Ugly SPA Revisited using Fetch and REST, this time using my CORS wrapper to perform CRUD operations across Host-named Site Collections (HNSC). I’ll repeat the basics of what the SPA is supposed to look like in the next section.
 
The goal of the wrapper is to make calling web services across CORS boundaries as easy as making web service calls that are not cross-origin. And ideally, the syntax of these cross-origin web service calls should look as close as possible to non-cross-origin web service calls. So by comparing my source code from this post to the source code from Ugly SPA Revisited using Fetch and REST, we’ll be able to see just how successful we were.

What We’re Going to Build

Below is the basic interface. It consists of an un-ordered list of picture names, each with a radio button next to it. And at the bottom is a div that will act as a drop zone for files dragged onto it. This interface demonstrates create and read. The un-ordered list is populated by reading the list items in our Picture library, and you can create by dragging images to the drop zone.
 
I already have some images in the library. If you don’t, the un-ordered list will be empty and the only visible thing in the form will be the drop zone to add new files. I’m not actually restricting files to be images, you can drop any file on the zone and it will get uploaded. In fact, you can point to any document library instead of a picture list, except that some of the code expects there to be a Description field, which exists on picture lists but not normally on document libraries.


Ugly SPA

If you click on one of the radio buttons to select an image, a form opens up below the list which provides edit and delete functionality.


Ugly SPA Edit Form

As I said, it’s not pretty, but this gives me enough interface to show the complete range of CRUD operations for SharePoint document libraries.

The HTML

There is nothing special about the HTML, but since I’m going to be manipulating it as I demonstrate the CRUD operations, I’ll show it here so you can refer back to it. It consists of an unordered list to show pictures currently in the library, a small form for edit/delete, and a div with some structure to implement drag and drop files.

As I process the read operation and shove list items into the unordered list, I’ll also add some hidden data- attributes to the item to store things like the title, description, id, and etag, that will be needed for other CRUD operations later on, so a list item will end up looking like this:

The Service Proxy Implementation

The service proxy implementation is shown below. It’s virtually the same as the service proxy from the previous post, except the dependencies it imports and the URL patterns it allows.
 
As before, I need to have a WebPartPages:AllowFraming control on the page in order to allow the page to be loaded in a cross-resource iframe.
 
And of course, instead of loading jQuery as a dependency, now I’m loading a fetch polyfill as a dependency, and also loading an ES6 Promise polyfill if needed.
 
The two URL patterns I’ve specified below are all that is required in order to allow CRUD operations specifically on the Pictures library.

The Proxy Client Implementation

This implementation is going to be quite large for a blog post, so I’m not going to show the full source code here. I’ll just show the script for various operations and include the full source as a download at the end of the post. And in general, I’m not going to talk much about the code, but rather just point out differences between this code and the code of Ugly SPA Revisited using Fetch and REST, since that’s really what we’re interested in for this post. For more information on the basic code, consult that post.

CORS Read

Below is the implementation of the read operation in the ugly SPA, using the CORS wrapper. I’ve highlighted just the lines of code that have changed (as compared to Ugly SPA Revisited using Fetch and REST). And it isn’t that many lines, which is great since the goal was to make CORS as easy as plain old Ajax. The changes are:
 
  • First, of course, I need to instantiate an instance of SPCORS.ProxyClient, and lock it down by specifying origin patterns for site collections that are allowed to message us.
  • Then, replace the call to fetch with a call to spcors.fetch.
And that’s it. The arguments that are passed to fetch don’t change, and the result is exactly the same. This seems like a good start.

CORS Create

And below is the implementation of create, using the CORS wrapper, again with the changes highlighted. All two lines of them. The changes are:
 
  • Again, change fetch to spcors.fetch.
  • Changed the call to reader.readAsArrayBuffer(file); to reader.readAsDataURL(file);.
Now the first change is obvious enough, but the second change may require a bit of explanation. FileReader.readAsArrayBuffer reads a file as binary data suitable for sending to SharePoint RESTful web services as an image to be saved. The problem is that now we’re using post messages to work around CORS issues. The only way to send post messages between frames in a way that is cross-browser compatible is to send them as a string. So I’m taking the init object that is to be passed to fetch and calling JSON.stringify on it to convert it to a string. And if that init object has a body that is a binary payload, then JSON.stringify is going to mangle that all to hell. Sorry, that’s the actual technical jargon, “mangle that all to hell.”
 
So I need to encode that binary payload somehow. Encode really just means convert to a JavaScript formatted string. And that’s exactly what readAsDataURL does. It returns a string, in the format of a data URI, with the payload base64 encoded (it may theoretically not be encoded at all or encoded some other way, but for images, it will be base64 encoded). So for instance:
 
data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D
 
is a base64 encoded representation of the plain text “Hello%2C%20World!”. The parts of this are:
 
  1. data: (indicates a data URI)
  2. text/plain (the mime type of the payload)
  3. base64 (the encoding of the payload)
  4. And a comma, followed by the encoded payload.
Now don’t get all glazed over, you don’t really need to understand this, readAsDataURL is going to take care of all of this for you, so the string that is returned as the file contents will be suitable for transmission as a post message. You should be aware that this encoding is going to bloat the size of the upload by 33%. And while the specification for post messages doesn’t limit the size of post messages, some browsers may.
 
And I do need to know about data URIs. Because before that data is transmitted to SharePoint, I need to convert it back to binary data. That means that first, I need to recognize it as a data URI, and then I need to decode it, or what gets saved to SharePoint won’t look like an image anymore. I do that work in the ServiceProxy.fetch method, which we’ll get to later.

CORS Update and Delete

And that’s really the last interesting thing I have to say about UglySpaClient.aspx. The update and delete operations are identical to those from Ugly SPA Revisited using Fetch and REST, except that all calls to fetch have been changed to spcors.fetch. I’ve included the code in the download, but there isn’t much point in showing it here, and there’s more than enough code in this post already!

The CORS Wrapper Implementation

I’m going to briefly talk about some of the more interesting aspects of the two main classes, ServiceProxy and ProxyClient, below, and otherwise just dump the code on you.

The ServiceProxy Class

The most interesting thing about the ServiceProxy class is that it converts data URIs in the body back to binary content. The method dataUri2Blob does the work of unencoding data uris before sending the body to the RESTful web service if needed. This is a bit involved. I’ll include the references I used to figure out how to do it at the end.
 
Other than that, I just converted the $.ajax calls to fetch. And again, since this class depends on ES6 Promises and a pretty robust implementation of fetch, to get broad cross-browser support you’ll need to polyfill them.

The ProxyClient Class

And last but not least, below is the ProxyClient implementation. It has no dependency on fetch, but it does depend on ES6 Promises so it won’t work on older browsers (including all versions of Internet Explorer) without a polyfill.
Converting the ProxyClient class to use fetch and ES6 promises were not intuitive to me. This is because of some fundamental differences between jQuery Deferreds and ES6 Promises.
 
I can instantiate a jQuery Deferred instance without any arguments, and call resolve or reject on that instantiation later to keep my promise.
 
But with an ES6 Promise, you have to pass it a function on instantiation. And the promise doesn’t have resolve and reject methods, resolve and reject callbacks are passed into the function that you give to the Promise constructor. It assumes that inside that function, you’re going to do something asynchronous, like Ajax, and then call resolve or reject when your asynchronous operation resolves.
 
But our asynchronous operation isn’t going to happen inside this function, it’s going to happen in a post message event receiver. Previously, I saved the Deferred so I could resolve the promise. Now, I have to save the resolve and reject callbacks too, because they’re not members of the promise. Not a big change, but it’s a change, and it confused me at first. What can I say, sometimes I’m easily confused.
 
This implementation has one other improvement over my jQuery-based implementation. All of the work is done inside the callback to the promise constructor. It’s all wrapped inside a try/catch block. And if it catches an exception, it rejects the promise. The previous implementation created a deferred and did some other processing, and then returned the deferred. If that other processing threw an exception, then it was thrown from the function, rather than returned a promise that gets rejected. This violates a simple rule; promise-based functions should not throw exceptions. The alternative is that callers would need to handle exceptions in two different ways.

Sum Up

This concludes my series on CORS and SharePoint. Hopefully, I’ve demonstrated that with some work CORS can be just as easy to work with as Ajax. And before you turn your nose up at using my wrapper, remember that Ajax wasn’t all that fun when all we had was XMLHttpUtility to work with. If you do any work with HNSCs, you’re likely to run up against the CORS wall at some point, and post message is nasty to work with on a project of any significant complexity without some sort of abstraction. There may be some edge cases where this needs to be adjusted to work with specific web service calls, but at this point, I can’t imagine what those could be?

References

dataURItoBlob – David Gomez-Urquiza
BlobBuilder – brettjthom
Promise-based functions should not throw exceptions – Dr. Axel Rauschmayer

 
FetchCORSWrapper.zip – the complete source for this post.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to top