Skip to content

Commit e4f29e7

Browse files
authored
feat: Promise (#940)
* feat: promise * doc: change example to use promise * test: add test for promise * chore: version and changelog * doc: add promise
1 parent c249922 commit e4f29e7

File tree

5 files changed

+182
-41
lines changed

5 files changed

+182
-41
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Changelog
22

3+
### 3.4.0
4+
5+
* feature: ([#940](https://github.com/node-formidable/formidable/pull/940)) form.parse returns a promise if no callback is provided
6+
* it resolves with and array `[fields, files]`
7+
8+
39
### 3.3.2
410

5-
* feature: ([#855](https://github.com/node-formidable/formidable/pull/855))add options.createDirsFromUploads, see README for usage
11+
* feature: ([#855](https://github.com/node-formidable/formidable/pull/855)) add options.createDirsFromUploads, see README for usage
612
* form.parse is an async function (ignore the promise)
713
* benchmarks: add e2e becnhmark with as many request as possible per second
814
* npm run to display all the commands

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,25 +100,26 @@ Parse an incoming file upload, with the
100100
import http from 'node:http';
101101
import formidable, {errors as formidableErrors} from 'formidable';
102102

103-
const server = http.createServer((req, res) => {
103+
const server = http.createServer(async (req, res) => {
104104
if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
105105
// parse a file upload
106106
const form = formidable({});
107-
108-
form.parse(req, (err, fields, files) => {
109-
if (err) {
107+
let fields;
108+
let files;
109+
try {
110+
[fields, files] = await form.parse(req);
111+
} catch (err) {
110112
// example to check for a very specific error
111113
if (err.code === formidableErrors.maxFieldsExceeded) {
112114

113115
}
116+
console.error(err);
114117
res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
115118
res.end(String(err));
116119
return;
117-
}
118-
res.writeHead(200, { 'Content-Type': 'application/json' });
119-
res.end(JSON.stringify({ fields, files }, null, 2));
120-
});
121-
120+
}
121+
res.writeHead(200, { 'Content-Type': 'application/json' });
122+
res.end(JSON.stringify({ fields, files }, null, 2));
122123
return;
123124
}
124125

@@ -382,10 +383,9 @@ const options = {
382383
```
383384

384385

385-
### .parse(request, callback)
386+
### .parse(request, ?callback)
386387

387-
Parses an incoming Node.js `request` containing form data. If `callback` is
388-
provided, all fields and files are collected and passed to the callback.
388+
Parses an incoming Node.js `request` containing form data. If `callback` is not provided a promise is returned.
389389

390390
```js
391391
const form = formidable({ uploadDir: __dirname });
@@ -394,6 +394,9 @@ form.parse(req, (err, fields, files) => {
394394
console.log('fields:', fields);
395395
console.log('files:', files);
396396
});
397+
398+
// with Promise
399+
const [fields, files] = await form.parse(req);
397400
```
398401

399402
You may overwrite this method if you are interested in directly accessing the

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "formidable",
3-
"version": "3.3.2",
3+
"version": "3.4.0",
44
"license": "MIT",
55
"description": "A node.js module for parsing form data, especially file uploads.",
66
"homepage": "https://github.com/node-formidable/formidable",

src/Formidable.js

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -178,40 +178,55 @@ class IncomingForm extends EventEmitter {
178178
return true;
179179
}
180180

181+
// returns a promise if no callback is provided
181182
async parse(req, cb) {
182183
this.req = req;
184+
let promise;
183185

184186
// Setup callback first, so we don't miss anything from data events emitted immediately.
185-
if (cb) {
186-
const callback = once(dezalgo(cb));
187-
this.fields = {};
188-
const files = {};
189-
190-
this.on('field', (name, value) => {
191-
if (this.type === 'multipart' || this.type === 'urlencoded') {
192-
if (!hasOwnProp(this.fields, name)) {
193-
this.fields[name] = [value];
194-
} else {
195-
this.fields[name].push(value);
196-
}
197-
} else {
198-
this.fields[name] = value;
199-
}
187+
if (!cb) {
188+
let resolveRef;
189+
let rejectRef;
190+
promise = new Promise((resolve, reject) => {
191+
resolveRef = resolve;
192+
rejectRef = reject;
200193
});
201-
this.on('file', (name, file) => {
202-
if (!hasOwnProp(files, name)) {
203-
files[name] = [file];
194+
cb = (err, fields, files) => {
195+
if (err) {
196+
rejectRef(err);
204197
} else {
205-
files[name].push(file);
198+
resolveRef([fields, files]);
206199
}
207-
});
208-
this.on('error', (err) => {
209-
callback(err, this.fields, files);
210-
});
211-
this.on('end', () => {
212-
callback(null, this.fields, files);
213-
});
200+
}
214201
}
202+
const callback = once(dezalgo(cb));
203+
this.fields = {};
204+
const files = {};
205+
206+
this.on('field', (name, value) => {
207+
if (this.type === 'multipart' || this.type === 'urlencoded') {
208+
if (!hasOwnProp(this.fields, name)) {
209+
this.fields[name] = [value];
210+
} else {
211+
this.fields[name].push(value);
212+
}
213+
} else {
214+
this.fields[name] = value;
215+
}
216+
});
217+
this.on('file', (name, file) => {
218+
if (!hasOwnProp(files, name)) {
219+
files[name] = [file];
220+
} else {
221+
files[name].push(file);
222+
}
223+
});
224+
this.on('error', (err) => {
225+
callback(err, this.fields, files);
226+
});
227+
this.on('end', () => {
228+
callback(null, this.fields, files);
229+
});
215230

216231
// Parse headers and setup the parser, ready to start listening for data.
217232
await this.writeHeaders(req.headers);
@@ -240,7 +255,9 @@ class IncomingForm extends EventEmitter {
240255
this._parser.end();
241256
}
242257
});
243-
258+
if (promise) {
259+
return promise;
260+
}
244261
return this;
245262
}
246263

test-node/standalone/promise.test.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {strictEqual, ok} from 'node:assert';
2+
import { createServer, request } from 'node:http';
3+
import formidable, {errors} from '../../src/index.js';
4+
import test from 'node:test';
5+
6+
const PORT = 13539;
7+
8+
const isPromise = (x) => {
9+
return x && typeof x === `object` && typeof x.then === `function`;
10+
};
11+
12+
test('parse returns promise if no callback is provided', (t,done) => {
13+
const server = createServer((req, res) => {
14+
const form = formidable();
15+
16+
const promise = form.parse(req);
17+
strictEqual(isPromise(promise), true);
18+
promise.then(([fields, files]) => {
19+
ok(typeof fields === 'object');
20+
ok(typeof files === 'object');
21+
res.writeHead(200);
22+
res.end("ok")
23+
}).catch(e => {
24+
done(e)
25+
})
26+
});
27+
28+
server.listen(PORT, () => {
29+
const chosenPort = server.address().port;
30+
const body = `----13068458571765726332503797717\r
31+
Content-Disposition: form-data; name="title"\r
32+
\r
33+
a\r
34+
----13068458571765726332503797717\r
35+
Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
36+
Content-Type: application/x-javascript\r
37+
\r
38+
\r
39+
\r
40+
a\r
41+
b\r
42+
c\r
43+
d\r
44+
\r
45+
----13068458571765726332503797717--\r
46+
`;
47+
fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
48+
method: 'POST',
49+
50+
headers: {
51+
'Content-Length': body.length,
52+
Host: `localhost:${chosenPort}`,
53+
'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
54+
},
55+
body
56+
}).then(res => {
57+
strictEqual(res.status, 200);
58+
server.close();
59+
done();
60+
});
61+
62+
});
63+
});
64+
65+
test('parse rejects with promise if it fails', (t,done) => {
66+
const server = createServer((req, res) => {
67+
const form = formidable({minFileSize: 10 ** 6}); // create condition to fail
68+
69+
const promise = form.parse(req);
70+
strictEqual(isPromise(promise), true);
71+
promise.then(() => {
72+
done('should have failed')
73+
}).catch(e => {
74+
res.writeHead(e.httpCode);
75+
strictEqual(e.code, errors.smallerThanMinFileSize);
76+
res.end(String(e))
77+
})
78+
});
79+
80+
server.listen(PORT, () => {
81+
const chosenPort = server.address().port;
82+
const body = `----13068458571765726332503797717\r
83+
Content-Disposition: form-data; name="title"\r
84+
\r
85+
a\r
86+
----13068458571765726332503797717\r
87+
Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
88+
Content-Type: application/x-javascript\r
89+
\r
90+
\r
91+
\r
92+
a\r
93+
b\r
94+
c\r
95+
d\r
96+
\r
97+
----13068458571765726332503797717--\r
98+
`;
99+
fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
100+
method: 'POST',
101+
102+
headers: {
103+
'Content-Length': body.length,
104+
Host: `localhost:${chosenPort}`,
105+
'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
106+
},
107+
body
108+
}).then(res => {
109+
strictEqual(res.status, 400);
110+
server.close();
111+
done();
112+
});
113+
114+
});
115+
});

0 commit comments

Comments
 (0)