@cloudron/superagent 1.0.0
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/package.json +20 -0
- package/superagent.js +230 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudron/superagent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Superagent Replacement",
|
|
5
|
+
"main": "superagent.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "ssh://git@git.cloudron.io:6000/utils/superagent.git"
|
|
12
|
+
},
|
|
13
|
+
"author": "Cloudron",
|
|
14
|
+
"private": false,
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"debug": "^4.4.1",
|
|
18
|
+
"safetydance": "^2.5.1"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/superagent.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports = module.exports = {
|
|
4
|
+
head,
|
|
5
|
+
get,
|
|
6
|
+
put,
|
|
7
|
+
post,
|
|
8
|
+
patch,
|
|
9
|
+
del,
|
|
10
|
+
options,
|
|
11
|
+
request
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// IMPORTANT: do not require box code here . This is used by migration scripts
|
|
15
|
+
const assert = require('assert'),
|
|
16
|
+
consumers = require('node:stream/consumers'),
|
|
17
|
+
debug = require('debug')('@cloudron/superagent'),
|
|
18
|
+
fs = require('fs'),
|
|
19
|
+
http = require('http'),
|
|
20
|
+
https = require('https'),
|
|
21
|
+
path = require('path'),
|
|
22
|
+
safe = require('safetydance');
|
|
23
|
+
|
|
24
|
+
class Request {
|
|
25
|
+
#boundary;
|
|
26
|
+
#redirectCount;
|
|
27
|
+
#retryCount;
|
|
28
|
+
#timer;
|
|
29
|
+
#body;
|
|
30
|
+
#okFunc;
|
|
31
|
+
#options;
|
|
32
|
+
#url;
|
|
33
|
+
#cookies;
|
|
34
|
+
|
|
35
|
+
constructor(method, url) {
|
|
36
|
+
assert.strictEqual(typeof url, 'string');
|
|
37
|
+
|
|
38
|
+
this.#url = new URL(url);
|
|
39
|
+
this.#options = {
|
|
40
|
+
method,
|
|
41
|
+
headers: {},
|
|
42
|
+
signal: null // set for timeouts
|
|
43
|
+
};
|
|
44
|
+
this.#okFunc = ({ status }) => status >=200 && status <= 299;
|
|
45
|
+
this.#timer = { timeout: 0, id: null, controller: null };
|
|
46
|
+
this.#retryCount = 0;
|
|
47
|
+
this.#body = Buffer.alloc(0);
|
|
48
|
+
this.#redirectCount = 5;
|
|
49
|
+
this.#boundary = null; // multipart only
|
|
50
|
+
this.#cookies = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async _handleResponse(url, response) {
|
|
54
|
+
// const contentLength = response.headers['content-length'];
|
|
55
|
+
// if (!contentLength || contentLength > 5*1024*1024*1024) throw new Error(`Response size unknown or too large: ${contentLength}`);
|
|
56
|
+
const [consumeError, data] = await safe(consumers.buffer(response)); // have to drain response
|
|
57
|
+
if (consumeError) throw new Error(`Error consuming body stream: ${consumeError.message}`);
|
|
58
|
+
if (!response.complete) throw new Error('Incomplete response');
|
|
59
|
+
const contentType = response.headers['content-type'];
|
|
60
|
+
|
|
61
|
+
const setCookie = response.headers['set-cookie'];
|
|
62
|
+
if (setCookie) this.#options.headers['cookie'] = setCookie.map(cookie => cookie.split(';')[0]).join('; ');
|
|
63
|
+
|
|
64
|
+
const result = {
|
|
65
|
+
url: new URL(response.headers['location'] || '', url),
|
|
66
|
+
status: response.statusCode,
|
|
67
|
+
headers: response.headers,
|
|
68
|
+
body: null,
|
|
69
|
+
text: null
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (contentType?.includes('application/json')) {
|
|
73
|
+
result.text = data.toString('utf8');
|
|
74
|
+
if (data.byteLength !== 0) result.body = safe.JSON.parse(result.text) || {};
|
|
75
|
+
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
76
|
+
result.text = data.toString('utf8');
|
|
77
|
+
const searchParams = new URLSearchParams(data);
|
|
78
|
+
result.body = Object.fromEntries(searchParams.entries());
|
|
79
|
+
} else if (!contentType || contentType.startsWith('text/')) {
|
|
80
|
+
result.body = data;
|
|
81
|
+
result.text = result.body.toString('utf8');
|
|
82
|
+
} else {
|
|
83
|
+
result.body = data;
|
|
84
|
+
result.text = `<binary data (${data.byteLength} bytes)>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async _makeRequest(url) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const proto = url.protocol === 'https:' ? https : http;
|
|
93
|
+
const request = proto.request(url, this.#options); // ClientRequest
|
|
94
|
+
|
|
95
|
+
request.on('error', reject); // network error, dns error
|
|
96
|
+
request.on('response', async (response) => {
|
|
97
|
+
const [error, result] = await safe(this._handleResponse(url, response));
|
|
98
|
+
if (error) reject(error); else resolve(result);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
request.write(this.#body);
|
|
102
|
+
|
|
103
|
+
request.end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _start() {
|
|
108
|
+
let error;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < this.#retryCount+1; i++) {
|
|
111
|
+
if (this.#timer.timeout) this.#timer.id = setTimeout(() => this.#timer.controller.abort(), this.#timer.timeout);
|
|
112
|
+
// debug(`${this.#options.method} ${this.#url.toString()}` + (i ? ` try ${i+1}` : ''));
|
|
113
|
+
|
|
114
|
+
let response, url = this.#url;
|
|
115
|
+
for (let redirects = 0; redirects < this.#redirectCount+1; redirects++) {
|
|
116
|
+
[error, response] = await safe(this._makeRequest(url));
|
|
117
|
+
if (error || (response.status < 300 || response.status > 399) || (this.#options.method !== 'GET')) break;
|
|
118
|
+
url = response.url; // follow
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!error && !this.#okFunc({ status: response.status })) {
|
|
122
|
+
error = new Error(`${response.status} ${http.STATUS_CODES[response.status]}`);
|
|
123
|
+
Object.assign(error, response);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (error) debug(`${this.#options.method} ${this.#url.toString()} ${error.message}`);
|
|
127
|
+
if (this.#timer.timeout) clearTimeout(this.#timer.id);
|
|
128
|
+
if (!error) return response;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
set(name, value) {
|
|
135
|
+
this.#options.headers[name.toLowerCase()] = value;
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
query(data) {
|
|
140
|
+
Object.entries(data).forEach(([key, value]) => this.#url.searchParams.append(key, value));
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
redirects(count) {
|
|
145
|
+
this.#redirectCount = count;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
send(data) {
|
|
150
|
+
const contentType = this.#options.headers['content-type'];
|
|
151
|
+
if (!contentType || contentType === 'application/json') {
|
|
152
|
+
this.#options.headers['content-type'] = 'application/json';
|
|
153
|
+
this.#body = Buffer.from(JSON.stringify(data), 'utf8');
|
|
154
|
+
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
155
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
156
|
+
this.#body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
|
|
157
|
+
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
158
|
+
}
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
timeout(msecs) {
|
|
163
|
+
this.#timer.controller = new AbortController();
|
|
164
|
+
this.#timer.timeout = msecs;
|
|
165
|
+
this.#options.signal = this.#timer.controller.signal;
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
retry(count) {
|
|
170
|
+
this.#retryCount = Math.max(0, count);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ok(func) {
|
|
175
|
+
this.#okFunc = func;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
disableTLSCerts() {
|
|
180
|
+
this.#options.rejectUnauthorized = false;
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
auth(username, password) {
|
|
185
|
+
this.set('Authorization', 'Basic ' + btoa(`${username}:${password}`));
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
field(name, value) {
|
|
190
|
+
if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
191
|
+
|
|
192
|
+
const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n`, 'utf8');
|
|
193
|
+
const partData = Buffer.from(value, 'utf8');
|
|
194
|
+
this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
|
|
195
|
+
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
attach(name, filepathOrBuffer) { // this is only used in tests and thus simplistic
|
|
200
|
+
if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
|
|
201
|
+
|
|
202
|
+
const filename = Buffer.isBuffer(filepathOrBuffer) ? name : path.basename(filepathOrBuffer);
|
|
203
|
+
const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${filename}"\r\n\r\n`, 'utf8');
|
|
204
|
+
const partData = Buffer.isBuffer(filepathOrBuffer) ? filepathOrBuffer : fs.readFileSync(filepathOrBuffer);
|
|
205
|
+
this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
|
|
206
|
+
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
then(onFulfilled, onRejected) {
|
|
211
|
+
if (this.#boundary) {
|
|
212
|
+
const partTrailer = Buffer.from(`--${this.#boundary}--\r\n`, 'utf8');
|
|
213
|
+
this.#body = Buffer.concat([this.#body, partTrailer]);
|
|
214
|
+
|
|
215
|
+
this.#options.headers['content-type'] = `multipart/form-data; boundary=${this.#boundary}`;
|
|
216
|
+
this.#options.headers['content-length'] = this.#body.byteLength;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this._start().then(onFulfilled, onRejected);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function head(url) { return new Request('GET', url); }
|
|
224
|
+
function get(url) { return new Request('GET', url); }
|
|
225
|
+
function put(url) { return new Request('PUT', url); }
|
|
226
|
+
function post(url) { return new Request('POST', url); }
|
|
227
|
+
function patch(url) { return new Request('PATCH', url); }
|
|
228
|
+
function del(url) { return new Request('DELETE', url); }
|
|
229
|
+
function options(url) { return new Request('OPTIONS', url); }
|
|
230
|
+
function request(method, url) { return new Request(method, url); }
|