Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions 9.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Parse Server 9 Migration Guide <!-- omit in toc -->

This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 9 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).

---
- [Route Path Syntax and Rate Limiting](#route-path-syntax-and-rate-limiting)
---

## Route Path Syntax and Rate Limiting
Parse Server 9 standardizes the route pattern syntax across cloud routes and rate-limiting to use the new **path-to-regexp v8** style. This update introduces validation and a clear deprecation error for the old wildcard route syntax.

### Key Changes
- **Standardization**: All route paths now use the path-to-regexp v8 syntax, which provides better consistency and security.
- **Validation**: Added validation to ensure route paths conform to the new syntax.
- **Deprecation**: Old wildcard route syntax is deprecated and will trigger a clear error message.

### Migration Steps

#### Path Syntax Examples

Update your rate limit configurations to use the new path-to-regexp v8 syntax:

| Old Syntax (deprecated) | New Syntax (v8) |
|------------------------|-----------------|
| `/functions/*` | `/functions/*path` |
| `/classes/*` | `/classes/*path` |
| `/*` | `/*path` |
| `*` | `*path` |

**Before:**
```javascript
rateLimit: {
requestPath: '/functions/*',
requestTimeWindow: 10000,
requestCount: 100
}
```

**After:**
```javascript
rateLimit: {
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 100
}
```

- Review your custom cloud routes and ensure they use the new path-to-regexp v8 syntax.
- Update any rate-limiting configurations to use the new route path format.
- Test your application to ensure all routes work as expected with the new syntax.
- **Consult the [Express 5 Migration Guide](https://expressjs.com/en/guide/migrating-5.html#path-syntax) for more details on the new path syntax.**

### Related Pull Request
- [#9942: feat: Bump path-to-regexp to v8](https://github.com/parse-community/parse-server/pull/9942)
32 changes: 12 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"mustache": "4.2.0",
"otpauth": "9.4.0",
"parse": "7.0.1",
"path-to-regexp": "6.3.0",
"path-to-regexp": "8.3.0",
"pg-monitor": "3.0.0",
"pg-promise": "12.2.0",
"pluralize": "8.0.0",
Expand Down
38 changes: 23 additions & 15 deletions spec/RateLimit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -26,7 +26,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -45,7 +45,7 @@ describe('rate limit', () => {
Parse.Cloud.define('test', () => 'Abc');
await reconfigureServer({
rateLimit: {
requestPath: '*',
requestPath: '/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand Down Expand Up @@ -83,7 +83,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -102,7 +102,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
includeMasterKey: true,
Expand All @@ -122,7 +122,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/classes/*',
requestPath: '/classes/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -141,7 +141,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/classes/*',
requestPath: '/classes/*path',
requestTimeWindow: 10000,
requestCount: 1,
requestMethods: 'POST',
Expand Down Expand Up @@ -240,7 +240,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/classes/Test/*',
requestPath: '/classes/Test/*path',
requestTimeWindow: 10000,
requestCount: 1,
requestMethods: 'DELETE',
Expand Down Expand Up @@ -294,7 +294,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 100,
errorResponseMessage: 'Too many requests',
Expand All @@ -320,7 +320,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -340,7 +340,7 @@ describe('rate limit', () => {
it('can use global zone', async () => {
await reconfigureServer({
rateLimit: {
requestPath: '*',
requestPath: '*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand Down Expand Up @@ -373,7 +373,7 @@ describe('rate limit', () => {
});
fakeRes.json = jasmine.createSpy('json').and.callFake(resolvingPromise);
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
throw 'Should not call next';
throw new Error('Should not call next');
});
await promise;
expect(fakeRes.status).toHaveBeenCalledWith(429);
Expand All @@ -386,7 +386,7 @@ describe('rate limit', () => {
it('can use session zone', async () => {
await reconfigureServer({
rateLimit: {
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand All @@ -407,7 +407,7 @@ describe('rate limit', () => {
it('can use user zone', async () => {
await reconfigureServer({
rateLimit: {
requestPath: '/functions/*',
requestPath: '/functions/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand Down Expand Up @@ -481,6 +481,14 @@ describe('rate limit', () => {
expect(() =>
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestCount: 'abc' }] })
).toThrow('rateLimit.requestPath must be defined');
expect(() =>
validateRateLimit({ rateLimit: [{ requestPath: '/*' }] })
).toThrow(`rateLimit.requestPath "/*" uses deprecated wildcard syntax. ` +
`Please update to path-to-regexp v8 syntax. Examples:\n` +
` Old: "/functions/*" → New: "/functions/*path"\n` +
` Old: "/classes/*" → New: "/classes/*path"\n` +
` Old: "*" → New: "*path"\n` +
`See parameter name on the express migration guide.`);
await expectAsync(
reconfigureServer({
rateLimit: [{ requestTimeWindow: 3, requestCount: 1, path: 'abc', requestPath: 'a' }],
Expand All @@ -494,7 +502,7 @@ describe('rate limit', () => {
await reconfigureServer({
rateLimit: [
{
requestPath: '/classes/*',
requestPath: '/classes/*path',
requestTimeWindow: 10000,
requestCount: 1,
errorResponseMessage: 'Too many requests',
Expand Down
22 changes: 22 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// mount is the URL for the root of the API; includes http, domain, etc.

import { isBoolean, isString } from 'lodash';
import { pathToRegexp } from 'path-to-regexp';
import net from 'net';
import AppCache from './cache';
import DatabaseController from './Controllers/DatabaseController';
Expand Down Expand Up @@ -687,6 +688,27 @@ export class Config {
if (typeof option.requestPath !== 'string') {
throw `rateLimit.requestPath must be a string`;
}

// Validate path-to-regexp v8 syntax
// Check for common old syntax patterns
const oldWildcardPattern = /(?:^|\/)\*(?:\/|$)/; // Matches /* or * at start/end
const nakedWildcard = /^[\s]*\*[\s]*$/; // Matches bare *
if (oldWildcardPattern.test(option.requestPath) || nakedWildcard.test(option.requestPath)) {
throw `rateLimit.requestPath "${option.requestPath}" uses deprecated wildcard syntax. ` +
`Please update to path-to-regexp v8 syntax. Examples:\n` +
` Old: "/functions/*" → New: "/functions/*path"\n` +
` Old: "/classes/*" → New: "/classes/*path"\n` +
` Old: "*" → New: "*path"\n` +
`See parameter name on the express migration guide.`;
}

// Validate that the path is valid path-to-regexp syntax
try {
pathToRegexp(option.requestPath);
} catch (error) {
throw `rateLimit.requestPath "${option.requestPath}" is not valid: ${error.message}`;
}

if (option.requestTimeWindow == null) {
throw `rateLimit.requestTimeWindow must be defined`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ module.exports.RateLimitOptions = {
requestPath: {
env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH',
help:
'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html',
'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following path-to-regexp v8 syntax. Wildcards must be named (e.g., `/*path` instead of `/*`). Examples: `/functions/*path`, `/classes/MyClass/*path`, `/*path`. See: https://github.com/pillarjs/path-to-regexp',
required: true,
},
requestTimeWindow: {
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ export interface ParseServerOptions {
}

export interface RateLimitOptions {
/* The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html */
/* The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings or string patterns following path-to-regexp v8 syntax. Wildcards must be named (e.g., `/*path` instead of `/*`). Examples: `/functions/*path`, `/classes/MyClass/*path`, `/*path`. See: https://github.com/pillarjs/path-to-regexp */
requestPath: string;
/* The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied. */
requestTimeWindow: ?number;
Expand Down
4 changes: 2 additions & 2 deletions src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ const getRoute = parseClass => {
'@Config' : 'config',
}[parseClass] || 'classes';
if (parseClass === '@File') {
return `/${route}/:id?(.*)`;
return `/${route}{/*id}`;
}
if (parseClass === '@Config') {
return `/${route}`;
}
return `/${route}/${parseClass}/:id?(.*)`;
return `/${route}/${parseClass}{/*id}`;
};
/** @namespace
* @name Parse
Expand Down
8 changes: 2 additions & 6 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ const handleRateLimit = async (req, res, next) => {
try {
await Promise.all(
rateLimits.map(async limit => {
const pathExp = new RegExp(limit.path);
const pathExp = limit.path.regexp || limit.path;
if (pathExp.test(req.url)) {
await limit.handler(req, res, err => {
if (err) {
Expand Down Expand Up @@ -560,12 +560,8 @@ export const addRateLimit = (route, config, cloud) => {
},
});
}
let transformPath = route.requestPath.split('/*').join('/(.*)');
if (transformPath === '*') {
transformPath = '(.*)';
}
config.rateLimits.push({
path: pathToRegexp(transformPath),
path: pathToRegexp(route.requestPath),
handler: rateLimit({
windowMs: route.requestTimeWindow,
max: route.requestCount,
Expand Down
Loading