Skip to content

Commit c258879

Browse files
authored
fix: can send FormData with File. (#16576)
1 parent c4aaa58 commit c258879

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

packages/driver/cypress/integration/commands/request_spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,32 @@ describe('src/cy/commands/request', () => {
499499
expect(dec.decode(response.body)).to.contain('1,2,3,4')
500500
})
501501
})
502+
503+
it('can send FormData with File', () => {
504+
const formData = new FormData()
505+
506+
formData.set('file', new File(['1,2,3,4'], 'upload.txt'), 'upload.txt')
507+
formData.set('name', 'Tony Stark')
508+
cy.request({
509+
method: 'POST',
510+
url: 'http://localhost:3500/dump-form-data',
511+
body: formData,
512+
headers: {
513+
'content-type': 'multipart/form-data',
514+
},
515+
})
516+
.then((response) => {
517+
expect(response.status).to.equal(200)
518+
// When user-passed body to the Nodejs server is a Buffer,
519+
// Nodejs doesn't provide any decoder in the response.
520+
// So, we need to decode it ourselves.
521+
const dec = new TextDecoder()
522+
const result = dec.decode(response.body)
523+
524+
expect(result).to.contain('Tony Stark')
525+
expect(result).to.contain('upload.txt')
526+
})
527+
})
502528
})
503529

504530
describe('subjects', () => {

packages/driver/cypress/plugins/server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const http = require('http')
66
const httpsProxy = require('@packages/https-proxy')
77
const path = require('path')
88
const Promise = require('bluebird')
9+
const multer = require('multer')
10+
const upload = multer({ dest: 'cypress/_test-output/' })
911

1012
const PATH_TO_SERVER_PKG = path.dirname(require.resolve('@packages/server'))
1113
const httpPorts = [3500, 3501]
@@ -144,6 +146,10 @@ const createApp = (port) => {
144146
return res.send(`<html><body>it worked!<br>request body:<br>${req.body.toString()}</body></html>`)
145147
})
146148

149+
app.all('/dump-form-data', upload.single('file'), (req, res) => {
150+
return res.send(`<html><body>it worked!<br>request body:<br>${JSON.stringify(req.body)}<br>original name:<br>${req.file.originalname}</body></html>`)
151+
})
152+
147153
app.get('/status-404', (req, res) => {
148154
return res
149155
.status(404)

packages/driver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"minimist": "1.2.5",
6060
"mocha": "7.0.1",
6161
"morgan": "1.9.1",
62+
"multer": "1.4.2",
6263
"ordinal": "1.0.3",
6364
"react-15.6.1": "npm:react@15.6.1",
6465
"react-16.0.0": "npm:react@16.0.0",

packages/driver/src/cy/commands/request.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,61 @@ module.exports = (Commands, Cypress, cy, state, config) => {
282282
// Check if body is Blob.
283283
// construct.name is added because the parent of the Blob is not the same Blob
284284
// if it's generated from the test spec code.
285-
if (requestOpts.body instanceof Blob || requestOpts?.body?.constructor.name === 'Blob') {
285+
if (requestOpts.body instanceof Blob || requestOpts.body?.constructor.name === 'Blob') {
286286
requestOpts.bodyIsBase64Encoded = true
287287

288288
return Cypress.Blob.blobToBase64String(requestOpts.body).then((str) => {
289289
requestOpts.body = str
290290
})
291291
}
292+
293+
// https://github.com/cypress-io/cypress/issues/1647
294+
// Handle if body is FormData
295+
if (requestOpts.body instanceof FormData || requestOpts.body?.constructor.name === 'FormData') {
296+
const boundary = '----CypressFormDataBoundary'
297+
298+
// reset content-type
299+
if (requestOpts.headers) {
300+
delete requestOpts.headers[Object.keys(requestOpts).find((key) => key.toLowerCase() === 'content-type')]
301+
} else {
302+
requestOpts.headers = {}
303+
}
304+
305+
// boundary is required for form data
306+
// @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
307+
requestOpts.headers['content-type'] = `multipart/form-data; boundary=${boundary}`
308+
309+
// socket.io ignores FormData.
310+
// So, we need to encode the data into base64 string format.
311+
const formBody = []
312+
313+
requestOpts.body.forEach((value, key) => {
314+
// HTTP line break style is \r\n.
315+
// @see https://stackoverflow.com/questions/5757290/http-header-line-break-style
316+
if (value instanceof File || value?.constructor.name === 'File') {
317+
formBody.push(`--${boundary}\r\n`)
318+
formBody.push(`Content-Disposition: form-data; name="${key}"; filename="${value.name}"\r\n`)
319+
formBody.push(`Content-Type: ${value.type || 'application/octet-stream'}\r\n`)
320+
formBody.push('\r\n')
321+
formBody.push(value)
322+
formBody.push('\r\n')
323+
} else {
324+
formBody.push(`--${boundary}\r\n`)
325+
formBody.push(`Content-Disposition: form-data; name="${key}"\r\n`)
326+
formBody.push('\r\n')
327+
formBody.push(value)
328+
formBody.push('\r\n')
329+
}
330+
})
331+
332+
formBody.push(`--${boundary}--\r\n`)
333+
334+
requestOpts.bodyIsBase64Encoded = true
335+
336+
return Cypress.Blob.blobToBase64String(new Blob(formBody)).then((str) => {
337+
requestOpts.body = str
338+
})
339+
}
292340
})
293341
.then(() => {
294342
return Cypress.backend('http:request', requestOpts)

0 commit comments

Comments
 (0)