Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Posting formData using cypress doesn't work #1647

Closed
ahmehri opened this issue Apr 27, 2018 · 32 comments · Fixed by #16576
Closed

Posting formData using cypress doesn't work #1647

ahmehri opened this issue Apr 27, 2018 · 32 comments · Fixed by #16576
Assignees
Labels
topic: cy.request Issues related to cy.request command type: enhancement Requested enhancement of existing feature

Comments

@ahmehri
Copy link

ahmehri commented Apr 27, 2018

Current behavior:

Uploading file via POST request of formData doesn't work.

Desired behavior:

Being able to post formData using cypress POST request.

Steps to reproduce:

The following code doesn't work.

  const formData = new FormData();
  formData.set('file', new File(['data'], 'upload.txt'), 'upload.txt');
  cy.request({
    method: 'POST',
    url: '/upload/end/point',
    body: formData,
    headers: {
      'content-type': 'multipart/form-data',
    },
  });

Versions

cypress@2.1.0
ubuntu 16.04 LTS
Chrome 66.0.3359

@brian-mann
Copy link
Member

You likely just want to read in a fixtures data and set it as the body. I'm not seeing a reason to construct an arbitrary formdata object in the browser when you can bypass these restrictions directly... I guess maybe it would change your server's implementation? Do I have that right?

@jennifer-shehane jennifer-shehane added the stage: needs information Not enough info to reproduce the issue label Apr 27, 2018
@ahmehri
Copy link
Author

ahmehri commented Apr 27, 2018

Yes correct, the server is expecting multipart/form-data content type and using a fixture won't solve the issue.

@srinivasrk
Copy link

@ahmehri Were you able to find a way to work around this? Please do let me know. Thanks

@srinivasrk
Copy link

srinivasrk commented Jun 8, 2018

      cy.fixture('FF_ViewAllActiveSites_20180503_102650').then((sites) => {
        cy.request({
          url: '/api/sites',
          method: 'post',
          form: true,
          headers: {
            'content-type': 'multipart/form-data',
          },
          files: {
            file: sites
          }

        }).then((res) => {
          console.log(res);
        })
      })

I have added the file to fixtures. I should be able to call my API which expects req.files.file to be set.
Is there any way to do this. I don't want to handle drag and drop event in my dropzone component.

 files:
   { file:
      { name: 'FF_ViewAllActiveSites_20180503_102650.csv',
        data: <Buffer ef bb bf 53 69 74 65 5f 49 44 2c 53 65 6c 65 63 74 65 64 20 50 72 6f 6a 65 63 74 2c 4d 61 69 6e 74 65 6e 61 6e 63 65 20 41 73 73 69 67 6e 65 64 20 74 ... >,
        encoding: '7bit',
        truncated: false,
        mimetype: 'application/vnd.ms-excel',
        md5: 'cb06f867b21209ae26616c3841a92ff0',
        mv: [Function: mv] } },

This is how the files parameter looks when I do the request from a browser

@ahmehri
Copy link
Author

ahmehri commented Jun 11, 2018

@srinivasrk no I didn't.

@srinivasrk
Copy link

Is there anyway we can get help on this ?

@drewbrend
Copy link

drewbrend commented Jul 25, 2018

cc: @srinivasrk @ahmehri

I was able to get a formData submitted with a combination of a mix of answers I found.

I hope this helps you.

Add this to support/commands.js:

Cypress.Commands.add("form_request", (url, formData) => {
    return cy
      .server()
      .route("POST", url)
      .as("formRequest")
      .window()
      .then(win => {
        var xhr = new win.XMLHttpRequest();
        xhr.open(method, url);
        xhr.send(formData);
      })
      .wait("@formRequest");
});

Then you'd be able to do the following in your tests:

cy
  .form_request(url, formData)
  .then(response => {
    // do stuff with your response
  });

@xMarkusSpringerx
Copy link

xMarkusSpringerx commented Aug 14, 2018

@drewbrend doesn't work fo me :(
Property 'XMLHttpRequest' does not exist on type 'Window'.

