feat: Upgrade to express 5.0.1 (#9530)

BREAKING CHANGE: This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info.
This commit is contained in:
Colin Ulin
2025-03-03 16:11:42 -05:00
committed by GitHub
parent cc8dad8fa1
commit e0480dfa8d
26 changed files with 995 additions and 401 deletions

1261
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,10 @@
"@parse/fs-files-adapter": "3.0.0", "@parse/fs-files-adapter": "3.0.0",
"@parse/push-adapter": "6.10.0", "@parse/push-adapter": "6.10.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.20.3",
"commander": "13.0.0", "commander": "13.0.0",
"cors": "2.8.5", "cors": "2.8.5",
"deepcopy": "2.1.0", "deepcopy": "2.1.0",
"express": "4.21.2", "express": "5.0.1",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"follow-redirects": "1.15.9", "follow-redirects": "1.15.9",
"graphql": "16.9.0", "graphql": "16.9.0",
@@ -58,6 +57,7 @@
"punycode": "2.3.1", "punycode": "2.3.1",
"rate-limit-redis": "4.2.0", "rate-limit-redis": "4.2.0",
"redis": "4.7.0", "redis": "4.7.0",
"router": "2.0.0",
"semver": "7.7.1", "semver": "7.7.1",
"subscriptions-transport-ws": "0.11.0", "subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0", "tv4": "1.3.0",

View File

@@ -2,7 +2,6 @@
const httpRequest = require('../lib/request'), const httpRequest = require('../lib/request'),
HTTPResponse = require('../lib/request').HTTPResponse, HTTPResponse = require('../lib/request').HTTPResponse,
bodyParser = require('body-parser'),
express = require('express'); express = require('express');
const port = 13371; const port = 13371;
@@ -10,7 +9,7 @@ const httpRequestServer = `http://localhost:${port}`;
function startServer(done) { function startServer(done) {
const app = express(); const app = express();
app.use(bodyParser.json({ type: '*/*' })); app.use(express.json({ type: '*/*' }));
app.get('/hello', function (req, res) { app.get('/hello', function (req, res) {
res.json({ response: 'OK' }); res.json({ response: 'OK' });
}); });

View File

@@ -4,7 +4,6 @@ const request = require('../lib/request');
const triggers = require('../lib/triggers'); const triggers = require('../lib/triggers');
const HooksController = require('../lib/Controllers/HooksController').default; const HooksController = require('../lib/Controllers/HooksController').default;
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser');
const auth = require('../lib/Auth'); const auth = require('../lib/Auth');
const Config = require('../lib/Config'); const Config = require('../lib/Config');
@@ -17,7 +16,7 @@ describe('Hooks', () => {
beforeEach(done => { beforeEach(done => {
if (!app) { if (!app) {
app = express(); app = express();
app.use(bodyParser.json({ type: '*/*' })); app.use(express.json({ type: '*/*' }));
server = app.listen(port, undefined, done); server = app.listen(port, undefined, done);
} else { } else {
done(); done();

View File

@@ -250,11 +250,10 @@ describe('Vulnerabilities', () => {
it_id('e8b5f1e1-8326-4c70-b5f4-1e8678dfff8d')(it)('denies creating a hook with polluted data', async () => { it_id('e8b5f1e1-8326-4c70-b5f4-1e8678dfff8d')(it)('denies creating a hook with polluted data', async () => {
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser');
const port = 34567; const port = 34567;
const hookServerURL = 'http://localhost:' + port; const hookServerURL = 'http://localhost:' + port;
const app = express(); const app = express();
app.use(bodyParser.json({ type: '*/*' })); app.use(express.json({ type: '*/*' }));
const server = await new Promise(resolve => { const server = await new Promise(resolve => {
const res = app.listen(port, undefined, () => resolve(res)); const res = app.listen(port, undefined, () => resolve(res));
}); });

View File

@@ -5,7 +5,7 @@ export class AnalyticsController extends AdaptableController {
appOpened(req) { appOpened(req) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
return this.adapter.appOpened(req.body, req); return this.adapter.appOpened(req.body || {}, req);
}) })
.then(response => { .then(response => {
return { response: response || {} }; return { response: response || {} };
@@ -18,7 +18,7 @@ export class AnalyticsController extends AdaptableController {
trackEvent(req) { trackEvent(req) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
return this.adapter.trackEvent(req.params.eventName, req.body, req); return this.adapter.trackEvent(req.params.eventName, req.body || {}, req);
}) })
.then(response => { .then(response => {
return { response: response || {} }; return { response: response || {} };

View File

@@ -1,7 +1,6 @@
// ParseServer - open-source compatible API Server for Parse apps // ParseServer - open-source compatible API Server for Parse apps
var batch = require('./batch'), var batch = require('./batch'),
bodyParser = require('body-parser'),
express = require('express'), express = require('express'),
middlewares = require('./middlewares'), middlewares = require('./middlewares'),
Parse = require('parse/node').Parse, Parse = require('parse/node').Parse,
@@ -253,6 +252,7 @@ class ParseServer {
var api = express(); var api = express();
//api.use("/apps", express.static(__dirname + "/public")); //api.use("/apps", express.static(__dirname + "/public"));
api.use(middlewares.allowCrossDomain(appId)); api.use(middlewares.allowCrossDomain(appId));
api.use(middlewares.allowDoubleForwardSlash);
// File handling needs to be before default middlewares are applied // File handling needs to be before default middlewares are applied
api.use( api.use(
'/', '/',
@@ -273,15 +273,16 @@ class ParseServer {
api.use( api.use(
'/', '/',
bodyParser.urlencoded({ extended: false }), express.urlencoded({ extended: false }),
pages.enableRouter pages.enableRouter
? new PagesRouter(pages).expressRouter() ? new PagesRouter(pages).expressRouter()
: new PublicAPIRouter().expressRouter() : new PublicAPIRouter().expressRouter()
); );
api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize })); api.use(express.json({ type: '*/*', limit: maxUploadSize }));
api.use(middlewares.allowMethodOverride); api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders); api.use(middlewares.handleParseHeaders);
api.set('query parser', 'extended');
const routes = Array.isArray(rateLimit) ? rateLimit : [rateLimit]; const routes = Array.isArray(rateLimit) ? rateLimit : [rateLimit];
for (const route of routes) { for (const route of routes) {
middlewares.addRateLimit(route, options); middlewares.addRateLimit(route, options);

View File

@@ -9,7 +9,7 @@ import Parse from 'parse/node';
import express from 'express'; import express from 'express';
import log from './logger'; import log from './logger';
import { inspect } from 'util'; import { inspect } from 'util';
const Layer = require('express/lib/router/layer'); const Layer = require('router/lib/layer');
function validateParameter(key, value) { function validateParameter(key, value) {
if (key == 'className') { if (key == 'className') {

View File

@@ -6,7 +6,7 @@ import UsersRouter from './UsersRouter';
export class AggregateRouter extends ClassesRouter { export class AggregateRouter extends ClassesRouter {
handleFind(req) { handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = {}; const options = {};
if (body.distinct) { if (body.distinct) {
options.distinct = String(body.distinct); options.distinct = String(body.distinct);

View File

@@ -8,7 +8,7 @@ export class AudiencesRouter extends ClassesRouter {
} }
handleFind(req) { handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
return rest return rest

View File

@@ -19,7 +19,7 @@ export class ClassesRouter extends PromiseRouter {
} }
handleFind(req) { handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
if (req.config.maxLimit && body.limit > req.config.maxLimit) { if (req.config.maxLimit && body.limit > req.config.maxLimit) {
// Silently replace the limit on the query with the max configured // Silently replace the limit on the query with the max configured
@@ -48,7 +48,7 @@ export class ClassesRouter extends PromiseRouter {
// Returns a promise for a {response} object. // Returns a promise for a {response} object.
handleGet(req) { handleGet(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = {}; const options = {};
for (const key of Object.keys(body)) { for (const key of Object.keys(body)) {
@@ -117,7 +117,7 @@ export class ClassesRouter extends PromiseRouter {
req.config, req.config,
req.auth, req.auth,
this.className(req), this.className(req),
req.body, req.body || {},
req.info.clientSDK, req.info.clientSDK,
req.info.context req.info.context
); );
@@ -130,7 +130,7 @@ export class ClassesRouter extends PromiseRouter {
req.auth, req.auth,
this.className(req), this.className(req),
where, where,
req.body, req.body || {},
req.info.clientSDK, req.info.clientSDK,
req.info.context req.info.context
); );

View File

@@ -77,7 +77,7 @@ export class CloudCodeRouter extends PromiseRouter {
} }
static createJob(req) { static createJob(req) {
const { job_schedule } = req.body; const { job_schedule } = req.body || {};
validateJobSchedule(req.config, job_schedule); validateJobSchedule(req.config, job_schedule);
return rest.create( return rest.create(
req.config, req.config,
@@ -91,7 +91,7 @@ export class CloudCodeRouter extends PromiseRouter {
static editJob(req) { static editJob(req) {
const { objectId } = req.params; const { objectId } = req.params;
const { job_schedule } = req.body; const { job_schedule } = req.body || {};
validateJobSchedule(req.config, job_schedule); validateJobSchedule(req.config, job_schedule);
return rest return rest
.update( .update(

View File

@@ -1,5 +1,4 @@
import express from 'express'; import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares'; import * as Middlewares from '../middlewares';
import Parse from 'parse/node'; import Parse from 'parse/node';
import Config from '../Config'; import Config from '../Config';
@@ -45,7 +44,7 @@ export class FilesRouter {
router.post( router.post(
'/files/:filename', '/files/:filename',
BodyParser.raw({ express.raw({
type: () => { type: () => {
return true; return true;
}, },

View File

@@ -58,7 +58,7 @@ export class FunctionsRouter extends PromiseRouter {
} }
static handleCloudJob(req) { static handleCloudJob(req) {
const jobName = req.params.jobName || req.body.jobName; const jobName = req.params.jobName || req.body?.jobName;
const applicationId = req.config.applicationId; const applicationId = req.config.applicationId;
const jobHandler = jobStatusHandler(req.config); const jobHandler = jobStatusHandler(req.config);
const jobFunction = triggers.getJob(jobName, applicationId); const jobFunction = triggers.getJob(jobName, applicationId);

View File

@@ -46,8 +46,8 @@ export class GlobalConfigRouter extends PromiseRouter {
"read-only masterKey isn't allowed to update the config." "read-only masterKey isn't allowed to update the config."
); );
} }
const params = req.body.params; const params = req.body.params || {};
const masterKeyOnly = req.body.masterKeyOnly || {}; const masterKeyOnly = req.body?.masterKeyOnly || {};
// Transform in dot notation to make sure it works // Transform in dot notation to make sure it works
const update = Object.keys(params).reduce((acc, key) => { const update = Object.keys(params).reduce((acc, key) => {
acc[`params.${key}`] = params[key]; acc[`params.${key}`] = params[key];

View File

@@ -19,7 +19,7 @@ export class GraphQLRouter extends PromiseRouter {
"read-only masterKey isn't allowed to update the GraphQL config." "read-only masterKey isn't allowed to update the GraphQL config."
); );
} }
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body.params); const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});
return { return {
response: data, response: data,
}; };

View File

@@ -12,7 +12,7 @@ export class HooksRouter extends PromiseRouter {
} }
handlePost(req) { handlePost(req) {
return this.createHook(req.body, req.config); return this.createHook(req.body || {}, req.config);
} }
handleGetFunctions(req) { handleGetFunctions(req) {
@@ -66,11 +66,11 @@ export class HooksRouter extends PromiseRouter {
handleUpdate(req) { handleUpdate(req) {
var hook; var hook;
if (req.params.functionName && req.body.url) { if (req.params.functionName && req.body?.url) {
hook = {}; hook = {};
hook.functionName = req.params.functionName; hook.functionName = req.params.functionName;
hook.url = req.body.url; hook.url = req.body.url;
} else if (req.params.className && req.params.triggerName && req.body.url) { } else if (req.params.className && req.params.triggerName && req.body?.url) {
hook = {}; hook = {};
hook.className = req.params.className; hook.className = req.params.className;
hook.triggerName = req.params.triggerName; hook.triggerName = req.params.triggerName;
@@ -82,7 +82,7 @@ export class HooksRouter extends PromiseRouter {
} }
handlePut(req) { handlePut(req) {
var body = req.body; var body = req.body || {};
if (body.__op == 'Delete') { if (body.__op == 'Delete') {
return this.handleDelete(req); return this.handleDelete(req);
} else { } else {

View File

@@ -68,8 +68,8 @@ function getFileForProductIdentifier(productIdentifier, req) {
export class IAPValidationRouter extends PromiseRouter { export class IAPValidationRouter extends PromiseRouter {
handleRequest(req) { handleRequest(req) {
let receipt = req.body.receipt; let receipt = req.body?.receipt;
const productIdentifier = req.body.productIdentifier; const productIdentifier = req.body?.productIdentifier;
if (!receipt || !productIdentifier) { if (!receipt || !productIdentifier) {
// TODO: Error, malformed request // TODO: Error, malformed request
@@ -84,7 +84,7 @@ export class IAPValidationRouter extends PromiseRouter {
} }
} }
if (process.env.TESTING == '1' && req.body.bypassAppStoreValidation) { if (process.env.TESTING == '1' && req.body?.bypassAppStoreValidation) {
return getFileForProductIdentifier(productIdentifier, req); return getFileForProductIdentifier(productIdentifier, req);
} }

View File

@@ -10,7 +10,7 @@ export class InstallationsRouter extends ClassesRouter {
} }
handleFind(req) { handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
return rest return rest
.find( .find(

View File

@@ -107,8 +107,8 @@ export class PagesRouter extends PromiseRouter {
resendVerificationEmail(req) { resendVerificationEmail(req) {
const config = req.config; const config = req.config;
const username = req.body.username; const username = req.body?.username;
const token = req.body.token; const token = req.body?.token;
if (!config) { if (!config) {
this.invalidRequest(); this.invalidRequest();
@@ -178,7 +178,7 @@ export class PagesRouter extends PromiseRouter {
this.invalidRequest(); this.invalidRequest();
} }
const { new_password, token: rawToken } = req.body; const { new_password, token: rawToken } = req.body || {};
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
if ((!token || !new_password) && req.xhr === false) { if ((!token || !new_password) && req.xhr === false) {
@@ -320,7 +320,7 @@ export class PagesRouter extends PromiseRouter {
*/ */
staticRoute(req) { staticRoute(req) {
// Get requested path // Get requested path
const relativePath = req.params[0]; const relativePath = req.params['resource'][0];
// Resolve requested path to absolute path // Resolve requested path to absolute path
const absolutePath = path.resolve(this.pagesPath, relativePath); const absolutePath = path.resolve(this.pagesPath, relativePath);
@@ -716,7 +716,7 @@ export class PagesRouter extends PromiseRouter {
mountStaticRoute() { mountStaticRoute() {
this.route( this.route(
'GET', 'GET',
`/${this.pagesEndpoint}/(*)?`, `/${this.pagesEndpoint}/*resource`,
req => { req => {
this.setConfig(req, true); this.setConfig(req, true);
}, },

View File

@@ -52,7 +52,7 @@ export class PublicAPIRouter extends PromiseRouter {
} }
resendVerificationEmail(req) { resendVerificationEmail(req) {
const username = req.body.username; const username = req.body?.username;
const appId = req.params.appId; const appId = req.params.appId;
const config = Config.get(appId); const config = Config.get(appId);
@@ -162,7 +162,7 @@ export class PublicAPIRouter extends PromiseRouter {
return this.missingPublicServerURL(); return this.missingPublicServerURL();
} }
const { new_password, token: rawToken } = req.body; const { new_password, token: rawToken } = req.body || {};
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
if ((!token || !new_password) && req.xhr === false) { if ((!token || !new_password) && req.xhr === false) {

View File

@@ -26,7 +26,7 @@ export class PushRouter extends PromiseRouter {
}); });
let pushStatusId; let pushStatusId;
pushController pushController
.sendPush(req.body, where, req.config, req.auth, objectId => { .sendPush(req.body || {}, where, req.config, req.auth, objectId => {
pushStatusId = objectId; pushStatusId = objectId;
resolve({ resolve({
headers: { headers: {

View File

@@ -77,18 +77,18 @@ async function createSchema(req) {
"read-only masterKey isn't allowed to create a schema." "read-only masterKey isn't allowed to create a schema."
); );
} }
if (req.params.className && req.body.className) { if (req.params.className && req.body?.className) {
if (req.params.className != req.body.className) { if (req.params.className != req.body.className) {
return classNameMismatchResponse(req.body.className, req.params.className); return classNameMismatchResponse(req.body.className, req.params.className);
} }
} }
const className = req.params.className || req.body.className; const className = req.params.className || req.body?.className;
if (!className) { if (!className) {
throw new Parse.Error(135, `POST ${req.path} needs a class name.`); throw new Parse.Error(135, `POST ${req.path} needs a class name.`);
} }
return await internalCreateSchema(className, req.body, req.config); return await internalCreateSchema(className, req.body || {}, req.config);
} }
function modifySchema(req) { function modifySchema(req) {
@@ -99,12 +99,12 @@ function modifySchema(req) {
"read-only masterKey isn't allowed to update a schema." "read-only masterKey isn't allowed to update a schema."
); );
} }
if (req.body.className && req.body.className != req.params.className) { if (req.body?.className && req.body.className != req.params.className) {
return classNameMismatchResponse(req.body.className, req.params.className); return classNameMismatchResponse(req.body.className, req.params.className);
} }
const className = req.params.className; const className = req.params.className;
return internalUpdateSchema(className, req.body, req.config); return internalUpdateSchema(className, req.body || {}, req.config);
} }
const deleteSchema = req => { const deleteSchema = req => {

View File

@@ -68,7 +68,7 @@ export class UsersRouter extends ClassesRouter {
_authenticateUserFromRequest(req) { _authenticateUserFromRequest(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Use query parameters instead if provided in url // Use query parameters instead if provided in url
let payload = req.body; let payload = req.body || {};
if ( if (
(!payload.username && req.query && req.query.username) || (!payload.username && req.query && req.query.username) ||
(!payload.email && req.query && req.query.email) (!payload.email && req.query && req.query.email)
@@ -219,7 +219,7 @@ export class UsersRouter extends ClassesRouter {
req.auth, req.auth,
'_User', '_User',
{ objectId: user.objectId }, { objectId: user.objectId },
req.body, req.body || {},
user, user,
req.info.clientSDK, req.info.clientSDK,
req.info.context req.info.context
@@ -336,7 +336,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required'); throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required');
} }
const userId = req.body.userId || req.query.userId; const userId = req.body?.userId || req.query.userId;
if (!userId) { if (!userId) {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_VALUE, Parse.Error.INVALID_VALUE,
@@ -438,8 +438,9 @@ export class UsersRouter extends ClassesRouter {
async handleResetRequest(req) { async handleResetRequest(req) {
this._throwOnBadEmailConfig(req); this._throwOnBadEmailConfig(req);
let email = req.body.email; let email = req.body?.email;
const token = req.body.token; const token = req.body?.token;
if (!email && !token) { if (!email && !token) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
} }
@@ -480,7 +481,7 @@ export class UsersRouter extends ClassesRouter {
async handleVerificationEmailRequest(req) { async handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req); this._throwOnBadEmailConfig(req);
const { email } = req.body; const { email } = req.body || {};
if (!email) { if (!email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
} }
@@ -513,7 +514,7 @@ export class UsersRouter extends ClassesRouter {
} }
async handleChallenge(req) { async handleChallenge(req) {
const { username, email, password, authData, challengeData } = req.body; const { username, email, password, authData, challengeData } = req.body || {};
// if username or email provided with password try to authenticate the user by username // if username or email provided with password try to authenticate the user by username
let user; let user;

View File

@@ -64,7 +64,7 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
// Returns a promise for a {response} object. // Returns a promise for a {response} object.
// TODO: pass along auth correctly // TODO: pass along auth correctly
function handleBatch(router, req) { function handleBatch(router, req) {
if (!Array.isArray(req.body.requests)) { if (!Array.isArray(req.body?.requests)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array'); throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array');
} }
@@ -85,12 +85,12 @@ function handleBatch(router, req) {
const batch = transactionRetries => { const batch = transactionRetries => {
let initialPromise = Promise.resolve(); let initialPromise = Promise.resolve();
if (req.body.transaction === true) { if (req.body?.transaction === true) {
initialPromise = req.config.database.createTransactionalSession(); initialPromise = req.config.database.createTransactionalSession();
} }
return initialPromise.then(() => { return initialPromise.then(() => {
const promises = req.body.requests.map(restRequest => { const promises = req.body?.requests.map(restRequest => {
const routablePath = makeRoutablePath(restRequest.path); const routablePath = makeRoutablePath(restRequest.path);
// Construct a request that we can send to a handler // Construct a request that we can send to a handler
@@ -113,7 +113,7 @@ function handleBatch(router, req) {
return Promise.all(promises) return Promise.all(promises)
.then(results => { .then(results => {
if (req.body.transaction === true) { if (req.body?.transaction === true) {
if (results.find(result => typeof result.error === 'object')) { if (results.find(result => typeof result.error === 'object')) {
return req.config.database.abortTransactionalSession().then(() => { return req.config.database.abortTransactionalSession().then(() => {
return Promise.reject({ response: results }); return Promise.reject({ response: results });

View File

@@ -196,7 +196,7 @@ export async function handleParseHeaders(req, res, next) {
info.clientSDK = ClientSDK.fromString(info.clientVersion); info.clientSDK = ClientSDK.fromString(info.clientVersion);
} }
if (fileViaJSON) { if (fileViaJSON && req.body) {
req.fileData = req.body.fileData; req.fileData = req.body.fileData;
// We need to repopulate req.body with a buffer // We need to repopulate req.body with a buffer
var base64 = req.body.base64; var base64 = req.body.base64;
@@ -450,7 +450,7 @@ export function allowCrossDomain(appId) {
} }
export function allowMethodOverride(req, res, next) { export function allowMethodOverride(req, res, next) {
if (req.method === 'POST' && req.body._method) { if (req.method === 'POST' && req.body?._method) {
req.originalMethod = req.method; req.originalMethod = req.method;
req.method = req.body._method; req.method = req.body._method;
delete req.body._method; delete req.body._method;
@@ -685,3 +685,16 @@ function malformedContext(req, res) {
res.status(400); res.status(400);
res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' }); res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' });
} }
/**
* Express 4 allowed a double forward slash between a route and router. Although
* this should be considered an anti-pattern, we need to support it for backwards
* compatibility.
*
* Technically valid URL with double foroward slash:
* http://localhost:1337/parse//functions/testFunction
*/
export function allowDoubleForwardSlash(req, res, next) {
req.url = req.url.startsWith('//') ? req.url.substring(1) : req.url;
next();
}