zapier-platform-core 9.4.0 → 9.6.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 +6 -4
- package/src/create-command-handler.js +2 -2
- package/src/errors.js +17 -10
- package/src/http-middlewares/before/disable-ssl-cert-check.js +10 -2
- package/src/tools/create-file-stasher.js +245 -114
- package/src/tools/create-lambda-handler.js +2 -1
- package/src/tools/fetch.js +27 -3
- package/src/tools/schema-tools.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-core",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.0",
|
|
4
4
|
"description": "The core SDK for CLI apps in the Zapier Developer Platform.",
|
|
5
5
|
"repository": "zapier/zapier-platform-core",
|
|
6
6
|
"homepage": "https://zapier.com/",
|
|
@@ -41,16 +41,18 @@
|
|
|
41
41
|
"bluebird": "3.5.5",
|
|
42
42
|
"content-disposition": "0.5.3",
|
|
43
43
|
"dotenv": "8.1.0",
|
|
44
|
-
"form-data": "
|
|
44
|
+
"form-data": "4.0.0",
|
|
45
45
|
"lodash": "4.17.15",
|
|
46
|
-
"
|
|
46
|
+
"mime-types": "2.1.34",
|
|
47
|
+
"node-fetch": "2.6.6",
|
|
47
48
|
"oauth-sign": "0.9.0",
|
|
48
49
|
"semver": "5.6.0",
|
|
49
|
-
"zapier-platform-schema": "9.
|
|
50
|
+
"zapier-platform-schema": "9.6.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"adm-zip": "0.4.13",
|
|
53
54
|
"aws-sdk": "2.238.1",
|
|
55
|
+
"dicer": "0.3.0",
|
|
54
56
|
"fs-extra": "8.1.0",
|
|
55
57
|
"mock-fs": "4.10.1"
|
|
56
58
|
},
|
|
@@ -18,7 +18,7 @@ const commandHandlers = {
|
|
|
18
18
|
commands like 'execute', 'validate', 'definition', 'request'.
|
|
19
19
|
*/
|
|
20
20
|
const createCommandHandler = compiledApp => {
|
|
21
|
-
return input => {
|
|
21
|
+
return async input => {
|
|
22
22
|
const command = input._zapier.event.command || 'execute'; // validate || definition || request
|
|
23
23
|
const handler = commandHandlers[command];
|
|
24
24
|
if (!handler) {
|
|
@@ -26,7 +26,7 @@ const createCommandHandler = compiledApp => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
try {
|
|
29
|
-
return handler(compiledApp, input);
|
|
29
|
+
return await handler(compiledApp, input);
|
|
30
30
|
} catch (err) {
|
|
31
31
|
return handleError(err);
|
|
32
32
|
}
|
package/src/errors.js
CHANGED
|
@@ -9,7 +9,7 @@ class AppError extends Error {
|
|
|
9
9
|
JSON.stringify({
|
|
10
10
|
message,
|
|
11
11
|
code,
|
|
12
|
-
status
|
|
12
|
+
status,
|
|
13
13
|
})
|
|
14
14
|
);
|
|
15
15
|
this.name = 'AppError';
|
|
@@ -19,16 +19,23 @@ class AppError extends Error {
|
|
|
19
19
|
|
|
20
20
|
class ResponseError extends Error {
|
|
21
21
|
constructor(response) {
|
|
22
|
+
let content;
|
|
23
|
+
try {
|
|
24
|
+
content = response.content;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// Stream request (z.request({raw: true})) doesn't have response.content
|
|
27
|
+
content = null;
|
|
28
|
+
}
|
|
22
29
|
super(
|
|
23
30
|
JSON.stringify({
|
|
24
31
|
status: response.status,
|
|
25
32
|
headers: {
|
|
26
|
-
'content-type': response.headers.get('content-type')
|
|
33
|
+
'content-type': response.headers.get('content-type'),
|
|
27
34
|
},
|
|
28
|
-
content
|
|
35
|
+
content,
|
|
29
36
|
request: {
|
|
30
|
-
url: response.request.url
|
|
31
|
-
}
|
|
37
|
+
url: response.request.url,
|
|
38
|
+
},
|
|
32
39
|
})
|
|
33
40
|
);
|
|
34
41
|
this.name = 'ResponseError';
|
|
@@ -37,8 +44,8 @@ class ResponseError extends Error {
|
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// Make some of the errors we'll use!
|
|
40
|
-
const createError = name => {
|
|
41
|
-
const NewError = function(message = '') {
|
|
47
|
+
const createError = (name) => {
|
|
48
|
+
const NewError = function (message = '') {
|
|
42
49
|
this.name = name;
|
|
43
50
|
this.message = message;
|
|
44
51
|
Error.call(this);
|
|
@@ -57,7 +64,7 @@ const names = [
|
|
|
57
64
|
'NotImplementedError',
|
|
58
65
|
'RefreshAuthError',
|
|
59
66
|
'RequireModuleError',
|
|
60
|
-
'StopRequestError'
|
|
67
|
+
'StopRequestError',
|
|
61
68
|
];
|
|
62
69
|
|
|
63
70
|
const exceptions = _.reduce(
|
|
@@ -68,7 +75,7 @@ const exceptions = _.reduce(
|
|
|
68
75
|
},
|
|
69
76
|
{
|
|
70
77
|
Error: AppError,
|
|
71
|
-
ResponseError
|
|
78
|
+
ResponseError,
|
|
72
79
|
}
|
|
73
80
|
);
|
|
74
81
|
|
|
@@ -89,5 +96,5 @@ const handleError = (...args) => {
|
|
|
89
96
|
|
|
90
97
|
module.exports = {
|
|
91
98
|
...exceptions,
|
|
92
|
-
handleError
|
|
99
|
+
handleError,
|
|
93
100
|
};
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const http = require('http');
|
|
3
4
|
const https = require('https');
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const httpAgent = new http.Agent({ rejectUnauthorized: false });
|
|
7
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
8
|
+
|
|
9
|
+
const disableSSLCertCheck = (req) => {
|
|
6
10
|
if (req.agent) {
|
|
7
11
|
req.agent.options.rejectUnauthorized = false;
|
|
8
12
|
} else if (req.url.startsWith('https://')) {
|
|
9
|
-
|
|
13
|
+
// Need to dynamically choose a different agent because redirection can be
|
|
14
|
+
// across HTTPS and HTTP.
|
|
15
|
+
// See https://github.com/node-fetch/node-fetch/tree/6ee9d318#custom-agent
|
|
16
|
+
req.agent = (parsedURL) =>
|
|
17
|
+
parsedURL.protocol === 'http:' ? httpAgent : httpsAgent;
|
|
10
18
|
}
|
|
11
19
|
return req;
|
|
12
20
|
};
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
|
-
const
|
|
6
|
+
const { pipeline } = require('stream');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
const { randomBytes } = require('crypto');
|
|
9
|
+
|
|
10
|
+
const _ = require('lodash');
|
|
6
11
|
const contentDisposition = require('content-disposition');
|
|
12
|
+
const FormData = require('form-data');
|
|
13
|
+
const mime = require('mime-types');
|
|
7
14
|
|
|
8
15
|
const request = require('./request-client-internal');
|
|
9
|
-
const ZapierPromise = require('./promise');
|
|
10
|
-
|
|
11
|
-
const isPromise = obj => obj && typeof obj.then === 'function';
|
|
12
16
|
|
|
13
17
|
const UPLOAD_MAX_SIZE = 1000 * 1000 * 150; // 150mb, in zapier backend too
|
|
14
18
|
|
|
@@ -19,33 +23,187 @@ const LENGTH_ERR_MESSAGE =
|
|
|
19
23
|
const DEFAULT_FILE_NAME = 'unnamedfile';
|
|
20
24
|
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
21
25
|
|
|
22
|
-
const
|
|
26
|
+
const streamPipeline = promisify(pipeline);
|
|
27
|
+
|
|
28
|
+
const filenameFromURL = (url) => {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(path.posix.basename(new URL(url).pathname));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const filenameFromHeader = (response) => {
|
|
37
|
+
const cd = response.headers.get('content-disposition');
|
|
38
|
+
let filename;
|
|
39
|
+
if (cd) {
|
|
40
|
+
try {
|
|
41
|
+
filename = contentDisposition.parse(cd).parameters.filename;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return filename || null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const resolveRemoteStream = async (stream) => {
|
|
50
|
+
// Download to a temp file, get the file size, and create a readable stream
|
|
51
|
+
// from the temp file.
|
|
52
|
+
//
|
|
53
|
+
// The streamPipeline usage is taken from
|
|
54
|
+
// https://github.com/node-fetch/node-fetch#streams
|
|
55
|
+
const tmpFilePath = path.join(
|
|
56
|
+
os.tmpdir(),
|
|
57
|
+
'stash-' + randomBytes(16).toString('hex')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await streamPipeline(stream, fs.createWriteStream(tmpFilePath));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
try {
|
|
64
|
+
fs.unlinkSync(tmpFilePath);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// File doesn't exist? Probably okay
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const length = fs.statSync(tmpFilePath).size;
|
|
72
|
+
const readStream = fs.createReadStream(tmpFilePath);
|
|
73
|
+
|
|
74
|
+
readStream.on('end', () => {
|
|
75
|
+
// Burn after reading
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(tmpFilePath);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// TODO: We probably want to log warning here
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
streamOrData: readStream,
|
|
85
|
+
length,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const resolveResponseToStream = async (response) => {
|
|
90
|
+
// Get filename from content-disposition header or URL
|
|
91
|
+
let filename =
|
|
92
|
+
filenameFromHeader(response) ||
|
|
93
|
+
filenameFromURL(response.url || _.get(response, ['request', 'url'])) ||
|
|
94
|
+
DEFAULT_FILE_NAME;
|
|
95
|
+
|
|
96
|
+
const contentType = response.headers.get('content-type');
|
|
97
|
+
if (contentType && !path.extname(filename)) {
|
|
98
|
+
const ext = mime.extension(contentType);
|
|
99
|
+
if (ext && ext !== 'bin') {
|
|
100
|
+
filename += '.' + ext;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (response.body && typeof response.body.pipe === 'function') {
|
|
105
|
+
// streamable response created by z.request({ raw: true })
|
|
106
|
+
return {
|
|
107
|
+
...(await resolveRemoteStream(response.body)),
|
|
108
|
+
contentType: contentType || DEFAULT_CONTENT_TYPE,
|
|
109
|
+
filename,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// regular response created by z.request({ raw: false })
|
|
114
|
+
return {
|
|
115
|
+
streamOrData: response.content,
|
|
116
|
+
length: Buffer.byteLength(response.content),
|
|
117
|
+
contentType: contentType || DEFAULT_CONTENT_TYPE,
|
|
118
|
+
filename,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const resolveStreamWithMeta = async (stream) => {
|
|
123
|
+
const isLocalFile = stream.path && fs.existsSync(stream.path);
|
|
124
|
+
if (isLocalFile) {
|
|
125
|
+
const filename = path.basename(stream.path);
|
|
126
|
+
return {
|
|
127
|
+
streamOrData: stream,
|
|
128
|
+
length: fs.statSync(stream.path).size,
|
|
129
|
+
contentType: mime.lookup(filename) || DEFAULT_CONTENT_TYPE,
|
|
130
|
+
filename,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...(await resolveRemoteStream(stream)),
|
|
136
|
+
contentType: DEFAULT_CONTENT_TYPE,
|
|
137
|
+
filename: DEFAULT_FILE_NAME,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Returns an object with fields:
|
|
142
|
+
// * streamOrData: a readable stream, a string, or a Buffer
|
|
143
|
+
// * length: content length in bytes
|
|
144
|
+
// * contentType
|
|
145
|
+
// * filename
|
|
146
|
+
const resolveToBufferStringStream = async (responseOrData) => {
|
|
147
|
+
if (typeof responseOrData === 'string' || responseOrData instanceof String) {
|
|
148
|
+
// The .toString() call only makes a difference for the String object case.
|
|
149
|
+
// It converts a String object to a regular string.
|
|
150
|
+
const str = responseOrData.toString();
|
|
151
|
+
return {
|
|
152
|
+
streamOrData: str,
|
|
153
|
+
length: Buffer.byteLength(str),
|
|
154
|
+
contentType: 'text/plain',
|
|
155
|
+
filename: `${DEFAULT_FILE_NAME}.txt`,
|
|
156
|
+
};
|
|
157
|
+
} else if (Buffer.isBuffer(responseOrData)) {
|
|
158
|
+
return {
|
|
159
|
+
streamOrData: responseOrData,
|
|
160
|
+
length: responseOrData.length,
|
|
161
|
+
contentType: DEFAULT_CONTENT_TYPE,
|
|
162
|
+
filename: DEFAULT_FILE_NAME,
|
|
163
|
+
};
|
|
164
|
+
} else if (
|
|
165
|
+
(responseOrData.body && typeof responseOrData.body.pipe === 'function') ||
|
|
166
|
+
typeof responseOrData.content === 'string'
|
|
167
|
+
) {
|
|
168
|
+
return resolveResponseToStream(responseOrData);
|
|
169
|
+
} else if (typeof responseOrData.pipe === 'function') {
|
|
170
|
+
return resolveStreamWithMeta(responseOrData);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
`z.stashFile() cannot stash type '${typeof responseOrData}'. ` +
|
|
175
|
+
'Pass it a request, readable stream, string, or Buffer.'
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const uploader = async (
|
|
23
180
|
signedPostData,
|
|
24
181
|
bufferStringStream,
|
|
25
182
|
knownLength,
|
|
26
183
|
filename,
|
|
27
184
|
contentType
|
|
28
185
|
) => {
|
|
29
|
-
const form = new FormData();
|
|
30
|
-
|
|
31
186
|
if (knownLength && knownLength > UPLOAD_MAX_SIZE) {
|
|
32
|
-
|
|
33
|
-
new Error(`${knownLength} is too big, ${UPLOAD_MAX_SIZE} is the max`)
|
|
34
|
-
);
|
|
187
|
+
throw new Error(`${knownLength} is too big, ${UPLOAD_MAX_SIZE} is the max`);
|
|
35
188
|
}
|
|
189
|
+
filename = path.basename(filename).replace('"', '');
|
|
36
190
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
191
|
+
const fields = {
|
|
192
|
+
...signedPostData.fields,
|
|
193
|
+
'Content-Disposition': contentDisposition(filename),
|
|
194
|
+
'Content-Type': contentType,
|
|
195
|
+
};
|
|
40
196
|
|
|
41
|
-
|
|
197
|
+
const form = new FormData();
|
|
42
198
|
|
|
43
|
-
|
|
199
|
+
Object.entries(fields).forEach(([key, value]) => {
|
|
200
|
+
form.append(key, value);
|
|
201
|
+
});
|
|
44
202
|
|
|
45
203
|
form.append('file', bufferStringStream, {
|
|
46
|
-
contentType,
|
|
47
204
|
knownLength,
|
|
48
|
-
|
|
205
|
+
contentType,
|
|
206
|
+
filename,
|
|
49
207
|
});
|
|
50
208
|
|
|
51
209
|
// Try to catch the missing length early, before upload to S3 fails.
|
|
@@ -56,120 +214,93 @@ const uploader = (
|
|
|
56
214
|
}
|
|
57
215
|
|
|
58
216
|
// Send to S3 with presigned request.
|
|
59
|
-
|
|
217
|
+
const response = await request({
|
|
60
218
|
url: signedPostData.url,
|
|
61
219
|
method: 'POST',
|
|
62
|
-
body: form
|
|
63
|
-
}).then(res => {
|
|
64
|
-
if (res.status === 204) {
|
|
65
|
-
return `${signedPostData.url}${signedPostData.fields.key}`;
|
|
66
|
-
}
|
|
67
|
-
if (
|
|
68
|
-
res.content.indexOf(
|
|
69
|
-
'You must provide the Content-Length HTTP header.'
|
|
70
|
-
) !== -1
|
|
71
|
-
) {
|
|
72
|
-
throw new Error(LENGTH_ERR_MESSAGE);
|
|
73
|
-
}
|
|
74
|
-
throw new Error(`Got ${res.status} - ${res.content}`);
|
|
220
|
+
body: form,
|
|
75
221
|
});
|
|
222
|
+
|
|
223
|
+
if (response.status === 204) {
|
|
224
|
+
return new URL(signedPostData.fields.key, signedPostData.url).href;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
response.content &&
|
|
229
|
+
response.content.includes &&
|
|
230
|
+
response.content.includes(
|
|
231
|
+
'You must provide the Content-Length HTTP header.'
|
|
232
|
+
)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(LENGTH_ERR_MESSAGE);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(`Got ${response.status} - ${response.content}`);
|
|
76
238
|
};
|
|
77
239
|
|
|
78
240
|
// Designed to be some user provided function/api.
|
|
79
|
-
const createFileStasher = input => {
|
|
241
|
+
const createFileStasher = (input) => {
|
|
80
242
|
const rpc = _.get(input, '_zapier.rpc');
|
|
81
243
|
|
|
82
|
-
return (
|
|
244
|
+
return async (requestOrData, knownLength, filename, contentType) => {
|
|
83
245
|
// TODO: maybe this could be smart?
|
|
84
246
|
// if it is already a public url, do we pass through? or upload?
|
|
85
247
|
if (!rpc) {
|
|
86
|
-
|
|
248
|
+
throw new Error('rpc is not available');
|
|
87
249
|
}
|
|
88
250
|
|
|
89
|
-
const isRunningOnHydrator =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
251
|
+
const isRunningOnHydrator = _.get(
|
|
252
|
+
input,
|
|
253
|
+
'_zapier.event.method',
|
|
254
|
+
''
|
|
255
|
+
).startsWith('hydrators.');
|
|
256
|
+
const isRunningOnCreate = _.get(
|
|
257
|
+
input,
|
|
258
|
+
'_zapier.event.method',
|
|
259
|
+
''
|
|
260
|
+
).startsWith('creates.');
|
|
93
261
|
|
|
94
262
|
if (!isRunningOnHydrator && !isRunningOnCreate) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'Files can only be stashed within a create or hydration function/method.'
|
|
98
|
-
)
|
|
263
|
+
throw new Error(
|
|
264
|
+
'Files can only be stashed within a create or hydration function/method.'
|
|
99
265
|
);
|
|
100
266
|
}
|
|
101
267
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
throw new Error(
|
|
140
|
-
'Cannot stash a Promise wrapped file of unknown type.'
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return uploader(
|
|
145
|
-
result,
|
|
146
|
-
newBufferStringStream,
|
|
147
|
-
knownLength,
|
|
148
|
-
filename,
|
|
149
|
-
fileContentType
|
|
150
|
-
);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
if (isStreamed) {
|
|
154
|
-
maybeResponse.throwForStatus();
|
|
155
|
-
return maybeResponse.buffer().then(buffer => {
|
|
156
|
-
maybeResponse.dataBuffer = buffer;
|
|
157
|
-
return parseFinalResponse(maybeResponse);
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
return parseFinalResponse(maybeResponse);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
} else {
|
|
164
|
-
return uploader(
|
|
165
|
-
result,
|
|
166
|
-
bufferStringStream,
|
|
167
|
-
knownLength,
|
|
168
|
-
filename,
|
|
169
|
-
fileContentType
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
268
|
+
// requestOrData can be one of these:
|
|
269
|
+
// * string
|
|
270
|
+
// * Buffer
|
|
271
|
+
// * z.request() - a Promise of a regular response
|
|
272
|
+
// * z.request({ raw: true }) - a Promise of a "streamable" response
|
|
273
|
+
// * await z.request() - a regular response
|
|
274
|
+
// * await z.request({ raw: true }) - a streamable response
|
|
275
|
+
//
|
|
276
|
+
// After the following, requestOrData is resolved to responseOrData, which
|
|
277
|
+
// is either:
|
|
278
|
+
// - string
|
|
279
|
+
// - Buffer
|
|
280
|
+
// - a regular response
|
|
281
|
+
// - a streamable response
|
|
282
|
+
const [signedPostData, responseOrData] = await Promise.all([
|
|
283
|
+
rpc('get_presigned_upload_post_data'),
|
|
284
|
+
requestOrData,
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
if (responseOrData.throwForStatus) {
|
|
288
|
+
responseOrData.throwForStatus();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
streamOrData,
|
|
293
|
+
length,
|
|
294
|
+
contentType: _contentType,
|
|
295
|
+
filename: _filename,
|
|
296
|
+
} = await resolveToBufferStringStream(responseOrData);
|
|
297
|
+
|
|
298
|
+
return uploader(
|
|
299
|
+
signedPostData,
|
|
300
|
+
streamOrData,
|
|
301
|
+
knownLength || length,
|
|
302
|
+
filename || _filename,
|
|
303
|
+
contentType || _contentType
|
|
173
304
|
);
|
|
174
305
|
};
|
|
175
306
|
};
|
|
@@ -204,7 +204,8 @@ const createLambdaHandler = appRawOrPath => {
|
|
|
204
204
|
// Adds logging for _all_ kinds of http(s) requests, no matter the library
|
|
205
205
|
if (!skipHttpPatch) {
|
|
206
206
|
const httpPatch = createHttpPatch(event);
|
|
207
|
-
httpPatch(require('http'));
|
|
207
|
+
httpPatch(require('http'));
|
|
208
|
+
httpPatch(require('https')); // 'https' needs to be patched separately
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// TODO: Avoid calling prepareApp(appRaw) repeatedly here as createApp()
|
package/src/tools/fetch.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { Writable } = require('stream');
|
|
4
|
+
|
|
3
5
|
const fetch = require('node-fetch');
|
|
4
6
|
|
|
5
7
|
// XXX: PatchedRequest is to get past node-fetch's check that forbids GET requests
|
|
@@ -7,10 +9,10 @@ const fetch = require('node-fetch');
|
|
|
7
9
|
// https://github.com/node-fetch/node-fetch/blob/v2.6.0/src/request.js#L75-L78
|
|
8
10
|
class PatchedRequest extends fetch.Request {
|
|
9
11
|
constructor(url, opts) {
|
|
10
|
-
const origMethod = (opts.method || 'GET').toUpperCase();
|
|
12
|
+
const origMethod = ((opts && opts.method) || 'GET').toUpperCase();
|
|
11
13
|
|
|
12
14
|
const isGetWithBody =
|
|
13
|
-
(origMethod === 'GET' || origMethod === 'HEAD') && opts.body;
|
|
15
|
+
(origMethod === 'GET' || origMethod === 'HEAD') && opts && opts.body;
|
|
14
16
|
let newOpts = opts;
|
|
15
17
|
if (isGetWithBody) {
|
|
16
18
|
// Temporary remove body to fool fetch.Request constructor
|
|
@@ -50,9 +52,31 @@ class PatchedRequest extends fetch.Request {
|
|
|
50
52
|
|
|
51
53
|
const newFetch = (url, opts) => {
|
|
52
54
|
const request = new PatchedRequest(url, opts);
|
|
55
|
+
|
|
53
56
|
// fetch actually accepts a Request object as an argument. It'll clone the
|
|
54
57
|
// request internally, that's why the PatchedRequest.body hack works.
|
|
55
|
-
|
|
58
|
+
const responsePromise = fetch(request);
|
|
59
|
+
|
|
60
|
+
// node-fetch clones request.body and use the cloned body internally. We need
|
|
61
|
+
// to make sure to consume the original body stream so its internal buffer is
|
|
62
|
+
// not filled up, which causes it to pause.
|
|
63
|
+
// See https://github.com/node-fetch/node-fetch/issues/151
|
|
64
|
+
//
|
|
65
|
+
// Exclude form-data object to be consistent with
|
|
66
|
+
// https://github.com/node-fetch/node-fetch/blob/v2.6.6/src/body.js#L403-L412
|
|
67
|
+
if (
|
|
68
|
+
request.body &&
|
|
69
|
+
typeof request.body.pipe === 'function' &&
|
|
70
|
+
typeof request.body.getBoundary !== 'function'
|
|
71
|
+
) {
|
|
72
|
+
const nullStream = new Writable();
|
|
73
|
+
nullStream._write = function (chunk, encoding, done) {
|
|
74
|
+
done();
|
|
75
|
+
};
|
|
76
|
+
request.body.pipe(nullStream);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return responsePromise;
|
|
56
80
|
};
|
|
57
81
|
|
|
58
82
|
newFetch.Promise = require('./promise');
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const dataTools = require('./data');
|
|
4
4
|
|
|
5
|
+
// AsyncFunction is not a global object and can be obtained in this way
|
|
6
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
|
|
7
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
8
|
+
|
|
5
9
|
const makeFunction = (source, args = []) => {
|
|
6
10
|
try {
|
|
7
|
-
return
|
|
11
|
+
return new AsyncFunction(...args, source);
|
|
8
12
|
} catch (err) {
|
|
9
13
|
return () => {
|
|
10
14
|
throw err;
|