Skip to content

Commit 8eeab8d

Browse files
authored
feat: Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax (#9980)
1 parent c500fc4 commit 8eeab8d

File tree

3 files changed

+330
-8
lines changed

3 files changed

+330
-8
lines changed

spec/CloudCode.spec.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4788,4 +4788,231 @@ describe('beforePasswordResetRequest hook', () => {
47884788
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
47894789
}).not.toThrow();
47904790
});
4791+
4792+
describe('Express-style cloud functions with (req, res) parameters', () => {
4793+
it('should support express-style cloud function with res.success()', async () => {
4794+
Parse.Cloud.define('expressStyleFunction', (req, res) => {
4795+
res.success({ message: 'Hello from express style!' });
4796+
});
4797+
4798+
const result = await Parse.Cloud.run('expressStyleFunction', {});
4799+
expect(result.message).toEqual('Hello from express style!');
4800+
});
4801+
4802+
it('should support express-style cloud function with res.error()', async () => {
4803+
Parse.Cloud.define('expressStyleError', (req, res) => {
4804+
res.error('Custom error message');
4805+
});
4806+
4807+
await expectAsync(Parse.Cloud.run('expressStyleError', {})).toBeRejectedWith(
4808+
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Custom error message')
4809+
);
4810+
});
4811+
4812+
it('should support setting custom HTTP status code with res.status().success()', async () => {
4813+
Parse.Cloud.define('customStatusCode', (req, res) => {
4814+
res.status(201).success({ created: true });
4815+
});
4816+
4817+
const response = await request({
4818+
method: 'POST',
4819+
url: 'http://localhost:8378/1/functions/customStatusCode',
4820+
headers: {
4821+
'X-Parse-Application-Id': 'test',
4822+
'X-Parse-REST-API-Key': 'rest',
4823+
},
4824+
json: true,
4825+
body: {},
4826+
});
4827+
4828+
expect(response.status).toBe(201);
4829+
expect(response.data.result.created).toBe(true);
4830+
});
4831+
4832+
it('should support 401 unauthorized status code with error', async () => {
4833+
Parse.Cloud.define('unauthorizedFunction', (req, res) => {
4834+
if (!req.user) {
4835+
res.status(401).error('Unauthorized access');
4836+
} else {
4837+
res.success({ message: 'Authorized' });
4838+
}
4839+
});
4840+
4841+
await expectAsync(
4842+
request({
4843+
method: 'POST',
4844+
url: 'http://localhost:8378/1/functions/unauthorizedFunction',
4845+
headers: {
4846+
'X-Parse-Application-Id': 'test',
4847+
'X-Parse-REST-API-Key': 'rest',
4848+
},
4849+
json: true,
4850+
body: {},
4851+
})
4852+
).toBeRejected();
4853+
});
4854+
4855+
it('should support 404 not found status code with error', async () => {
4856+
Parse.Cloud.define('notFoundFunction', (req, res) => {
4857+
res.status(404).error('Resource not found');
4858+
});
4859+
4860+
await expectAsync(
4861+
request({
4862+
method: 'POST',
4863+
url: 'http://localhost:8378/1/functions/notFoundFunction',
4864+
headers: {
4865+
'X-Parse-Application-Id': 'test',
4866+
'X-Parse-REST-API-Key': 'rest',
4867+
},
4868+
json: true,
4869+
body: {},
4870+
})
4871+
).toBeRejected();
4872+
});
4873+
4874+
it('should default to 200 status code when not specified', async () => {
4875+
Parse.Cloud.define('defaultStatusCode', (req, res) => {
4876+
res.success({ message: 'Default status' });
4877+
});
4878+
4879+
const response = await request({
4880+
method: 'POST',
4881+
url: 'http://localhost:8378/1/functions/defaultStatusCode',
4882+
headers: {
4883+
'X-Parse-Application-Id': 'test',
4884+
'X-Parse-REST-API-Key': 'rest',
4885+
},
4886+
json: true,
4887+
body: {},
4888+
});
4889+
4890+
expect(response.status).toBe(200);
4891+
expect(response.data.result.message).toBe('Default status');
4892+
});
4893+
4894+
it('should maintain backward compatibility with single-parameter functions', async () => {
4895+
Parse.Cloud.define('traditionalFunction', (req) => {
4896+
return { message: 'Traditional style works!' };
4897+
});
4898+
4899+
const result = await Parse.Cloud.run('traditionalFunction', {});
4900+
expect(result.message).toEqual('Traditional style works!');
4901+
});
4902+
4903+
it('should maintain backward compatibility with implicit return functions', async () => {
4904+
Parse.Cloud.define('implicitReturnFunction', () => 'Implicit return works!');
4905+
4906+
const result = await Parse.Cloud.run('implicitReturnFunction', {});
4907+
expect(result).toEqual('Implicit return works!');
4908+
});
4909+
4910+
it('should support async express-style functions', async () => {
4911+
Parse.Cloud.define('asyncExpressStyle', async (req, res) => {
4912+
await new Promise(resolve => setTimeout(resolve, 10));
4913+
res.success({ async: true });
4914+
});
4915+
4916+
const result = await Parse.Cloud.run('asyncExpressStyle', {});
4917+
expect(result.async).toBe(true);
4918+
});
4919+
4920+
it('should access request parameters in express-style functions', async () => {
4921+
Parse.Cloud.define('expressWithParams', (req, res) => {
4922+
const { name } = req.params;
4923+
res.success({ greeting: `Hello, ${name}!` });
4924+
});
4925+
4926+
const result = await Parse.Cloud.run('expressWithParams', { name: 'World' });
4927+
expect(result.greeting).toEqual('Hello, World!');
4928+
});
4929+
4930+
it('should access user in express-style functions', async () => {
4931+
const user = new Parse.User();
4932+
user.set('username', 'testuser');
4933+
user.set('password', 'testpass');
4934+
await user.signUp();
4935+
4936+
Parse.Cloud.define('expressWithUser', (req, res) => {
4937+
if (req.user) {
4938+
res.success({ username: req.user.get('username') });
4939+
} else {
4940+
res.status(401).error('Not authenticated');
4941+
}
4942+
});
4943+
4944+
const result = await Parse.Cloud.run('expressWithUser', {});
4945+
expect(result.username).toEqual('testuser');
4946+
4947+
await Parse.User.logOut();
4948+
});
4949+
4950+
it('should support setting custom headers with res.header()', async () => {
4951+
Parse.Cloud.define('customHeaderFunction', (req, res) => {
4952+
res.header('X-Custom-Header', 'custom-value').success({ message: 'OK' });
4953+
});
4954+
4955+
const response = await request({
4956+
method: 'POST',
4957+
url: 'http://localhost:8378/1/functions/customHeaderFunction',
4958+
headers: {
4959+
'X-Parse-Application-Id': 'test',
4960+
'X-Parse-REST-API-Key': 'rest',
4961+
},
4962+
json: true,
4963+
body: {},
4964+
});
4965+
4966+
expect(response.status).toBe(200);
4967+
expect(response.headers['x-custom-header']).toBe('custom-value');
4968+
expect(response.data.result.message).toBe('OK');
4969+
});
4970+
4971+
it('should support setting multiple custom headers', async () => {
4972+
Parse.Cloud.define('multipleHeadersFunction', (req, res) => {
4973+
res.header('X-Header-One', 'value1')
4974+
.header('X-Header-Two', 'value2')
4975+
.success({ message: 'Multiple headers' });
4976+
});
4977+
4978+
const response = await request({
4979+
method: 'POST',
4980+
url: 'http://localhost:8378/1/functions/multipleHeadersFunction',
4981+
headers: {
4982+
'X-Parse-Application-Id': 'test',
4983+
'X-Parse-REST-API-Key': 'rest',
4984+
},
4985+
json: true,
4986+
body: {},
4987+
});
4988+
4989+
expect(response.status).toBe(200);
4990+
expect(response.headers['x-header-one']).toBe('value1');
4991+
expect(response.headers['x-header-two']).toBe('value2');
4992+
expect(response.data.result.message).toBe('Multiple headers');
4993+
});
4994+
4995+
it('should support combining status code and custom headers', async () => {
4996+
Parse.Cloud.define('statusAndHeaderFunction', (req, res) => {
4997+
res.status(201)
4998+
.header('X-Resource-Id', '12345')
4999+
.success({ created: true });
5000+
});
5001+
5002+
const response = await request({
5003+
method: 'POST',
5004+
url: 'http://localhost:8378/1/functions/statusAndHeaderFunction',
5005+
headers: {
5006+
'X-Parse-Application-Id': 'test',
5007+
'X-Parse-REST-API-Key': 'rest',
5008+
},
5009+
json: true,
5010+
body: {},
5011+
});
5012+
5013+
expect(response.status).toBe(201);
5014+
expect(response.headers['x-resource-id']).toBe('12345');
5015+
expect(response.data.result.created).toBe(true);
5016+
});
5017+
});
47915018
});

