From c2588798a03aebcdb46bbdf7dd4a2056f88b3d8d Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Mon, 24 May 2021 23:07:44 +0900 Subject: [PATCH] fix: can send FormData with File. (#16576) --- .../integration/commands/request_spec.js | 26 ++++++++++ packages/driver/cypress/plugins/server.js | 6 +++ packages/driver/package.json | 1 + packages/driver/src/cy/commands/request.js | 50 ++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress/integration/commands/request_spec.js b/packages/driver/cypress/integration/commands/request_spec.js index e03c033f93b4..b466141e3613 100644 --- a/packages/driver/cypress/integration/commands/request_spec.js +++ b/packages/driver/cypress/integration/commands/request_spec.js @@ -499,6 +499,32 @@ describe('src/cy/commands/request', () => { expect(dec.decode(response.body)).to.contain('1,2,3,4') }) }) + + it('can send FormData with File', () => { + const formData = new FormData() + + formData.set('file', new File(['1,2,3,4'], 'upload.txt'), 'upload.txt') + formData.set('name', 'Tony Stark') + cy.request({ + method: 'POST', + url: 'http://localhost:3500/dump-form-data', + body: formData, + headers: { + 'content-type': 'multipart/form-data', + }, + }) + .then((response) => { + expect(response.status).to.equal(200) + // When user-passed body to the Nodejs server is a Buffer, + // Nodejs doesn't provide any decoder in the response. + // So, we need to decode it ourselves. + const dec = new TextDecoder() + const result = dec.decode(response.body) + + expect(result).to.contain('Tony Stark') + expect(result).to.contain('upload.txt') + }) + }) }) describe('subjects', () => { diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index 03f810960682..45ed8a2cb4be 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -6,6 +6,8 @@ const http = require('http') const httpsProxy = require('@packages/https-proxy') const path = require('path') const Promise = require('bluebird') +const multer = require('multer') +const upload = multer({ dest: 'cypress/_test-output/' }) const PATH_TO_SERVER_PKG = path.dirname(require.resolve('@packages/server')) const httpPorts = [3500, 3501] @@ -144,6 +146,10 @@ const createApp = (port) => { return res.send(`it worked!
request body:
${req.body.toString()}`) }) + app.all('/dump-form-data', upload.single('file'), (req, res) => { + return res.send(`it worked!
request body:
${JSON.stringify(req.body)}
original name:
${req.file.originalname}`) + }) + app.get('/status-404', (req, res) => { return res .status(404) diff --git a/packages/driver/package.json b/packages/driver/package.json index 7bcc8deac9c2..176d64fcabc5 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -59,6 +59,7 @@ "minimist": "1.2.5", "mocha": "7.0.1", "morgan": "1.9.1", + "multer": "1.4.2", "ordinal": "1.0.3", "react-15.6.1": "npm:react@15.6.1", "react-16.0.0": "npm:react@16.0.0", diff --git a/packages/driver/src/cy/commands/request.js b/packages/driver/src/cy/commands/request.js index f6b782d07eba..fdc54ca45d54 100644 --- a/packages/driver/src/cy/commands/request.js +++ b/packages/driver/src/cy/commands/request.js @@ -282,13 +282,61 @@ module.exports = (Commands, Cypress, cy, state, config) => { // Check if body is Blob. // construct.name is added because the parent of the Blob is not the same Blob // if it's generated from the test spec code. - if (requestOpts.body instanceof Blob || requestOpts?.body?.constructor.name === 'Blob') { + if (requestOpts.body instanceof Blob || requestOpts.body?.constructor.name === 'Blob') { requestOpts.bodyIsBase64Encoded = true return Cypress.Blob.blobToBase64String(requestOpts.body).then((str) => { requestOpts.body = str }) } + + // https://github.com/cypress-io/cypress/issues/1647 + // Handle if body is FormData + if (requestOpts.body instanceof FormData || requestOpts.body?.constructor.name === 'FormData') { + const boundary = '----CypressFormDataBoundary' + + // reset content-type + if (requestOpts.headers) { + delete requestOpts.headers[Object.keys(requestOpts).find((key) => key.toLowerCase() === 'content-type')] + } else { + requestOpts.headers = {} + } + + // boundary is required for form data + // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST + requestOpts.headers['content-type'] = `multipart/form-data; boundary=${boundary}` + + // socket.io ignores FormData. + // So, we need to encode the data into base64 string format. + const formBody = [] + + requestOpts.body.forEach((value, key) => { + // HTTP line break style is \r\n. + // @see https://stackoverflow.com/questions/5757290/http-header-line-break-style + if (value instanceof File || value?.constructor.name === 'File') { + formBody.push(`--${boundary}\r\n`) + formBody.push(`Content-Disposition: form-data; name="${key}"; filename="${value.name}"\r\n`) + formBody.push(`Content-Type: ${value.type || 'application/octet-stream'}\r\n`) + formBody.push('\r\n') + formBody.push(value) + formBody.push('\r\n') + } else { + formBody.push(`--${boundary}\r\n`) + formBody.push(`Content-Disposition: form-data; name="${key}"\r\n`) + formBody.push('\r\n') + formBody.push(value) + formBody.push('\r\n') + } + }) + + formBody.push(`--${boundary}--\r\n`) + + requestOpts.bodyIsBase64Encoded = true + + return Cypress.Blob.blobToBase64String(new Blob(formBody)).then((str) => { + requestOpts.body = str + }) + } }) .then(() => { return Cypress.backend('http:request', requestOpts)