Uploading Attachments to SharePoint Lists Using REST

In a previous post, I walked through Uploading Attachments to SharePoint Lists Using SPServices. Well, in the “modern” world, I want to use REST whenever I can, especially with SharePoint Online.

Add an attachmentI ran into a challenge figuring out how to make an attachment upload work in an AngularJS project. There are dozens of blog posts and articles out there about uploading files to SharePoint, and some of them even mention attachments. But I found that not a single one of the worked for me. It was driving me nuts. I could upload a file, but it was corrupt when it got there. Images didn’t look like images, Excel couldn’t open XLSX files, etc.

I reached out to Julie Turner (@jfj1997) and asked for help, but as is often the case, when you’re not as immersed in something it’s tough to get the context right. She gave me some excellent pointers, but I ended up traversing some of the same unfruitful ground with her.

Finally I decided to break things down into the simplest example possible, much like I did in my earlier post with SOAP. I created a test page called UploadTEST with a Content Editor Web Part pointing to UploadText.html, below:

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.js"></script>

<input id="my-attachments" type="file" fileread="run.AttachmentData" fileinfo="run.AttachmentInfo" />

<script type="text/javascript" src="/sites/MySiteCollection/_catalogs/masterpage/_MyProject/js/UploadTEST.js"></script>

Like I said, I wanted to keep it simple. Here’s what I’m doing:

  • In line 1, I load a recent version of jQuery from cdnjs. On the theory that Angular just adds another layer of complexity, I wanted to try just jQuery, which is useful for its $.ajax function, among many other things.
  • In line 3, I’ve got an input field with type=”file”. In “modern” browsers, this gives us a familiar file picker.
  • In line 5, I’m loading my script file called UploadTest.js, below:
$(document).ready(function() {

 var ID = 1;
 var listname = "UploadTEST";

 $("#my-attachments").change(function() {

  var file = $(this)[0].files[0];

  var getFileBuffer = function(file) {

   var deferred = $.Deferred();
   var reader = new FileReader();

   reader.onload = function(e) {
    deferred.resolve(e.target.result);
   }

   reader.onerror = function(e) {
    deferred.reject(e.target.error);
   }

   reader.readAsArrayBuffer(file);

   return deferred.promise();
  };

  getFileBuffer(file).then(function(buffer) {

   $.ajax({
    url: _spPageContextInfo.webAbsoluteUrl +
     "/_api/web/lists/getbytitle('" + listname + "')/items(" + ID + ")/AttachmentFiles/add(FileName='" + file.name + "')",
    method: 'POST',
    data: buffer,
    processData: false,
    headers: {
     "Accept": "application/json; odata=verbose",
     "content-type": "application/json; odata=verbose",
     "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
     "content-length": buffer.byteLength
    }
   });

  });

 });
});

Here’s how this works – and yes, it does work!

  • I’ve got everything wrapped in a $(document).ready() to ensure the page is fully loaded before my script runs.
  • Lines 3-4 just set up some variables I could use for testing to keep things simple.
  • In line 6, I bind to the change event for the input field. Whenever the user chooses a file in the dialog, this event will fire.
  • In line 8, I’m getting the information about the file selected from the input element.
  • Lines 10-26 is the same getFileBuffer function I used with SOAP; it’s where we use a FileReader to get the contents of the selected file into a buffer.
  • The function in lines 28-42 runs when the file contents have been fully read. The getFileBuffer function returns a promise, and that promise is resolved when the function has gotten all of the file contents. With a large file, this could take a little while, and by using a promise we avoid getting ahead of ourselves. Here we make the REST call that uploads the attachment.
    • The URL ends up looking something like: “/my/site/path/_api/web/lists/getbytitle(‘UploadTEST’)/items(1)/AttachmentFiles/add(FileName=’boo.txt’)”
    • The method is a POST because we’re writing data to SharePoint
    • The data parameter contains the “payload”, which is the buffer returned from the getFileBuffer function.
    • The headers basically tell the server what we’re up to:
      • Accept doesn’t really come into play here unless we get a result back, but it says “send me back json and be verbose”
      • content-type is similar, but tells the server what it is getting from us
      • X-RequestDigest is the magic string from the page that tells the server we are who it thinks we are
      • content-length tells the server how many bytes it should expect

This worked for me on a basic Custom List (UploadTEST). So now I knew it was possible to upload an attachment using REST.

I took my logic and shoved it back into my AngularJS page, and it still worked! However, when I switched from $.ajax to AngularJS’s $http, I was back to corrupt files again. I’m still not sure why that is the case, but since I’m loading jQuery for some other things, anyway, I’ve decided to stick with $.ajax for now instead. If anyone reading this has an idea why $http would cause a problem, I’d love to hear about it.

Once again, I’m hoping my beating my head against this saves some folks some time. Ideally there would be actual documentation for the REST endpoints that would explain how to do things. Unfortunately, if there is for this, I can’t find it. I’d also love to know if /add allows any other parameters, particularly whether I can overwrite the attachment if one of the same name is already there. Maybe another day.

Similar Posts

