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.
I 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.
Have you checked the requests with fiddler or even chrome developer console to see if $http is adding and headers or changing anything.
@bmath:
That would be a good next step, but I’m moving on for now. It works.
M.
Hey Marc, could it be that it’s the same issue as with uploading files to a Group? I have some sample code you could test for your scenario at https://blog.mastykarz.nl/2-practical-tips-office-365-group-files-api. Hope it helps.
@Waldek:
Could be. Thanks for the pointer!
M.
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]
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.
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
@Mike:
odata=verbose
is especially helpful while you’re debugging. There are several other modes available on Office 365:[verbose | minimalmetadata | nometadata | [none] ]
This post on the Office blogs spells it all out: https://blogs.office.com/2014/08/13/json-light-support-rest-sharepoint-api-released/
M.
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.
// 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;
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
@Shelly:
That’s excellent info. It seems like a flaw in the function, IMO. At least knowing about it we can work around it.
Thanks,
M.
@shelly – Good information! You can still use “transformRequest: angular.identity” in the $http service to stop your request from transforming. Thanks!
@Aravind, It works perfectly, thank you very much
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
@Charles:
For years now, my approach has been almost 100% client side, whether simply JavaScript or using libs and frameworks.
M.
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
@Narasimha:
Sure. You can just use the uploading part of the code if you already have the URL.
M.
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?
@Rob:
You should be able to figure this put by parsing apart the two sections of code here. You can call GetListItems to get the URL of the document.
M.
Is there any way to upload attachment without FileReader?
@Andrew:
The answer here is certainly “it depends”. You need something client side that can handle the file for you.
M.
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
@Tuan:
It’s hard to say what the issue is without going through your code. I’d suggest posting it to a public forum like SharePoint StackExchange. It’s much easier to work through code on a site like that than it is here.
M.
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.