src/Routers/FunctionsRouter.js

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,52 @@ export class FunctionsRouter extends PromiseRouter {
103103
});
104104
}
105105

106-
static createResponseObject(resolve, reject) {
107-
return {
106+
static createResponseObject(resolve, reject, statusCode = null) {
107+
let httpStatusCode = statusCode;
108+
const customHeaders = {};
109+
let responseSent = false;
110+
const responseObject = {
108111
success: function (result) {
109-
resolve({
112+
if (responseSent) {
113+
throw new Error('Cannot call success() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
114+
}
115+
responseSent = true;
116+
const response = {
110117
response: {
111118
result: Parse._encode(result),
112119
},
113-
});
120+
};
121+
if (httpStatusCode !== null) {
122+
response.status = httpStatusCode;
123+
}
124+
if (Object.keys(customHeaders).length > 0) {
125+
response.headers = customHeaders;
126+
}
127+
resolve(response);
114128
},
115129
error: function (message) {
130+
if (responseSent) {
131+
throw new Error('Cannot call error() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
132+
}
133+
responseSent = true;
116134
const error = triggers.resolveError(message);
135+
// If a custom status code was set, attach it to the error
136+
if (httpStatusCode !== null) {
137+
error.status = httpStatusCode;
138+
}
117139
reject(error);
118140
},
141+
status: function (code) {
142+
httpStatusCode = code;
143+
return responseObject;
144+
},
145+
header: function (key, value) {
146+
customHeaders[key] = value;
147+
return responseObject;
148+
},
149+
_isResponseSent: () => responseSent,
119150
};
151+
return responseObject;
120152
}
121153
static handleCloudFunction(req) {
122154
const functionName = req.params.functionName;
@@ -143,7 +175,7 @@ export class FunctionsRouter extends PromiseRouter {
143175

144176
return new Promise(function (resolve, reject) {
145177
const userString = req.auth && req.auth.user ? req.auth.user.id : undefined;
146-
const { success, error } = FunctionsRouter.createResponseObject(
178+
const responseObject = FunctionsRouter.createResponseObject(
147179
result => {
148180
try {
149181
if (req.config.logLevels.cloudFunctionSuccess !== 'silent') {
@@ -184,14 +216,37 @@ export class FunctionsRouter extends PromiseRouter {
184216
}
185217
}
186218
);
219+
const { success, error } = responseObject;
220+
187221
return Promise.resolve()
188222
.then(() => {
189223
return triggers.maybeRunValidator(request, functionName, req.auth);
190224
})
191225
.then(() => {
192-
return theFunction(request);
226+
// Check if function expects 2 parameters (req, res) - Express style
227+
if (theFunction.length >= 2) {
228+
return theFunction(request, responseObject);
229+
} else {
230+
// Traditional style - single parameter
231+
return theFunction(request);
232+
}
193233
})
194-
.then(success, error);
234+
.then(result => {
235+
// For Express-style functions, only send response if not already sent
236+
if (theFunction.length >= 2) {
237+
if (!responseObject._isResponseSent()) {
238+
// If Express-style function returns a value without calling res.success/error
239+
if (result !== undefined) {
240+
success(result);
241+
}
242+
// If no response sent and no value returned, this is an error in user code
243+
// but we don't handle it here to maintain backward compatibility
244+
}
245+
} else {
246+
// For traditional functions, always call success with the result (even if undefined)
247+
success(result);
248+
}
249+
}, error);
195250
});
196251
}
197252
}

0 commit comments

Comments
 (0)