32 Comments

  1. Have you checked the requests with fiddler or even chrome developer console to see if $http is adding and headers or changing anything.

  2. Hey Marc , I had the same problem when uploading the files through the angular’s $http service. My understanding was that the $http service by default was transforming the array buffer that we are sending through POST into some json format that corrupts the file we attach. So i had passed in the $http service the parameter “transformRequest: angular.identity” from the $http service to stop converting my array buffers to json format .[I would love to know if my assumptions were true or not]

  3. And this is just one more reason why AngularJS SUCKS! It adds complexity that jQuery alone solves just fine. Please spread the word. I’m sick of companies REQUIRING you to know/use AngularJS just because the developer community seems to love it so much! It does nothing that you can’t just code on your own without it. Thank you for this post.

  4. Marc, I was working on getting twitter typeahead and bloodhound working with SharePoint’s REST service. After a few hours of going through the code. I realized that Bloodhound was sending the $.ajax request with the headers: “application/json” which in the Microsoft world means to return the results in XML. As you have the headers set above in your $.ajax call you are setting the headers as “application/json; odata=verbose”. Why MS needs the “odata=verbose” is beyond me. The $http service is probably doing the same thing. I don’t have enough in-depth knowledge of that service. Hopefully it is using an $.ajax call. If that is the case you might be able to solve it with adding:

    $.ajaxSetup({headers: “application/json; odata=verbose”});

    If you are interested I can send what I did to get the twitter typehead/Bloodhound working with SharePoint 2013 if you want to post that solution.

    Mike

    1. Hi Mike, I recently started looking at adopting Twitter typeahead into our SharePoint site and converting all my JSOM apps to use REST/JSON. Would you mind sharing your solution on how you integrated Bloodhound with the REST service? Much appreciated.

      1. // Used to generate the query string for querying multiple items from different fields in a list
        var urlMaker = “https://your_stie_url/_api/web/lists/getbytitle(‘Phonebook’)/items?\
        $select=Lookup_x0020_Name,ID&$filter=substringof(‘%QUERY’,Title) or \
        substringof(‘%QUERY’,FirstName) or \
        substringof(‘%QUERY’,Preferred_x0020__x0020_Name) or \
        substringof(‘%QUERY’,Company) or \
        substringof(‘%QUERY’,Office) or \
        substringof(‘%QUERY’,Supporting_x0020_Unit) or \
        substringof(‘%QUERY’,Title) or \
        substringof(‘%QUERY’,Email2)”;
        urlMaker =urlMaker.replace(/\s+/g,” “);

        // Instantiate the Bloodhound suggestion engine
        var members = new Bloodhound({
        datumTokenizer: function(datum) {
        return Bloodhound.tokenizers.whitespace(datum.value);
        },
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
        wildcard: ‘%QUERY’,
        url: urlMaker,
        transform: function(response) {
        // Map the remote source JSON array to a JavaScript object array
        x = response.d.results;
        return $.map(response.d.results, function(member) {
        return {
        value: member.Lookup_x0020_Name,
        id: member.ID
        };
        });
        }
        }
        });
        members.initialize();

        // Instantiate the Typeahead UI
        $(‘.typeahead’).typeahead(null, {
        display: ‘value’,
        highlight: true,
        source: members.ttAdapter()
        }).on(‘typeahead:selected’, function (obj, datum) {
        console.log(“Typeahead selected is ” + datum.id );
        });

        /* you might also need to switch lines 1726 and 1727 in the typeahead.bundle.js file
        this is a known bug that has not been corrected.

        1726 rendered += suggestions.length;
        1727 that._append(query, suggestions.slice(0, that.limit – rendered));

        …. goes to

        that._append(query, suggestions.slice(0, that.limit – rendered));
        rendered += suggestions.length;

  5. Marc,

    Angular’s default transformRequest is kind of a pain in the neck. On the $http service, it calls angular.toJson() on any outgoing object unless it is a file, blob, or formData.

    // transform outgoing request data
    transformRequest: [function(d) {
    return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d;
    }],

    Since the file is being read into an ArrayBuffer, that makes it a typeof object, not a file, so Angular does attempt to convert it into JSON.

    jQuery provides the a property for “processData: false” which is missing from Angular. Like someone suggested above, I just override the default requestTransform with one of my own:

    transformRequest: function (data, headersGetter, status) { return data; }

    I appreciate all the info you share, so I hope this is helpful info in return!

    Shelly

  6. Marc,
    You mentioned using Angular in this post, and using Knockout in a previous post. I’m curious about your overall approach to SharePoint development. Are you developing in the .NET environment, or using another approach?

    Thanks,

    Charles

  7. marc its a nice article. Is there any way to add an attachment that is located in a URL path instead of from a input tag

      1. Thanks for the great explanation, helped me a lot.

        I’m also looking for a way to copy a document from a document library to a list attachment, so I have the url of the document source.

        Do you have an example of how to do this, use the uploading part of your code and add extra code to get the file form the url?

  8. Sir,
    I am facing a similar problem, in my case, i try to upload a file to SPO document library from my external web application by jquery ajax, it shows an error: 401 Unauthorized, the request included the authorization token already

    When i try to use $http of angular, no error message, the file is uploaded but corrupt like your case.

    Appreciate a lot if you can help,

    Thank You,
    Tuan Nguyen

      1. Thank you for your quick reply. I’ve found the solution in another comment on this post. Adding “transformRequest: angular.identity” to $http method really fix my problem.

Leave a Reply to Aravind (@Arvind_Mp) Cancel reply

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

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