@drewbrend
Copy link

I googled your error, I'm guessing you're using TypeScript? Maybe this will work?
https://stackoverflow.com/questions/41474445/xmlhttprequest-does-not-exist-on-type-window

@grantglidewell
Copy link

Has there been resolution to this? Ive tried your "form_request" function to no avail. taking the window() route and using fetch, no go.

@grantglidewell
Copy link

grantglidewell commented Dec 6, 2018

this is what the command looks like currently, My server gives me an error saying the stream ended unexpectedly. The error from Cypress is:

Error: socket hang up

let body = new FormData();

  body.append("email", email || Cypress.env("validEmail"));
  body.append("password", password || Cypress.env("validPassword"));
  cy.request({
    method: "POST",
    url: `${Cypress.env("API_AUTH")}/login`,
    headers: {
      "content-type":
        "multipart/form-data; boundary=----WebKitFormBoundaryoh3GYY6WEwLKtV1k"
    },
    body
  });

@grantglidewell
Copy link

Followup, I worked around this by using fetch() it still seems to accomplish the goal of setting the cookie in the browser.

@chit786
Copy link

chit786 commented Dec 15, 2018

cc: @srinivasrk @ahmehri

I was able to get a formData submitted with a combination of a mix of answers I found.

I hope this helps you.

Add this to support/commands.js:

Cypress.Commands.add("form_request", (url, formData) => {
    return cy
      .server()
      .route("POST", url)
      .as("formRequest")
      .window()
      .then(win => {
        var xhr = new win.XMLHttpRequest();
        xhr.open(method, url);
        xhr.send(formData);
      })
      .wait("@formRequest");
});

Then you'd be able to do the following in your tests:

cy
  .form_request(url, formData)
  .then(response => {
    // do stuff with your response
  });

I tried this solution, this works on chrome browser but not in electron both in --headed and headless moe. Somehow form_request method do not wait till the response.

@drewbrend any suggestion on this?

@Atticus29
Copy link

@chit786
I've tried the above, but get a "ReferenceError: method is not defined" when .then(win =>{}) is called. Any advice?

@Atticus29
Copy link

Atticus29 commented Dec 19, 2018

cc: @srinivasrk @ahmehri

I was able to get a formData submitted with a combination of a mix of answers I found.

I hope this helps you.

Add this to support/commands.js:

Cypress.Commands.add("form_request", (url, formData) => {
    return cy
      .server()
      .route("POST", url)
      .as("formRequest")
      .window()
      .then(win => {
        var xhr = new win.XMLHttpRequest();
        xhr.open(method, url);
        xhr.send(formData);
      })
      .wait("@formRequest");
});

Then you'd be able to do the following in your tests:

cy
  .form_request(url, formData)
  .then(response => {
    // do stuff with your response
  });

@drewbrend doesn't work for me, either.

ReferenceError: method is not defined.

Looks like it happens somewhere in the cy.server......window().then(...) chain. Error right after THEN.
I even have

Cypress.on('uncaught:exception', (err, runnable) => {
  Cy.log("Hi Mark Uncaught Exception");
  debugger;
  // returning false here prevents Cypress from
  // failing the test
  // return false;
  throw error;
})

in index.js in cypress/support. It never gets hit.

@Atticus29
Copy link

Atticus29 commented Dec 19, 2018

Whoops... when I changed, xhr.open(method, url); to xhr.open('POST', url);, the error went away.

@Songyu-Wang
Copy link
Contributor

I would hope this issue can get fixed the right way.
For us, the workaround does not work due to CORS since we are doing direct request to api in testing instead of of web server

@cypress-bot cypress-bot bot added stage: proposal 💡 No work has been done of this issue and removed stage: needs information Not enough info to reproduce the issue labels Jul 11, 2019
@NateDawg90
Copy link

@ahmehri How did you end up working around this to POST with formData? I'm having the same issue.

@ericblade
Copy link

ericblade commented Nov 26, 2019

