@jnode/request 1.1.3 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -138
- package/package.json +3 -3
- package/src/index.js +227 -133
package/README.md
CHANGED
|
@@ -1,173 +1,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@jnode/request`
|
|
2
2
|
|
|
3
|
-
Simple HTTP(
|
|
3
|
+
Simple HTTP(S) requesting package for Node.js.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
npm
|
|
7
|
+
```
|
|
8
|
+
npm i @jnode/request@beta
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick start
|
|
12
12
|
|
|
13
|
-
### Import
|
|
13
|
+
### Import
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
const { request,
|
|
15
|
+
```js
|
|
16
|
+
const { request, FormPart } = require('@jnode/request');
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
###
|
|
20
|
-
|
|
21
|
-
```javascript
|
|
22
|
-
async function fetchData() {
|
|
23
|
-
try {
|
|
24
|
-
const response = await request('GET', 'https://example.com/api/data');
|
|
25
|
-
console.log('Status Code:', response.statusCode);
|
|
26
|
-
console.log('Body:', response.text()); // Use response.json() if expecting a JSON response
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error('Request failed:', error);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
19
|
+
### Make a request
|
|
31
20
|
|
|
32
|
-
|
|
21
|
+
```js
|
|
22
|
+
request('GET', 'https://example.com/').then((res) => {
|
|
23
|
+
console.log(res.text());
|
|
24
|
+
});
|
|
33
25
|
```
|
|
34
26
|
|
|
35
|
-
###
|
|
36
|
-
|
|
37
|
-
```javascript
|
|
38
|
-
async function postData() {
|
|
39
|
-
try {
|
|
40
|
-
const headers = {
|
|
41
|
-
'Content-Type': 'application/json'
|
|
42
|
-
};
|
|
43
|
-
const body = JSON.stringify({ key: 'value' });
|
|
44
|
-
const response = await request('POST', 'https://example.com/api/post', headers, body);
|
|
45
|
-
console.log('Status Code:', response.statusCode);
|
|
46
|
-
console.log('Response:', response.text());
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error('Request failed:', error);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
postData();
|
|
53
|
-
```
|
|
27
|
+
### Make a `multipart/form-body` request
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const parts = [{
|
|
63
|
-
disposition: 'form-data; name="file"; filename="example.txt"',
|
|
64
|
-
contentType: 'text/plain',
|
|
65
|
-
data: fileData
|
|
66
|
-
}, {
|
|
67
|
-
disposition: 'form-data; name="key"',
|
|
68
|
-
data: 'value'
|
|
69
|
-
}];
|
|
70
|
-
const body = generateMultipartBody(parts);
|
|
71
|
-
|
|
72
|
-
const headers = {
|
|
73
|
-
'Content-Type': 'multipart/form-data; boundary=----JustNodeFormBoundary'
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const response = await request('POST', 'https://example.com/api/upload', headers, body);
|
|
78
|
-
console.log('Status Code:', response.statusCode);
|
|
79
|
-
console.log('Response:', response.text());
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error('Request failed:', error);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
uploadFile();
|
|
29
|
+
```js
|
|
30
|
+
request('POST', 'https://example.com/', [
|
|
31
|
+
new FormPart('foo', 'bar'),
|
|
32
|
+
new FormPart('awa', 'uwu')
|
|
33
|
+
]).then((res) => {
|
|
34
|
+
console.log(res.text());
|
|
35
|
+
});
|
|
86
36
|
```
|
|
87
37
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
38
|
+
--------
|
|
39
|
+
|
|
40
|
+
# Reference
|
|
41
|
+
|
|
42
|
+
## `request.request(method, url[, body, headers, options])`
|
|
43
|
+
|
|
44
|
+
- `method` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) A string specifying the HTTP request method. **Default:** `'GET'`.
|
|
45
|
+
- `url` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [\<URL\>](https://nodejs.org/docs/latest/api/url.html#the-whatwg-url-api) The target URL to request, support both `http` and `https`.
|
|
46
|
+
- `body` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [\<Buffer\>](https://nodejs.org/docs/latest/api/buffer.html#class-buffer) | [\<Uint8Array\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) | [\<stream.Readable\>](https://nodejs.org/docs/latest/api/stream.html#class-streamreadable) | [\<URLSearchParams\>](https://nodejs.org/docs/latest/api/url.html#class-urlsearchparams) | [\<request.FormPart[]\>](#class-requestformpart) | [\<Object[]\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) The request body. Using [\<URLSearchParams\>](https://nodejs.org/docs/latest/api/url.html#class-urlsearchparams) will send a `application/x-www-form-urlencoded` request; using [\<request.FormPart[]\>](#class-requestformpart) or [\<Object[]\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) will send a `multipart/form-body` request.
|
|
47
|
+
- `headers` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) An object containing request headers. `Content-Length` is automatically calculated for [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type), [\<Buffer\>](https://nodejs.org/docs/latest/api/buffer.html#class-buffer), [\<Uint8Array\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), or [\<URLSearchParams\>](https://nodejs.org/docs/latest/api/url.html#class-urlsearchparams) body. And `Content-Type` will be force replaced while sending a `multipart/form-body` request with [\<FormPart[]\>](#class-requestformpart) or [\<Object[]\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) body.
|
|
48
|
+
- `options` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) `options` in [`http.request()`](https://nodejs.org/docs/latest/api/http.html#httprequestoptions-callback).
|
|
49
|
+
- Returns: [\<request.RequestResponse\>](#class-requestrequestresponse)
|
|
50
|
+
|
|
51
|
+
Make a HTTP(S) request.
|
|
52
|
+
|
|
53
|
+
## Class: `request.FormPart`
|
|
54
|
+
|
|
55
|
+
### `new request.FormPart(name, body[, headers])`
|
|
56
|
+
|
|
57
|
+
- `name` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) Name of the part.
|
|
58
|
+
- `body` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [\<Buffer\>](https://nodejs.org/docs/latest/api/buffer.html#class-buffer) | [\<Uint8Array\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) | [\<stream.Readable\>](https://nodejs.org/docs/latest/api/stream.html#class-streamreadable) | [\<URLSearchParams\>](https://nodejs.org/docs/latest/api/url.html#class-urlsearchparams) The body of the part. Using [\<URLSearchParams\>](https://nodejs.org/docs/latest/api/url.html#class-urlsearchparams) will create a `application/x-www-form-urlencoded` part.
|
|
59
|
+
- `headers` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) An object containing part headers. There is a special `filename` field, will be added to `Content-Disposition`.
|
|
60
|
+
|
|
61
|
+
## Class: `request.RequestResponse`
|
|
62
|
+
|
|
63
|
+
### `new request.RequestResponse(res)`
|
|
64
|
+
|
|
65
|
+
- `res` [\<http.IncomingMessage\>](https://nodejs.org/docs/latest/api/http.html#class-httpincomingmessage) The original `node:http` response object.
|
|
66
|
+
|
|
67
|
+
### `requestResponse.res`
|
|
68
|
+
|
|
69
|
+
- Type: [\<http.IncomingMessage\>](https://nodejs.org/docs/latest/api/http.html#class-httpincomingmessage)
|
|
70
|
+
|
|
71
|
+
### `requestResponse.statusCode`
|
|
72
|
+
|
|
73
|
+
- Type: [\<number\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type)
|
|
74
|
+
|
|
75
|
+
The response HTTP status code.
|
|
76
|
+
|
|
77
|
+
### `requestResponse.headers`
|
|
78
|
+
|
|
79
|
+
- Type: [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
|
|
80
|
+
|
|
81
|
+
The response HTTP headers.
|
|
82
|
+
|
|
83
|
+
### `requestResponse._body`
|
|
84
|
+
|
|
85
|
+
- Type: [\<Buffer\>](https://nodejs.org/docs/latest/api/buffer.html#class-buffer)
|
|
86
|
+
|
|
87
|
+
> **Notice**: You should avoid reading this property directly, use [`requestRespond.buffer()`](#requestrespondbuffer) instead.
|
|
88
|
+
|
|
89
|
+
### `requestResponse.buffer()`
|
|
90
|
+
|
|
91
|
+
- Returns: [\<Promise\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) Fulfills with a [\<Buffer\>](https://nodejs.org/docs/latest/api/buffer.html#class-buffer) object.
|
|
92
|
+
|
|
93
|
+
Receives the body of the response stream and return as a buffer.
|
|
94
|
+
|
|
95
|
+
### `requestResponse.text([encoding])`
|
|
96
|
+
|
|
97
|
+
- `encoding` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) The character encoding to use. **Default:** `'utf8'`.
|
|
98
|
+
- Returns: [\<Promise\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) Fulfills with a string.
|
|
99
|
+
|
|
100
|
+
Receives the body of the response stream and return as a string.
|
|
101
|
+
|
|
102
|
+
### `requestResponse.json([encoding])`
|
|
103
|
+
|
|
104
|
+
- `encoding` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) The character encoding to use. **Default:** `'utf8'`.
|
|
105
|
+
- Returns: [\<Promise\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) Fulfills with any of [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object), [\<Array\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array), [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type), [\<number\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type), [\<boolean\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type), or [\<null\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#null_type).
|
|
106
|
+
|
|
107
|
+
Receives the body of the response stream and parse string as JSON.
|
|
108
|
+
|
|
109
|
+
### `requestResponse.rl()`
|
|
110
|
+
|
|
111
|
+
- Returns: [\<readline.Interface\>](https://nodejs.org/docs/latest/api/readline.html#class-readlineinterface)
|
|
116
112
|
|
|
117
|
-
|
|
113
|
+
Provides an interface for reading data from response one line at a time.
|
|
118
114
|
|
|
119
|
-
### `
|
|
115
|
+
### `requestResponse.sse()`
|
|
120
116
|
|
|
121
|
-
|
|
117
|
+
- Returns: [\<request.EventReceiver\>](#class-requesteventreceiver)
|
|
122
118
|
|
|
123
|
-
-
|
|
124
|
-
- `url`: The request URL.
|
|
125
|
-
- `headers`: An object containing request headers. Defaults to an empty object.
|
|
126
|
-
- `body`: The request body (string or Buffer).
|
|
119
|
+
Provides an interface for reading data from response as Server-Sent Events (SSE, `text/event-stream`).
|
|
127
120
|
|
|
128
|
-
|
|
121
|
+
## Class: `request.EventReceiver`
|
|
129
122
|
|
|
130
|
-
|
|
123
|
+
- Extends: [\<EventEmitter\>](https://nodejs.org/docs/latest/api/events.html#class-eventemitter)
|
|
131
124
|
|
|
132
|
-
|
|
125
|
+
An interface for reading data from [\<stream.Readable\>](https://nodejs.org/docs/latest/api/stream.html#class-streamreadable) as Server-Sent Events (SSE, `text/event-stream`).
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
- `url`: The request URL.
|
|
136
|
-
- `headers`: An object containing request headers.
|
|
137
|
-
- `parts`: An array of objects, where each object represents a part of the form data.
|
|
138
|
-
- `disposition` (Optional): Content disposition.
|
|
139
|
-
- `type` (Optional): Content type of the data. Defaults to `application/octet-stream`.
|
|
140
|
-
- `data`: Data (string or Buffer) of this part.
|
|
141
|
-
- `file` (Optional): Path to a local file for streaming.
|
|
142
|
-
- `base64` (Optional): base64 encoded data string.
|
|
143
|
-
- `stream` (Optional): Any readable stream.
|
|
144
|
-
- `options`: Other options.
|
|
127
|
+
### `new request.EventReceiver(res)`
|
|
145
128
|
|
|
146
|
-
|
|
129
|
+
- `res` [\<stream.Readable\>](https://nodejs.org/docs/latest/api/stream.html#class-streamreadable) The response stream or any readable stream.
|
|
147
130
|
|
|
148
|
-
###
|
|
131
|
+
### Event: `'close'`
|
|
149
132
|
|
|
150
|
-
|
|
133
|
+
Emitted when the request has been completed.
|
|
151
134
|
|
|
152
|
-
|
|
153
|
-
- `disposition` (Optional): Content disposition.
|
|
154
|
-
- `contentType` (Optional): Content type of the data. Defaults to `application/octet-stream`.
|
|
155
|
-
- `data`: Data (string or Buffer) of this part.
|
|
156
|
-
- `encoded` (Optional): If provided, the data is already base64 encoded, you may skip the default base64 encoding.
|
|
135
|
+
### Event: `'event'`
|
|
157
136
|
|
|
158
|
-
|
|
137
|
+
- `event` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
|
|
138
|
+
- `data` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) The full data string of this event.
|
|
139
|
+
- `event` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) The event name.
|
|
140
|
+
- `id` [\<string\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) The event id.
|
|
159
141
|
|
|
160
|
-
|
|
142
|
+
### `eventReceiver.res`
|
|
161
143
|
|
|
162
|
-
|
|
144
|
+
- Type: [\<stream.Readable\>](https://nodejs.org/docs/latest/api/stream.html#class-streamreadable)
|
|
163
145
|
|
|
164
|
-
|
|
146
|
+
The response stream or any readable stream.
|
|
165
147
|
|
|
166
|
-
|
|
167
|
-
- `headers`: An object containing the response headers.
|
|
168
|
-
- `body`: The raw response body (Buffer).
|
|
148
|
+
### `eventReceiver.rl`
|
|
169
149
|
|
|
170
|
-
|
|
150
|
+
- Type: [\<readline.Interface\>](https://nodejs.org/docs/latest/api/readline.html#class-readlineinterface)
|
|
171
151
|
|
|
172
|
-
|
|
173
|
-
- `json()`: Attempts to parse the response body as JSON. Returns a JSON object or `undefined` if parsing fails.
|
|
152
|
+
An interface for reading data from response one line at a time.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jnode/request",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Simple HTTP(s) package for Node.js.",
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
|
+
"description": "Simple HTTP(s) requesting package for Node.js.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Hello, HTTP.\""
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"url": "https://github.com/japple-jnode/request/issues"
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://github.com/japple-jnode/request#readme"
|
|
26
|
-
}
|
|
26
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,162 +1,256 @@
|
|
|
1
1
|
/*
|
|
2
2
|
JustRequest
|
|
3
|
-
|
|
3
|
+
v2
|
|
4
4
|
|
|
5
|
-
Simple HTTP(s) package for Node.js.
|
|
5
|
+
Simple HTTP(s) requesting package for Node.js.
|
|
6
6
|
|
|
7
|
-
by
|
|
7
|
+
by JustApple
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// dependencies
|
|
11
11
|
const http = require('http');
|
|
12
12
|
const https = require('https');
|
|
13
13
|
const fs = require('fs');
|
|
14
|
-
const { URL } = require('url');
|
|
15
|
-
const
|
|
14
|
+
const { URL, URLSearchParams } = require('url');
|
|
15
|
+
const stream = require('stream');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
|
+
const readline = require('readline');
|
|
18
|
+
const EventEmitter = require('events');
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
function request(method, url, headers = {},
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
//auto provide `Content-Length` header
|
|
22
|
-
headers['Content-Length'] = headers['Content-Length'] ?? (body ? Buffer.byteLength(body) : 0);
|
|
23
|
-
|
|
24
|
-
//support http and https
|
|
25
|
-
const protocol = url.startsWith('https://') ? https : http;
|
|
26
|
-
|
|
27
|
-
//make request
|
|
28
|
-
protocol.request(url, {
|
|
29
|
-
method: method,
|
|
30
|
-
headers: headers
|
|
31
|
-
}, (res) => {
|
|
32
|
-
let chunks = [];
|
|
33
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
34
|
-
res.on('end', () => {
|
|
35
|
-
resolve(new RequestResponse(res, Buffer.concat(chunks)));
|
|
36
|
-
});
|
|
37
|
-
res.on('error', reject);
|
|
38
|
-
}).end(body).on('error', reject);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
//generate `multipart/form-data` type request body
|
|
43
|
-
function generateMultipartBody(parts = []) {
|
|
44
|
-
//we use `----JustNodeFormBoundary` as boundary, and will encode data with base64
|
|
45
|
-
//every part of `parts` is an object with item `disposition`, `contentType` and `data` (Buffer)
|
|
46
|
-
|
|
47
|
-
let result = '';
|
|
48
|
-
for (let i of parts) { //add every part
|
|
49
|
-
result += '------JustNodeFormBoundary\r\n' +
|
|
50
|
-
'Content-Disposition: form-data' + (i.disposition ? '; ' + i.disposition : '') + '\r\n' +
|
|
51
|
-
'Content-Type: ' + (i.contentType ?? 'application/octet-stream') + '\r\n' +
|
|
52
|
-
'Content-Transfer-Encoding: base64\r\n\r\n' +
|
|
53
|
-
(i.encoded ?? ((typeof i.data === 'string') ? Buffer.from(i.data) : i.data).toString('base64')) + '\r\n';
|
|
54
|
-
}
|
|
55
|
-
result += '------JustNodeFormBoundary--'; //end
|
|
56
|
-
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
//http multipart/form-data request
|
|
61
|
-
async function multipartRequest(method, url, headers, parts, options) {
|
|
20
|
+
// make a HTTP(S) request
|
|
21
|
+
function request(method = 'GET', url = '', body, headers = {}, options = {}) {
|
|
62
22
|
return new Promise(async (resolve, reject) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//set options
|
|
71
|
-
options = {
|
|
72
|
-
method,
|
|
73
|
-
headers,
|
|
74
|
-
...options
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
//make a request
|
|
78
|
-
const req = reqModule.request(url, options, (res) => {
|
|
79
|
-
//save buffers
|
|
80
|
-
let chunks = [];
|
|
81
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
82
|
-
|
|
83
|
-
res.on('end', () => {
|
|
84
|
-
resolve(new RequestResponse(res, Buffer.concat(chunks)));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
res.on('error', (err) => {
|
|
88
|
-
reject(err);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
req.on('error', (err) => {
|
|
93
|
-
reject(err);
|
|
23
|
+
// protocol select
|
|
24
|
+
if (typeof url === 'string') url = new URL(url);
|
|
25
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
26
|
+
|
|
27
|
+
// make request
|
|
28
|
+
const req = protocol.request(url, { method, headers }, (res) => {
|
|
29
|
+
resolve(new RequestResponse(res));
|
|
94
30
|
});
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
req.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
31
|
+
|
|
32
|
+
// write headers and body
|
|
33
|
+
if (typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array) { // static body
|
|
34
|
+
req.setHeader('Content-Length', Buffer.byteLength(body));
|
|
35
|
+
req.end(body);
|
|
36
|
+
} else if (body instanceof stream.Readable) { // stream
|
|
37
|
+
body.pipe(req);
|
|
38
|
+
} else if (Array.isArray(body)) { // multipart/form-body
|
|
39
|
+
// generate boundary
|
|
40
|
+
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}${crypto.randomUUID()}`;
|
|
41
|
+
req.setHeader('Content-Type', `multipart/form-data; boundary=${boundary}`);
|
|
42
|
+
|
|
43
|
+
// write every parts
|
|
44
|
+
for (const part of body) {
|
|
45
|
+
req.write(`--${boundary}\r\n`); // boundary
|
|
46
|
+
|
|
47
|
+
// part headers
|
|
48
|
+
const partHeaders = part.headers || {};
|
|
49
|
+
|
|
50
|
+
// set Content-Disposition header
|
|
51
|
+
if (!partHeaders['Content-Disposition']) {
|
|
52
|
+
partHeaders['Content-Disposition'] = `form-data; name="${part.name}"${part.filename ? `; filename="${part.filename}"` : ''}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// write headers and body
|
|
56
|
+
if (typeof part.body === 'string' || Buffer.isBuffer(part.body) || part.body instanceof Uint8Array) { // static body
|
|
57
|
+
for (const [key, value] of Object.entries(partHeaders)) {
|
|
58
|
+
req.write(`${key}: ${value}\r\n`);
|
|
59
|
+
}
|
|
60
|
+
req.write('\r\n');
|
|
61
|
+
|
|
62
|
+
req.write(part.body);
|
|
63
|
+
req.write('\r\n');
|
|
64
|
+
} else if (part.body instanceof stream.Readable) { // stream body
|
|
65
|
+
for (const [key, value] of Object.entries(partHeaders)) {
|
|
66
|
+
req.write(`${key}: ${value}\r\n`);
|
|
67
|
+
}
|
|
68
|
+
req.write('\r\n');
|
|
69
|
+
|
|
70
|
+
await new Promise((res, rej) => {
|
|
71
|
+
part.body.on('error', rej);
|
|
72
|
+
part.body.on('end', res);
|
|
73
|
+
part.body.pipe(req, { end: false });
|
|
74
|
+
});
|
|
75
|
+
req.write('\r\n');
|
|
76
|
+
} else if (part.body instanceof URLSearchParams) {
|
|
77
|
+
const encoded = part.body.toString();
|
|
78
|
+
partHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
79
|
+
for (const [key, value] of Object.entries(partHeaders)) {
|
|
80
|
+
req.write(`${key}: ${value}\r\n`);
|
|
81
|
+
}
|
|
82
|
+
req.write('\r\n');
|
|
83
|
+
|
|
84
|
+
req.write(encoded);
|
|
85
|
+
req.write('\r\n');
|
|
86
|
+
} else { // no body
|
|
87
|
+
for (const [key, value] of Object.entries(partHeaders)) {
|
|
88
|
+
req.write(`${key}: ${value}\r\n`);
|
|
89
|
+
}
|
|
90
|
+
req.write('\r\n\r\n');
|
|
91
|
+
}
|
|
127
92
|
}
|
|
93
|
+
|
|
94
|
+
// end multipart body
|
|
95
|
+
req.write(`--${boundary}--`);
|
|
96
|
+
req.end();
|
|
97
|
+
} else if (body instanceof URLSearchParams) { // application/x-www-form-urlencoded
|
|
98
|
+
const encoded = body.toString();
|
|
99
|
+
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
100
|
+
req.setHeader('Content-Length', Buffer.byteLength(encoded));
|
|
101
|
+
req.end(encoded);
|
|
102
|
+
} else { // no body
|
|
103
|
+
req.end();
|
|
128
104
|
}
|
|
129
|
-
|
|
130
|
-
//end request
|
|
131
|
-
req.write(`--${boundary}--`);
|
|
132
|
-
req.end();
|
|
133
105
|
});
|
|
134
106
|
}
|
|
135
107
|
|
|
108
|
+
// form part
|
|
109
|
+
class FormPart {
|
|
110
|
+
constructor(name = '', body, headers = {}) {
|
|
111
|
+
this.name = name;
|
|
112
|
+
this.body = body;
|
|
113
|
+
this.headers = headers;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
136
117
|
//request response
|
|
137
118
|
class RequestResponse {
|
|
138
|
-
constructor(res
|
|
119
|
+
constructor(res) {
|
|
139
120
|
this.res = res;
|
|
140
|
-
|
|
141
|
-
|
|
121
|
+
|
|
142
122
|
this.statusCode = res.statusCode;
|
|
143
123
|
this.headers = res.headers;
|
|
144
124
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
|
|
126
|
+
buffer() {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
if (!this._body) {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
this.res.on('data', (d) => { chunks.push(d); });
|
|
131
|
+
this.res.on('end', () => {
|
|
132
|
+
this._body = Buffer.concat(chunks);
|
|
133
|
+
resolve(Buffer.concat(chunks));
|
|
134
|
+
});
|
|
135
|
+
this.res.on('error', reject);
|
|
136
|
+
} else {
|
|
137
|
+
resolve(this._body);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async text(encoding = 'utf8') {
|
|
143
|
+
return (await this.buffer()).toString(encoding);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async json(encoding = 'utf8') {
|
|
147
|
+
return JSON.parse((await this.text(encoding)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
rl() {
|
|
151
|
+
return readline.createInterface({
|
|
152
|
+
input: this.res,
|
|
153
|
+
crlfDelay: Infinity
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
sse() {
|
|
158
|
+
return new EventReceiver(this.res);
|
|
149
159
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// SSE (text/event-stream) event receiver
|
|
163
|
+
class EventReceiver extends EventEmitter {
|
|
164
|
+
constructor(res) {
|
|
165
|
+
super();
|
|
166
|
+
this.res = res;
|
|
167
|
+
this.rl = readline.createInterface({
|
|
168
|
+
input: res,
|
|
169
|
+
crlfDelay: Infinity
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let name = 'message';
|
|
173
|
+
let data = '';
|
|
174
|
+
let id = null;
|
|
175
|
+
|
|
176
|
+
this.rl.on('line', (line) => {
|
|
177
|
+
// comment
|
|
178
|
+
if (line.startsWith(':')) return;
|
|
179
|
+
|
|
180
|
+
// fields
|
|
181
|
+
if (line.startsWith('event: ')) name = line.slice(7);
|
|
182
|
+
else if (line.startsWith('event:')) name = line.slice(6);
|
|
183
|
+
else if (line.startsWith('data: ')) data += line.slice(6) + '\n';
|
|
184
|
+
else if (line.startsWith('data:')) data += line.slice(5) + '\n';
|
|
185
|
+
else if (line.startsWith('id: ')) id = line.slice(4).trim();
|
|
186
|
+
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
|
187
|
+
else if (line === '') { // event finished
|
|
188
|
+
if (data.endsWith('\n')) data = data.slice(0, -1); // remove last \n
|
|
189
|
+
this.emit('event', { name, data, id });
|
|
190
|
+
|
|
191
|
+
// reset
|
|
192
|
+
name = 'message';
|
|
193
|
+
data = '';
|
|
194
|
+
id = null;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.rl.on('close', () => { this.emit('close'); });
|
|
199
|
+
this.rl.on('error', (err) => {
|
|
200
|
+
this.emit('error', err);
|
|
201
|
+
this.emit('close');
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// async generator support
|
|
206
|
+
[Symbol.asyncIterator]() {
|
|
207
|
+
let queue = [];
|
|
208
|
+
let done = false;
|
|
209
|
+
let resolvers = [];
|
|
210
|
+
|
|
211
|
+
// event
|
|
212
|
+
const onEvent = (event) => {
|
|
213
|
+
if (resolvers.length > 0) {
|
|
214
|
+
resolvers.shift()({ value: event, done: false });
|
|
215
|
+
} else {
|
|
216
|
+
queue.push(event);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
this.on('event', onEvent);
|
|
220
|
+
|
|
221
|
+
// close (including error)
|
|
222
|
+
const onClose = () => {
|
|
223
|
+
done = true;
|
|
224
|
+
while (resolvers.length > 0) {
|
|
225
|
+
resolvers.shift()({ done: true });
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
this.on('close', onClose);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
next: () => {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
if (queue.length > 0) {
|
|
234
|
+
resolve({ value: queue.shift(), done: false });
|
|
235
|
+
} else if (done) {
|
|
236
|
+
resolve({ done: true });
|
|
237
|
+
} else {
|
|
238
|
+
resolvers.push(resolve);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
return: () => {
|
|
243
|
+
done = true;
|
|
244
|
+
while (resolvers.length > 0) {
|
|
245
|
+
resolvers.shift()({ done: true });
|
|
246
|
+
}
|
|
247
|
+
this.off('event', onEvent);
|
|
248
|
+
this.off('close', onClose);
|
|
249
|
+
},
|
|
250
|
+
[Symbol.asyncIterator]() { return this; }
|
|
157
251
|
}
|
|
158
252
|
}
|
|
159
253
|
}
|
|
160
254
|
|
|
161
|
-
//export
|
|
162
|
-
module.exports = { request,
|
|
255
|
+
// export
|
|
256
|
+
module.exports = { request, FormPart, RequestResponse };
|