Skip to content

Commit 59d6e80

Browse files
authored
feat: add function serialization to actor input (#169)
1 parent a488bd8 commit 59d6e80

File tree

5 files changed

+100
-1
lines changed

5 files changed

+100
-1
lines changed

src/interceptors.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const axios = require('axios');
2+
const contentTypeParser = require('content-type');
23
const { maybeParseBody } = require('./body_parser');
34
const {
45
isNode,
@@ -28,10 +29,46 @@ class InvalidResponseBodyError extends Error {
2829
*/
2930
function serializeRequest(config) {
3031
const [defaultTransform] = axios.defaults.transformRequest;
31-
config.data = defaultTransform(config.data, config.headers);
32+
33+
// The function not only serializes data, but it also adds correct headers.
34+
const data = defaultTransform(config.data, config.headers);
35+
36+
// Actor inputs can include functions and we don't want to omit those,
37+
// because it's convenient for users. JSON.stringify removes them.
38+
// It's a bit inefficient that we serialize the JSON twice, but I feel
39+
// it's a small price to pay. The axios default transform does a lot
40+
// of body type checks and we would have to copy all of them to the resource clients.
41+
if (config.stringifyFunctions) {
42+
const contentTypeHeader = config.headers['Content-Type'] || config.headers['content-type'];
43+
try {
44+
const { type } = contentTypeParser.parse(contentTypeHeader);
45+
if (type === 'application/json') {
46+
config.data = stringifyWithFunctions(config.data);
47+
} else {
48+
config.data = data;
49+
}
50+
} catch (err) {
51+
config.data = data;
52+
}
53+
} else {
54+
config.data = data;
55+
}
56+
3257
return config;
3358
}
3459

60+
/**
61+
* JSON.stringify() that serializes functions to string instead
62+
* of replacing them with null or removing them.
63+
* @param {object} obj
64+
* @return {string}
65+
*/
66+
function stringifyWithFunctions(obj) {
67+
return JSON.stringify(obj, (key, value) => {
68+
return typeof value === 'function' ? value.toString() : value;
69+
});
70+
}
71+
3572
/**
3673
* @param {object} config
3774
* @return {Promise<object>}

src/resource_clients/actor.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ class ActorClient extends ResourceClient {
9494
method: 'POST',
9595
data: input,
9696
params: this._params(params),
97+
// Apify internal property. Tells the request serialization interceptor
98+
// to stringify functions to JSON, instead of omitting them.
99+
stringifyFunctions: true,
97100
};
98101
if (options.contentType) {
99102
request.headers = {

src/resource_clients/task.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ class TaskClient extends ResourceClient {
8888
method: 'POST',
8989
data: input,
9090
params: this._params(params),
91+
// Apify internal property. Tells the request serialization interceptor
92+
// to stringify functions to JSON, instead of omitting them.
93+
stringifyFunctions: true,
9194
};
9295

9396
const response = await this.httpClient.call(request);

test/actors.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,34 @@ describe('Actor methods', () => {
133133
validateRequest(query, { actorId }, { some: 'body' }, { 'content-type': contentType });
134134
});
135135

136+
test('start() works with functions in input', async () => {
137+
const actorId = 'some-id';
138+
const input = {
139+
foo: 'bar',
140+
fn: async (a, b) => a + b,
141+
};
142+
143+
const expectedRequestProps = [
144+
{},
145+
{ actorId },
146+
{ foo: 'bar', fn: input.fn.toString() },
147+
{ 'content-type': 'application/json;charset=utf-8' },
148+
];
149+
150+
const res = await client.actor(actorId).start(input);
151+
expect(res.id).toEqual('run-actor');
152+
validateRequest(...expectedRequestProps);
153+
154+
const browserRes = await page.evaluate((id) => {
155+
return client.actor(id).start({
156+
foo: 'bar',
157+
fn: async (a, b) => a + b,
158+
});
159+
}, actorId);
160+
expect(browserRes).toEqual(res);
161+
validateRequest(...expectedRequestProps);
162+
});
163+
136164
test('start() with webhook works', async () => {
137165
const actorId = 'some-id';
138166
const webhooks = [

test/tasks.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,34 @@ describe('Task methods', () => {
163163
validateRequest(query, { taskId }, input);
164164
});
165165

166+
test('start() works with functions in input', async () => {
167+
const taskId = 'some-id';
168+
const input = {
169+
foo: 'bar',
170+
fn: async (a, b) => a + b,
171+
};
172+
173+
const expectedRequestProps = [
174+
{},
175+
{ taskId },
176+
{ foo: 'bar', fn: input.fn.toString() },
177+
{ 'content-type': 'application/json;charset=utf-8' },
178+
];
179+
180+
const res = await client.task(taskId).start(input);
181+
expect(res.id).toEqual('run-task');
182+
validateRequest(...expectedRequestProps);
183+
184+
const browserRes = await page.evaluate((id) => {
185+
return client.task(id).start({
186+
foo: 'bar',
187+
fn: async (a, b) => a + b,
188+
});
189+
}, taskId);
190+
expect(browserRes).toEqual(res);
191+
validateRequest(...expectedRequestProps);
192+
});
193+
166194
test('start() works with webhooks', async () => {
167195
const taskId = 'some-id';
168196
const webhooks = [

0 commit comments

Comments
 (0)