based on this thread, i have ended up so far with:

Cypress.Commands.add('createFile', (file, type) => {
    const formData = new FormData();
    formData.set('type', type);
    return cy.fixture(file).then(fileData => {
        formData.set('file', new File([JSON.stringify(fileData)], file), file);
        return cy
            .server().route('POST', '/api/files').as('formRequest').window()
            .then(win => {
                return new Promise((resolve, reject) => {
                    const xhr = new win.XMLHttpRequest();
                    xhr.onreadystatechange = () => {
                        if (xhr.readyState === 4) {
                            resolve(xhr.response);
                        }
                    };
                    xhr.open('POST', '/api/files');
                    xhr.send(formData);
                });
            })
            .wait('@formRequest').then((xhr) => {
                return cy.wrap({ status: xhr.status, body: xhr.response.body });
            });
    });
});

... The unfortunate side of this, is that I have absolutely no idea what it's doing. On the bright side, though, at least it works.

@AlexanderHritsun
Copy link

AlexanderHritsun commented Dec 19, 2019

Good day, the workaround described above does not work for me because I try to send a file to another subdomain, that why I got 404 error because I need to set correct Referrer( that is forbidden for xhr requests)
my code

Cypress.Commands.add('form_request', (method, url, formData, accountName, tokens, done) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, false);
    xhr.setRequestHeader('Access-Control-Allow-Origin',`https://${accountName}.condogenie-app.brocoders.xyz`);
    xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
    xhr.setRequestHeader('Access-Control-Request-Method', 'POST');
    xhr.setRequestHeader('Access-Control-Allow-Credentials', 'true');
    xhr.setRequestHeader('access-token', tokens['access-token']);
    xhr.setRequestHeader('token-type', tokens['token-type']);
    xhr.setRequestHeader('client', tokens['client']);
    xhr.setRequestHeader('uid', tokens['uid']);
    xhr.onload = function () {
        done(xhr);
    };
    xhr.onerror = function () {
        done(xhr);
    };
    xhr.send(formData);
});

@CoryDanielson
Copy link
Contributor

CoryDanielson commented Feb 4, 2020

I was able to do this by sending a form request using jQuery off of the window. I chose to use a synchronous request here just to keep the code simple.

cy.window().then(({$}) => {
  const fd = new FormData();
  fd.append('foo', 'bar');
  $.ajax({
    type: 'POST',
    url: '/some/api/url',
    data: fd,
    dataType: 'json',
    async: false, // send as synchronous request, to keep this simple
    cache: false,
    contentType: false,
    processData: false,
  });
});

@shwarcu
Copy link

shwarcu commented Feb 28, 2020

If XHR request is part of your test setup and you need its response later on, here is the solution I worked out:

In a helper file you can define a function to execute the request:

export function formRequest(method, url, formData, headers) {
  const request = new XMLHttpRequest();
  request.open(method, url, false);
  if (headers) {
    headers.forEach(function(header) {
      request.setRequestHeader(header.name, header.value);
    });
  }
  request.send(formData);
  return JSON.parse(request.response);
}

Using helper function you can define a specific request to your API as:

export function customerUploadFile(accessToken, file, fileName, fileType) {
  return cy.fixture(file, 'binary').then(function(binary) {
    return Cypress.Blob.binaryStringToBlob(binary, fileType).then(function(blob) {
      const targetUrl = `${apiUrl}/${apiVersion}/customer/action/upload_file?license_id=${licenseId}`;
      const headers = [{ name: 'Authorization', value: `Bearer ${accessToken}` }];
      const formData = new FormData();
      formData.set('file', blob, fileName);
      return formRequest('POST', targetUrl, formData, headers);
    });
  });
}

And your custom command:

Cypress.Commands.add('customerUploadFile', function(customerToken, fixtureName) {
  return cy.fixture(fixtureName).then(function(fixture) {
    const { file, fileName, fileType } = fixture;
    return customerUploadFile(customerToken, file, fileName, fileType);
  });
});

