From bbf83dcc4e0805ae94439e24868e74f3f42b03e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:20:08 +0000 Subject: [PATCH 01/11] Initial plan From 90a96ce7720e9229737f18e468b17d141f61fef8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:26:49 +0000 Subject: [PATCH 02/11] Implement Express-style (req, res) support for cloud functions Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 157 +++++++++++++++++++++++++++++++++ src/Routers/FunctionsRouter.js | 37 ++++++-- 2 files changed, 187 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 400efbc380..efc6cf0b6c 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -4788,4 +4788,161 @@ describe('beforePasswordResetRequest hook', () => { Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { }); }).not.toThrow(); }); + + describe('Express-style cloud functions with (req, res) parameters', () => { + it('should support express-style cloud function with res.success()', async () => { + Parse.Cloud.define('expressStyleFunction', (req, res) => { + res.success({ message: 'Hello from express style!' }); + }); + + const result = await Parse.Cloud.run('expressStyleFunction', {}); + expect(result.message).toEqual('Hello from express style!'); + }); + + it('should support express-style cloud function with res.error()', async () => { + Parse.Cloud.define('expressStyleError', (req, res) => { + res.error('Custom error message'); + }); + + await expectAsync(Parse.Cloud.run('expressStyleError', {})).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Custom error message') + ); + }); + + it('should support setting custom HTTP status code with res.status().success()', async () => { + Parse.Cloud.define('customStatusCode', (req, res) => { + res.status(201).success({ created: true }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/customStatusCode', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(201); + expect(response.data.result.created).toBe(true); + }); + + it('should support 401 unauthorized status code', async () => { + Parse.Cloud.define('unauthorizedFunction', (req, res) => { + res.status(401).success({ error: 'Unauthorized access' }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/unauthorizedFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(401); + expect(response.data.result.error).toBe('Unauthorized access'); + }); + + it('should support 404 not found status code', async () => { + Parse.Cloud.define('notFoundFunction', (req, res) => { + res.status(404).success({ error: 'Resource not found' }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/notFoundFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(404); + expect(response.data.result.error).toBe('Resource not found'); + }); + + it('should default to 200 status code when not specified', async () => { + Parse.Cloud.define('defaultStatusCode', (req, res) => { + res.success({ message: 'Default status' }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/defaultStatusCode', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(200); + expect(response.data.result.message).toBe('Default status'); + }); + + it('should maintain backward compatibility with single-parameter functions', async () => { + Parse.Cloud.define('traditionalFunction', (req) => { + return { message: 'Traditional style works!' }; + }); + + const result = await Parse.Cloud.run('traditionalFunction', {}); + expect(result.message).toEqual('Traditional style works!'); + }); + + it('should maintain backward compatibility with implicit return functions', async () => { + Parse.Cloud.define('implicitReturnFunction', () => 'Implicit return works!'); + + const result = await Parse.Cloud.run('implicitReturnFunction', {}); + expect(result).toEqual('Implicit return works!'); + }); + + it('should support async express-style functions', async () => { + Parse.Cloud.define('asyncExpressStyle', async (req, res) => { + await new Promise(resolve => setTimeout(resolve, 10)); + res.success({ async: true }); + }); + + const result = await Parse.Cloud.run('asyncExpressStyle', {}); + expect(result.async).toBe(true); + }); + + it('should access request parameters in express-style functions', async () => { + Parse.Cloud.define('expressWithParams', (req, res) => { + const { name } = req.params; + res.success({ greeting: `Hello, ${name}!` }); + }); + + const result = await Parse.Cloud.run('expressWithParams', { name: 'World' }); + expect(result.greeting).toEqual('Hello, World!'); + }); + + it('should access user in express-style functions', async () => { + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'testpass'); + await user.signUp(); + + Parse.Cloud.define('expressWithUser', (req, res) => { + if (req.user) { + res.success({ username: req.user.get('username') }); + } else { + res.status(401).error('Not authenticated'); + } + }); + + const result = await Parse.Cloud.run('expressWithUser', {}); + expect(result.username).toEqual('testuser'); + + await Parse.User.logOut(); + }); + }); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 9720e4679c..9831a04f1f 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -103,20 +103,30 @@ export class FunctionsRouter extends PromiseRouter { }); } - static createResponseObject(resolve, reject) { - return { + static createResponseObject(resolve, reject, statusCode = null) { + let httpStatusCode = statusCode; + const responseObject = { success: function (result) { - resolve({ + const response = { response: { result: Parse._encode(result), }, - }); + }; + if (httpStatusCode !== null) { + response.status = httpStatusCode; + } + resolve(response); }, error: function (message) { const error = triggers.resolveError(message); reject(error); }, + status: function (code) { + httpStatusCode = code; + return responseObject; + }, }; + return responseObject; } static handleCloudFunction(req) { const functionName = req.params.functionName; @@ -143,7 +153,7 @@ export class FunctionsRouter extends PromiseRouter { return new Promise(function (resolve, reject) { const userString = req.auth && req.auth.user ? req.auth.user.id : undefined; - const { success, error } = FunctionsRouter.createResponseObject( + const responseObject = FunctionsRouter.createResponseObject( result => { try { if (req.config.logLevels.cloudFunctionSuccess !== 'silent') { @@ -184,14 +194,27 @@ export class FunctionsRouter extends PromiseRouter { } } ); + const { success, error } = responseObject; + return Promise.resolve() .then(() => { return triggers.maybeRunValidator(request, functionName, req.auth); }) .then(() => { - return theFunction(request); + // Check if function expects 2 parameters (req, res) - Express style + if (theFunction.length >= 2) { + return theFunction(request, responseObject); + } else { + // Traditional style - single parameter + return theFunction(request); + } }) - .then(success, error); + .then(result => { + // If result is returned (not using response object), use traditional success + if (result !== undefined) { + success(result); + } + }, error); }); } } From 1c39887c957da782e45f1e970e0c9c0656529df5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:28:49 +0000 Subject: [PATCH 03/11] Add JSDoc documentation for Express-style cloud functions Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/cloud-code/Parse.Cloud.js | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 3fc38c3aad..7419a49853 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -107,22 +107,45 @@ var ParseCloud = {}; * * **Available in Cloud Code only.** * + * **Traditional Style:** * ``` * Parse.Cloud.define('functionName', (request) => { * // code here + * return result; * }, (request) => { * // validation code here * }); * * Parse.Cloud.define('functionName', (request) => { * // code here + * return result; * }, { ...validationObject }); * ``` * + * **Express Style with Custom HTTP Status Codes:** + * ``` + * Parse.Cloud.define('functionName', (request, response) => { + * // Set custom HTTP status code and send response + * response.status(201).success({ message: 'Created' }); + * }); + * + * Parse.Cloud.define('unauthorizedFunction', (request, response) => { + * if (!request.user) { + * response.status(401).success({ error: 'Unauthorized' }); + * } else { + * response.success({ data: 'OK' }); + * } + * }); + * + * Parse.Cloud.define('errorFunction', (request, response) => { + * response.error('Something went wrong'); + * }); + * ``` + * * @static * @memberof Parse.Cloud * @param {String} name The name of the Cloud Function - * @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. + * @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or two parameters (request, response) for Express-style functions where response is a {@link Parse.Cloud.FunctionResponse}. * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.define = function (functionName, handler, validationHandler) { @@ -788,9 +811,21 @@ module.exports = ParseCloud; * @property {Boolean} master If true, means the master key was used. * @property {Parse.User} user If set, the user that made the request. * @property {Object} params The params passed to the cloud function. + * @property {String} ip The IP address of the client making the request. + * @property {Object} headers The original HTTP headers for the request. + * @property {Object} log The current logger inside Parse Server. + * @property {String} functionName The name of the cloud function. + * @property {Object} context The context of the cloud function call. * @property {Object} config The Parse Server config. */ +/** + * @interface Parse.Cloud.FunctionResponse + * @property {function} success Call this function to return a successful response with an optional result. Usage: `response.success(result)` + * @property {function} error Call this function to return an error response with an error message. Usage: `response.error(message)` + * @property {function} status Call this function to set a custom HTTP status code for the response. Returns the response object for chaining. Usage: `response.status(code).success(result)` or `response.status(code).error(message)` + */ + /** * @interface Parse.Cloud.JobRequest * @property {Object} params The params passed to the background job. From 8f814150a26ee92fa6b7ac83d4d028dc6c7021db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:30:31 +0000 Subject: [PATCH 04/11] Fix semantic usage of res.error() with error status codes Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 62 ++++++++++++++++++----------------- src/cloud-code/Parse.Cloud.js | 2 +- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index efc6cf0b6c..3325faf1ac 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -4829,44 +4829,46 @@ describe('beforePasswordResetRequest hook', () => { expect(response.data.result.created).toBe(true); }); - it('should support 401 unauthorized status code', async () => { + it('should support 401 unauthorized status code with error', async () => { Parse.Cloud.define('unauthorizedFunction', (req, res) => { - res.status(401).success({ error: 'Unauthorized access' }); - }); - - const response = await request({ - method: 'POST', - url: 'http://localhost:8378/1/functions/unauthorizedFunction', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - body: {}, + if (!req.user) { + res.status(401).error('Unauthorized access'); + } else { + res.success({ message: 'Authorized' }); + } }); - expect(response.status).toBe(401); - expect(response.data.result.error).toBe('Unauthorized access'); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/unauthorizedFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }) + ).toBeRejected(); }); - it('should support 404 not found status code', async () => { + it('should support 404 not found status code with error', async () => { Parse.Cloud.define('notFoundFunction', (req, res) => { - res.status(404).success({ error: 'Resource not found' }); + res.status(404).error('Resource not found'); }); - const response = await request({ - method: 'POST', - url: 'http://localhost:8378/1/functions/notFoundFunction', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - body: {}, - }); - - expect(response.status).toBe(404); - expect(response.data.result.error).toBe('Resource not found'); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/notFoundFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }) + ).toBeRejected(); }); it('should default to 200 status code when not specified', async () => { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 7419a49853..a110815577 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -131,7 +131,7 @@ var ParseCloud = {}; * * Parse.Cloud.define('unauthorizedFunction', (request, response) => { * if (!request.user) { - * response.status(401).success({ error: 'Unauthorized' }); + * response.status(401).error('Unauthorized'); * } else { * response.success({ data: 'OK' }); * } From dd8ae640a6387a079df08bda9fa137dec9ce97e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:33:32 +0000 Subject: [PATCH 05/11] Preserve custom status codes when using res.error() Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/Routers/FunctionsRouter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 9831a04f1f..c229ae1b5a 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -119,6 +119,10 @@ export class FunctionsRouter extends PromiseRouter { }, error: function (message) { const error = triggers.resolveError(message); + // If a custom status code was set, attach it to the error + if (httpStatusCode !== null) { + error.status = httpStatusCode; + } reject(error); }, status: function (code) { From 9840547f224045d9b08b63ec206e7fbac95f0a38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:35:22 +0000 Subject: [PATCH 06/11] Add double-response protection and fix code formatting Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/Routers/FunctionsRouter.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index c229ae1b5a..16bbf55f8f 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -105,8 +105,13 @@ export class FunctionsRouter extends PromiseRouter { static createResponseObject(resolve, reject, statusCode = null) { let httpStatusCode = statusCode; + let responseSent = false; const responseObject = { success: function (result) { + if (responseSent) { + throw new Error('Response already sent'); + } + responseSent = true; const response = { response: { result: Parse._encode(result), @@ -118,6 +123,10 @@ export class FunctionsRouter extends PromiseRouter { resolve(response); }, error: function (message) { + if (responseSent) { + throw new Error('Response already sent'); + } + responseSent = true; const error = triggers.resolveError(message); // If a custom status code was set, attach it to the error if (httpStatusCode !== null) { @@ -129,6 +138,7 @@ export class FunctionsRouter extends PromiseRouter { httpStatusCode = code; return responseObject; }, + _isResponseSent: () => responseSent, }; return responseObject; } @@ -199,7 +209,7 @@ export class FunctionsRouter extends PromiseRouter { } ); const { success, error } = responseObject; - + return Promise.resolve() .then(() => { return triggers.maybeRunValidator(request, functionName, req.auth); @@ -214,8 +224,8 @@ export class FunctionsRouter extends PromiseRouter { } }) .then(result => { - // If result is returned (not using response object), use traditional success - if (result !== undefined) { + // If result is returned and response wasn't already sent via res.success/res.error + if (result !== undefined && !responseObject._isResponseSent()) { success(result); } }, error); From e6f4745c5f04014b8183a9e1a62273a6626165a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:36:45 +0000 Subject: [PATCH 07/11] Fix grammatical and whitespace formatting issues Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 2 +- src/Routers/FunctionsRouter.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 3325faf1ac..536958fbaf 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -4943,7 +4943,7 @@ describe('beforePasswordResetRequest hook', () => { const result = await Parse.Cloud.run('expressWithUser', {}); expect(result.username).toEqual('testuser'); - + await Parse.User.logOut(); }); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 16bbf55f8f..67e4ea1a51 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -224,7 +224,7 @@ export class FunctionsRouter extends PromiseRouter { } }) .then(result => { - // If result is returned and response wasn't already sent via res.success/res.error + // If result is returned and response hasn't already been sent via res.success/res.error if (result !== undefined && !responseObject._isResponseSent()) { success(result); } From 0ca87109ea5336e0b7c618e5e2e6c3d157d610d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:37:47 +0000 Subject: [PATCH 08/11] Improve error messages for double-response protection Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/Routers/FunctionsRouter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 67e4ea1a51..52e7f20435 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -109,7 +109,7 @@ export class FunctionsRouter extends PromiseRouter { const responseObject = { success: function (result) { if (responseSent) { - throw new Error('Response already sent'); + 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.'); } responseSent = true; const response = { @@ -124,7 +124,7 @@ export class FunctionsRouter extends PromiseRouter { }, error: function (message) { if (responseSent) { - throw new Error('Response already sent'); + 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.'); } responseSent = true; const error = triggers.resolveError(message); From eeff6f9dc48a67e2fe0fae2799cb34d0953c5038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:57:07 +0000 Subject: [PATCH 09/11] Fix: Always call success() for traditional functions, even when returning undefined Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/Routers/FunctionsRouter.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 52e7f20435..7cfe0cb020 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -224,8 +224,18 @@ export class FunctionsRouter extends PromiseRouter { } }) .then(result => { - // If result is returned and response hasn't already been sent via res.success/res.error - if (result !== undefined && !responseObject._isResponseSent()) { + // For Express-style functions, only send response if not already sent + if (theFunction.length >= 2) { + if (!responseObject._isResponseSent()) { + // If Express-style function returns a value without calling res.success/error + if (result !== undefined) { + success(result); + } + // If no response sent and no value returned, this is an error in user code + // but we don't handle it here to maintain backward compatibility + } + } else { + // For traditional functions, always call success with the result (even if undefined) success(result); } }, error); From 5fc7b580ab983ebed72072373a63f70fb4de4b05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Dec 2025 01:13:58 +0000 Subject: [PATCH 10/11] feat: Add custom header support to Express-style cloud functions via res.header() Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 68 ++++++++++++++++++++++++++++++++++ src/Routers/FunctionsRouter.js | 8 ++++ src/cloud-code/Parse.Cloud.js | 5 +++ 3 files changed, 81 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 536958fbaf..f1ce71c4b8 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -4946,5 +4946,73 @@ describe('beforePasswordResetRequest hook', () => { await Parse.User.logOut(); }); + + it('should support setting custom headers with res.header()', async () => { + Parse.Cloud.define('customHeaderFunction', (req, res) => { + res.header('X-Custom-Header', 'custom-value').success({ message: 'OK' }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/customHeaderFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(200); + expect(response.headers['x-custom-header']).toBe('custom-value'); + expect(response.data.result.message).toBe('OK'); + }); + + it('should support setting multiple custom headers', async () => { + Parse.Cloud.define('multipleHeadersFunction', (req, res) => { + res.header('X-Header-One', 'value1') + .header('X-Header-Two', 'value2') + .success({ message: 'Multiple headers' }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/multipleHeadersFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(200); + expect(response.headers['x-header-one']).toBe('value1'); + expect(response.headers['x-header-two']).toBe('value2'); + expect(response.data.result.message).toBe('Multiple headers'); + }); + + it('should support combining status code and custom headers', async () => { + Parse.Cloud.define('statusAndHeaderFunction', (req, res) => { + res.status(201) + .header('X-Resource-Id', '12345') + .success({ created: true }); + }); + + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/functions/statusAndHeaderFunction', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + body: {}, + }); + + expect(response.status).toBe(201); + expect(response.headers['x-resource-id']).toBe('12345'); + expect(response.data.result.created).toBe(true); + }); }); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 7cfe0cb020..f6f4072be6 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -105,6 +105,7 @@ export class FunctionsRouter extends PromiseRouter { static createResponseObject(resolve, reject, statusCode = null) { let httpStatusCode = statusCode; + let customHeaders = {}; let responseSent = false; const responseObject = { success: function (result) { @@ -120,6 +121,9 @@ export class FunctionsRouter extends PromiseRouter { if (httpStatusCode !== null) { response.status = httpStatusCode; } + if (Object.keys(customHeaders).length > 0) { + response.headers = customHeaders; + } resolve(response); }, error: function (message) { @@ -138,6 +142,10 @@ export class FunctionsRouter extends PromiseRouter { httpStatusCode = code; return responseObject; }, + header: function (key, value) { + customHeaders[key] = value; + return responseObject; + }, _isResponseSent: () => responseSent, }; return responseObject; diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index a110815577..5c1a2b156c 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -137,6 +137,10 @@ var ParseCloud = {}; * } * }); * + * Parse.Cloud.define('withCustomHeaders', (request, response) => { + * response.header('X-Custom-Header', 'value').success({ data: 'OK' }); + * }); + * * Parse.Cloud.define('errorFunction', (request, response) => { * response.error('Something went wrong'); * }); @@ -824,6 +828,7 @@ module.exports = ParseCloud; * @property {function} success Call this function to return a successful response with an optional result. Usage: `response.success(result)` * @property {function} error Call this function to return an error response with an error message. Usage: `response.error(message)` * @property {function} status Call this function to set a custom HTTP status code for the response. Returns the response object for chaining. Usage: `response.status(code).success(result)` or `response.status(code).error(message)` + * @property {function} header Call this function to set a custom HTTP header for the response. Returns the response object for chaining. Usage: `response.header('X-Custom-Header', 'value').success(result)` */ /** From 53d6dc3dc5ec4629a4904242475deca12ed238f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:52:54 +0000 Subject: [PATCH 11/11] fix: Fix ESLint errors - use const for customHeaders and correct indentation Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 8 ++++---- src/Routers/FunctionsRouter.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index f1ce71c4b8..685210b4e1 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -4971,8 +4971,8 @@ describe('beforePasswordResetRequest hook', () => { it('should support setting multiple custom headers', async () => { Parse.Cloud.define('multipleHeadersFunction', (req, res) => { res.header('X-Header-One', 'value1') - .header('X-Header-Two', 'value2') - .success({ message: 'Multiple headers' }); + .header('X-Header-Two', 'value2') + .success({ message: 'Multiple headers' }); }); const response = await request({ @@ -4995,8 +4995,8 @@ describe('beforePasswordResetRequest hook', () => { it('should support combining status code and custom headers', async () => { Parse.Cloud.define('statusAndHeaderFunction', (req, res) => { res.status(201) - .header('X-Resource-Id', '12345') - .success({ created: true }); + .header('X-Resource-Id', '12345') + .success({ created: true }); }); const response = await request({ diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index f6f4072be6..93183f6f76 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -105,7 +105,7 @@ export class FunctionsRouter extends PromiseRouter { static createResponseObject(resolve, reject, statusCode = null) { let httpStatusCode = statusCode; - let customHeaders = {}; + const customHeaders = {}; let responseSent = false; const responseObject = { success: function (result) {