mgonto / restangular

AngularJS service to handle Rest API Restful Resources properly and easily
MIT License
7.87k stars 840 forks source link

multipart form data submit with restangular #420

Closed zoheb closed 10 years ago

zoheb commented 10 years ago

I have to submit a form which includes a user selected file to the server. I can get this working using $http directly as below,

    var fd = new FormData();
            fd.append("profile",angular.toJson(profile));
             fd.append("file", file);
            return $http.post("/server/api/profile/me/bio", fd, {
                withCredentials: true,
                headers: {'Content-Type': undefined },
                transformRequest: angular.identity
            });

I tried to do the same submit using restangular as below return userAPI.one('me').customPOST(fd,"bio",{}, {'Content-type':undefined});

However this doesn't do the multipart submit correctly and attempts to send a empty payload request. I suspect this is because I need to specify the transformRequest flag to address this issue: https://github.com/angular/angular.js/issues/1587

Is there a way to do set a request transformer just on this request?

Thanks

mgonto commented 10 years ago

Hey,

You can do that in the latest version of Restangular

userAPI.one('me').withHttpConfig({transformRequest: angular.identity}).customPOST(...`

That should work for you :).

Also, all other $httpConfigurations can be set in that withHttpConfig call as a parameter.

Please tell me if it worked and reopen if it didn't

zoheb commented 10 years ago

Thanks for the reply.

I updated to the latest version and changed to use withHttpConfig as you mentioned, but it still didn't quite work. Now the request payload is not empty but has the string "[object Object]". Note that the object I am passing in ("fd") is a FormData object.

mgonto commented 10 years ago

Did you set there the withCredentials and contentType as well?

It should be the same with those.

Can you debug https://github.com/mgonto/restangular/blob/master/src/restangular.js#L457 and check what's being sent in to $http? You can compare that to see what's going on.

zoheb commented 10 years ago

Everything seems to be the same except the "data" attribute shows up in the debugger as "Object" instead of "FormData" when I used $http directly and put a breakpoint in angular.js. Since it's no longer a FormData object I think $http skips all the multipart handling stuff.

I wasn't able to figure out where the passed in FormData object was being changed in the restangular codebase. When a formdata object is passed in think it should just preserve it.

zoheb commented 10 years ago

Typo - I meant is shows up as "Object" when using restangular, but shows up correctly as FormData object when using $http

mgonto commented 10 years ago

I know what it's. The problem is that I'm stripping Restangular stuff. As a hack go to elemFunction and remove StripRestangular part. 

If that works Ill get it fixed :) 

— Mobile mail. Excuse brevity and typos.

On Thu, Nov 21, 2013 at 8:28 PM, zohebsait notifications@github.com wrote:

Typo - I meant is shows up as "Object" when using restangular, but shows up correctly as FormData object when using $http

Reply to this email directly or view it on GitHub: https://github.com/mgonto/restangular/issues/420#issuecomment-29035132

zoheb commented 10 years ago

Yes! I temporarily commented out lines 918-921 shown below and it's works! I will work around till you push a fix. Thanks for your help.

//                  if (_.isObject(callObj)) {
//                      callObj = stripRestangular(callObj);
//                  }
mgonto commented 10 years ago

Thank you. 

Ill leave this open meanwhile

— Mobile mail. Excuse brevity and typos.

On Thu, Nov 21, 2013 at 8:44 PM, zohebsait notifications@github.com wrote:

Yes! I temporarily commented out lines 918-921 shown below and it's works! I will work around till you push a fix. Thanks for your help.

//                  if (_.isObject(callObj)) {
//                      callObj = stripRestangular(callObj);
//                  }

Reply to this email directly or view it on GitHub: https://github.com/mgonto/restangular/issues/420#issuecomment-29036205

alonisser commented 10 years ago

was this fixed?

mgonto commented 10 years ago

Not yet, please comment those lines temporarily. I haven’t had much time lately to work on this bugs, but I’ll try to work on them ASAP.


Martin Gontovnikas Software Engineer Buenos Aires, Argentina

Twitter: @mgonto (https://twitter.com/mgonto) Linkedin: http://www.linkedin.com/in/mgonto Github: https://github.com/mgonto

On Wednesday, December 4, 2013 at 9:58 AM, Alonisser wrote:

was this fixed?

— Reply to this email directly or view it on GitHub (https://github.com/mgonto/restangular/issues/420#issuecomment-29802349).

mgonto commented 10 years ago

Pushing fix :)

alonisser commented 10 years ago

great!

Twitter:@alonisser https://twitter.com/alonisser LinkedIn Profile http://www.linkedin.com/in/alonisser Facebook https://www.facebook.com/alonisser _Tech blog:_4p-tech.co.il/blog _Personal Blog:_degeladom.wordpress.com Tel:972-54-6734469

On Mon, Dec 9, 2013 at 10:49 PM, Martin Gontovnikas < notifications@github.com> wrote:

Pushing fix :)

— Reply to this email directly or view it on GitHubhttps://github.com/mgonto/restangular/issues/420#issuecomment-30171863 .

deltaepsilon commented 10 years ago

This thread just saved my bacon, but I wasted some time trying to figure out how to set the headers correctly. Here's a solution that worked for me. It's Coffeescript. Sorry. Not my choice.

    postIt = () ->
      formData = new FormData()
      formData.append('file', file) # file is an ArrayBuffer read with fileReader.readAsArrayBuffer(file)
      formData.append('name', file.name)

      apiAuth.one('api/client', client.id).one('note', note.id).withHttpConfig({transformRequest: angular.identity}).customPOST(formData, 'file', undefined, {'Content-Type': undefined}).then (res)->
        deferred.resolve(res)
gen4sp commented 9 years ago

It still here, guys. I have same problem with 1.4 What i can do with it?

Restangular.all('points').withHttpConfig({transformRequest: angular.identity}).customPOST(formData,'',undefined,{'Content-Type': undefined}).then(function (response) {
ncourtial commented 9 years ago

Hi, All seems to be OK when you don't set global default headers. When adding the following default values in config step, form data are posted without requested headers : form-data and boundary but with 'application/json'. RestangularProvider.setDefaultHeaders({ "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest" });

When removing "Content-Type": "application/json" from config , customPOST(formData,'',undefined,{'Content-Type': undefined}) does its stuff.

Any idea?

Many thanks in advance.

Cheers

Tallyb commented 9 years ago

+1

diosney commented 9 years ago

Hi!

I'm trying to implement this but can't figure it out with my use case since it seems that Restangular.service('entities') doesn't have a valid .withHttpConfig() method.

How can achieve that functionality using the service approach?

Thanks in advance.

diosney commented 9 years ago

It seems that Restangular.service('entities') only exports getList(), get(), post() methods :(

Nevertheless, in my Entities service I changed Restangular.service('entities') for Restangular.all('entities') and the file upload worked like a charm.

steinerj commented 9 years ago

I'm having the exact same issue with using httpConfigs for objects created with .service

I have provided a PR, because I think this is a bit of a inconsistency. See #1068

augnustin commented 9 years ago

Thanks for comments and code snippets. It helps quite a lot!

I have a working implementation but I'm halfly satified because it uses Base64-encoded data upload which seems less efficient than Multipart Upload, and would not work with heavy file (as far as I understood).

@deltaepsilon I tried your code but it only sends the file itself, whereas I would like to update my user during the same call. It should be possible as that's what a regular form does.

Here's the code:

View:

<img ng-src="{{user.avatar}}" class="img-circle img-responsive">
<input type="file" name="avatar" onchange="angular.element(this).scope().updateClientImage(this.files)" />
<input class="btn btn-success btn-lg" type="submit" value="Save" ng-click="save_user()" />

Controller:

        $scope.save_user = function() {
            var deferred;
            // if ($scope.file) {
            //     var formData = new FormData();
            //     formData.append('user[avatar]', $scope.file);
            //     deferred = $scope.user.withHttpConfig({transformRequest: angular.identity}).customPUT(formData, undefined, undefined, {'Content-Type': undefined});
            // } else {
                deferred = $scope.user.put();
            // }
            deferred.then(function(res) {
                console.log(res.msg);
            }, function(err){
                console.log('An error occured:');
                console.log(err);
            });
        };

        $scope.updateClientImage = function(files) {
            $scope.file = files[0];
            var reader = new FileReader();
            reader.onload = function(e) {
                $scope.user.avatar = e.target.result;
                $scope.$apply();
            };
            reader.readAsDataURL($scope.file);
        };

Simply calling $scope.user.put() sends a Base64-encoded image as a user.avatar attributes, which works Ok in my backend.

But how can I have a customPUT that sends over the user attributes + the formData - user.avatar (otherwise it is sent twice)?

 $scope.user.withHttpConfig({transformRequest: angular.identity}).customPUT(formData, undefined, undefined, {'Content-Type': undefined});

I'll report final working solution. Thanks a lot!

ArthurianX commented 9 years ago

Any progress on this? I'm having the same problem. :(

Waxo commented 9 years ago

I'm using jsPDF to create a PDF then i try to submit it with return Restangular.all('users').one('pdf', id).withHttpConfig({transformRequest: angular.identity}).customPOST(fd, 'submit', undefined, {'Content-Type': undefined});
But the header is application/json and i need a multipart/* If i force the Content-Type to multipart/form-data i have a boundary problem. Is there a workaround ?

rhodul commented 9 years ago

@ncourtial, the default headers problem is fixed like so: postFile: function (dataUri, path, keyName, fileName) { var authenticatedHeaders = angular.extend({}, DEFAULT_HTTP_HEADERS, {"x-authtoken": localStorage.authtoken}); authenticatedHeaders["Content-Type"] = undefined; var formData = new FormData(); var blob = dataURItoBlob(dataUri); formData.append(keyName, blob, fileName); // kill default headers var restangularRoute = Restangular.withConfig(function (RestangularConfigurer) { RestangularConfigurer.setDefaultHeaders(authenticatedHeaders); }); return restangularRoute.one(path).withHttpConfig({transformRequest: angular.identity}) .customPOST(formData, '', undefined, authenticatedHeaders); }

suvarnajayanth commented 8 years ago

if i set ["Content-Type"] = undefined, It calls webservice , but i i want to send text data in other language (utf-8) it won't receive as expected in Rest api

andrzej-aa commented 8 years ago

I found a workaround for setting default headers. Instead of setting undefined, set a function returning undefined:

RestangularProvider.setDefaultHeaders({
  'Content-Type': 'application/json'
});

return API.all(UPLOAD_RESOURCE)
  .withHttpConfig({transformRequest: angular.identity})
  .customPOST(file, 'image', {}, {
    'Content-Type': () => {
      return undefined;
    }
  });
abbood commented 6 years ago

I can't seem to have it work.. this is what I'm using:

I'm using restangular 1.51 with angular 1.6. My html looks like this

<input type="file" name="file" />

and the angular code:

let directive = {
  ..
  link: (scope, element, attrs) => {
        let inputElement = angular.element(element[0].querySelector('input'));
        inputElement.bind('change', function () {
        var formData = new FormData();
        formData.append('file', inputElement[0].files[0]);

        API.all('stores/csv').withHttpConfig({transformRequest: angular.identity})             .customPOST(formData,'' , undefined,
          { 'Content-Type': undefined }).then((response) => {console.log(response);
          });
    });

laravel code:

public function upload(Request $request)
{

    $this->validate($request, [
        'file' => 'required',
        'type'  => 'in:csv,xls,xlsx',
        ]);

    $file = $request->input('file');
    var_dump($file);
    return response()->success(['file' => $file]);
}

thing is the $file here is appearing as an empty array in the laravel dump. The documentation is pretty bad on this. Ideas?

abbood commented 6 years ago

so basically i had to brute force the content-type to application/x-www-form-urlencoded..

btw i'm using restangular 1.51.. more details here https://stackoverflow.com/questions/49077674/how-to-upload-files-with-angular-using-cors/49081727#49081727