Now in your fixtures you can prepare following files:

  • fixtures/image/image.jpg
  • fixture/image.json
{
  "file": "images/image.jpg",
  "fileName": "image.jpg",
  "fileType": "image/jpg"
}

And in your test you can easily handle such image upload:

    it('test', function() {
      const fixtureName = 'image';
      const constAuthToken = 'however you obtain it';
      cy.customerUploadFile(constAuthToken, fixtureName).then(function(uploadResponse) {
       cy.log(uploadResponse);
        });
      });
    });

This will get necessary information about image from the json fixture and then get the binary and upload it.

@Nasicus
Copy link

Nasicus commented Mar 4, 2020

As far as I can see, all this code in here is way too complicated and uses too many "custom" XHR requests and whatsoever, I wrote this simple command:

Cypress.Commands.add('fileRequest', (filePath, requestOptions) => {
  return cy
    .fixture(filePath, 'binary')
    .then(binary => Cypress.Blob.binaryStringToBlob(binary))
    .then(blob => {
      const formData = new FormData();
      formData.set('file', blob);

      return cy.request({ ...requestOptions, form: true, body: formData });
    });
});

The typescript signature for this function is like this:
fileRequest(filePath: string, requestOptions: Partial<RequestOptions>): Chainable<Response>;

and it can be called like this:

 cy.fileRequest(filePath, {
    method: 'POST',
    url: `/attachments/files/my-new-file`,
    headers: {
      authorization: `Bearer ${peterReaderAad.token}`
    }
  });

Since the second parameter is RequestOptions you can use the same object as is used by the official cy.request method.

@shwarcu
Copy link

shwarcu commented Mar 17, 2020

@Nasicus your solution doesn't result in request with correct multipart/form-data; boundary=--- header

@xap5xap
Copy link

xap5xap commented Jun 10, 2020

This worked for me with cypress 4.8.0

   cy.request({
      url: "https:/....",
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      form: true,
      body:{ some body}
    }).then(response => {
      console.log("response", response);
    });

@ryanwelch-degreed
Copy link

If XHR request is part of your test setup and you need its response later on, here is the solution I worked out:

In a helper file you can define a function to execute the request:

export function formRequest(method, url, formData, headers) {
  const request = new XMLHttpRequest();
  request.open(method, url, false);
  if (headers) {
    headers.forEach(function(header) {
      request.setRequestHeader(header.name, header.value);
    });
  }
  request.send(formData);
  return JSON.parse(request.response);
}

Using helper function you can define a specific request to your API as:

export function customerUploadFile(accessToken, file, fileName, fileType) {
  return cy.fixture(file, 'binary').then(function(binary) {
    return Cypress.Blob.binaryStringToBlob(binary, fileType).then(function(blob) {
      const targetUrl = `${apiUrl}/${apiVersion}/customer/action/upload_file?license_id=${licenseId}`;
      const headers = [{ name: 'Authorization', value: `Bearer ${accessToken}` }];
      const formData = new FormData();
      formData.set('file', blob, fileName);
      return formRequest('POST', targetUrl, formData, headers);
    });
  });
}

And your custom command:

Cypress.Commands.add('customerUploadFile', function(customerToken, fixtureName) {
  return cy.fixture(fixtureName).then(function(fixture) {
    const { file, fileName, fileType } = fixture;
    return customerUploadFile(customerToken, file, fileName, fileType);
  });
});

Now in your fixtures you can prepare following files:

  • fixtures/image/image.jpg
  • fixture/image.json
{
  "file": "images/image.jpg",
  "fileName": "image.jpg",
  "fileType": "image/jpg"
}

And in your test you can easily handle such image upload:

    it('test', function() {
      const fixtureName = 'image';
      const constAuthToken = 'however you obtain it';
      cy.customerUploadFile(constAuthToken, fixtureName).then(function(uploadResponse) {
       cy.log(uploadResponse);
        });
      });
    });

This will get necessary information about image from the json fixture and then get the binary and upload it.

Thanks @shwarcu this is pretty much everything you need. There are a couple of little things you need to make sure you do with this.

  • cy.fixture() in combination with "Cypress.Blob.binaryStringToBlob()" does not appear to parse the file correctly, unless you specify 'binary' in cy.fixture, i.e. cy.fixture('filepath', 'binary'). If you exclude this the FormData object is not created correctly.
  • Make sure you do not specify the content type when using multipart/form-data. The boundary needs to be added to that header in the request, so we need to let the request add that header on it's own.
  • logging anything about the FormData or request in this situation is inaccurate. Cypress does not capture the actual request being sent, so you would need to capture the request with a different tool to see what is actually being sent.
  • with Cypress 5 and up, the "Cypress.Blob.binaryStringToBlob" can be assigned directly to a variable as it is no longer a promise.

It would really be ideal for Cypress to add support for multipart/form-data requests using their cy.request() command, but this is a reasonable workaround for the moment. This is very picky on how everything is set up and executed, so it is likely to break with future Cypress updates.

@bahmutov bahmutov added topic: cy.request Issues related to cy.request command type: enhancement Requested enhancement of existing feature labels Sep 15, 2020
@anjalichhabra29
Copy link

cc: @srinivasrk @ahmehri
I was able to get a formData submitted with a combination of a mix of answers I found.
I hope this helps you.
Add this to support/commands.js:

Cypress.Commands.add("form_request", (url, formData) => {
    return cy
      .server()
      .route("POST", url)
      .as("formRequest")
      .window()
      .then(win => {
        var xhr = new win.XMLHttpRequest();
        xhr.open(method, url);
        xhr.send(formData);
      })
      .wait("@formRequest");
});

Then you'd be able to do the following in your tests:

cy
  .form_request(url, formData)
  .then(response => {
    // do stuff with your response
  });

I tried this solution, this works on chrome browser but not in electron both in --headed and headless moe. Somehow form_request method do not wait till the response.

@drewbrend any suggestion on this?

Hi,

I'm facing same issue on Electron Browser.. Any solution on this so far ?

@kimgysen
Copy link

lmfao is all I can say.

@JesusSalinas
Copy link

JesusSalinas commented Feb 24, 2021

Hi everyone!
is this one still in progress?

@jakubmichalec
Copy link

jakubmichalec commented Apr 2, 2021

workaround which work with .net controller IFormFile:
need some styling tweaks but working ;)

    cy.readFile(`cypress/fixtures/file.xml`).then((str) => {
        let blob = new Blob([str], { type: "text/plain" });
        let formData = new FormData();
        formData.append("file", blob);

        const fileBody = [
            "--someBoundaryGuidGeneratedByYou",
            'Content-Disposition: form-data; name="file"; filename="someFIleName"',
            "Content-Type: text/plain\r\n",
            str,
            "--someBoundaryGuidGeneratedByYou--\r\n",
        ].join("\r\n");

        cy.request({
            method: "POST",
            url:
                "api/...",
            auth: {
                bearer: token // generatedToken,
            },
            body: fileBody ,
            headers: {
                "content-type": "multipart/form-data; boundary=someBoundaryGuidGeneratedByYou",
            },
        });
    });

@cypress-bot cypress-bot bot added stage: work in progress stage: needs review The PR code is done & tested, needs review and removed stage: proposal 💡 No work has been done of this issue stage: work in progress stage: needs review The PR code is done & tested, needs review labels May 19, 2021
@cypress-bot
Copy link
Contributor

cypress-bot bot commented May 24, 2021

The code for this is done in cypress-io/cypress#16576, but has yet to be released.
We'll update this issue and reference the changelog when it's released.

@cypress-bot cypress-bot bot removed the stage: needs review The PR code is done & tested, needs review label May 24, 2021
@cypress-bot
Copy link
Contributor

cypress-bot bot commented May 24, 2021

Released in 7.4.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v7.4.0, please open a new issue.

@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators May 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
topic: cy.request Issues related to cy.request command type: enhancement Requested enhancement of existing feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.