feat: Add Parse.File option maxUploadSize to override the Parse Server option maxUploadSize per file upload (#10093)

This commit is contained in:
Manuel
2026-03-04 19:50:09 +00:00
committed by GitHub
parent 792af37d29
commit 3d8807b4ec
4 changed files with 272 additions and 17 deletions
+7 -7
View File
@@ -38,7 +38,7 @@
"mongodb": "7.1.0",
"mustache": "4.2.0",
"otpauth": "9.4.0",
"parse": "8.4.0",
"parse": "8.5.0",
"path-to-regexp": "8.3.0",
"pg-monitor": "3.1.0",
"pg-promise": "12.6.0",
@@ -18402,9 +18402,9 @@
}
},
"node_modules/parse": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/parse/-/parse-8.4.0.tgz",
"integrity": "sha512-Pfb0Oedh9PHU0ZQ54EupCgh9zZR0OqitiXSHJ6oyGIqCsaOZ+ENAuw6Nr06T0FqzYb/fYkq4cC4hdte2QmjjNA==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz",
"integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "7.28.6",
@@ -35512,9 +35512,9 @@
}
},
"parse": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/parse/-/parse-8.4.0.tgz",
"integrity": "sha512-Pfb0Oedh9PHU0ZQ54EupCgh9zZR0OqitiXSHJ6oyGIqCsaOZ+ENAuw6Nr06T0FqzYb/fYkq4cC4hdte2QmjjNA==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/parse/-/parse-8.5.0.tgz",
"integrity": "sha512-X9gI4Yjbi9LPMPnCtKL4h0Nxe1aSCFMPWcB1zbu11qU/Be3eVSB5I5IMBunTuWlVz6Wchu3dtM5jl/1aBZ9wiQ==",
"requires": {
"@babel/runtime": "7.28.6",
"@babel/runtime-corejs3": "7.29.0",
+1 -1
View File
@@ -48,7 +48,7 @@
"mongodb": "7.1.0",
"mustache": "4.2.0",
"otpauth": "9.4.0",
"parse": "8.4.0",
"parse": "8.5.0",
"path-to-regexp": "8.3.0",
"pg-monitor": "3.1.0",
"pg-promise": "12.6.0",
+204
View File
@@ -2021,6 +2021,210 @@ describe('Parse.File testing', () => {
}
});
describe('maxUploadSize override', () => {
it('allows streaming upload exceeding server limit with maxUploadSize override and master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '1mb',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/override-stream.txt',
body: 'this content is definitely longer than 10 bytes',
});
expect(response.data.name).toContain('override-stream');
expect(response.data.url).toBeDefined();
});
it('allows buffered upload exceeding server limit with maxUploadSize override and master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'X-Parse-File-Max-Upload-Size': '1mb',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/override-buffer.txt',
body: 'this content is definitely longer than 10 bytes',
});
expect(response.data.name).toContain('override-buffer');
expect(response.data.url).toBeDefined();
});
it('rejects maxUploadSize override without master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '1mb',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/no-master.txt',
body: 'this content is longer than 10 bytes',
});
fail('should have thrown');
} catch (response) {
expect(response.status).toBe(403);
}
});
it('rejects invalid maxUploadSize override value', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': 'notasize',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/bad-value.txt',
body: 'some data',
});
fail('should have thrown');
} catch (response) {
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
expect(response.data.error).toContain('Invalid maxUploadSize override');
}
});
it('rejects streaming upload exceeding the overridden maxUploadSize', async () => {
await reconfigureServer({ maxUploadSize: '5b' });
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '10b',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/still-too-big.txt',
body: 'this content is definitely longer than 10 bytes',
});
fail('should have thrown');
} catch (response) {
expect(response.data.code).toBe(Parse.Error.FILE_SAVE_ERROR);
expect(response.data.error).toContain('exceeds');
}
});
it('rejects maxUploadSize override with wrong master key', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'wrong-key',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '1mb',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/wrong-key.txt',
body: 'some data',
});
fail('should have thrown');
} catch (response) {
expect(response.status).toBe(403);
}
});
it('rejects maxUploadSize override with invalid application ID', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'invalid-app-id',
'X-Parse-Master-Key': 'test',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '1mb',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/bad-app.txt',
body: 'some data',
});
fail('should have thrown');
} catch (response) {
expect(response.status).toBe(403);
}
});
it('rejects maxUploadSize override when masterKeyIps blocks the IP', async () => {
await reconfigureServer({ masterKeyIps: ['10.0.0.1'] });
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'X-Parse-Upload-Mode': 'stream',
'X-Parse-File-Max-Upload-Size': '1mb',
};
try {
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/files/blocked-ip.txt',
body: 'some data',
});
fail('should have thrown');
} catch (response) {
expect(response.status).toBe(403);
}
});
});
describe('maxUploadSize override via SDK', () => {
it('saves buffer file with maxUploadSize override and master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const data = Buffer.alloc(100, 'a');
const file = new Parse.File('sdk-buffer-override.txt', data, 'text/plain');
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
expect(result.url()).toBeDefined();
expect(result.name()).toContain('sdk-buffer-override');
});
it('saves stream file with maxUploadSize override and master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const { Readable } = require('stream');
const stream = Readable.from(Buffer.alloc(100, 'b'));
const file = new Parse.File('sdk-stream-override.txt', stream, 'text/plain');
const result = await file.save({ useMasterKey: true, maxUploadSize: '1mb' });
expect(result.url()).toBeDefined();
expect(result.name()).toContain('sdk-stream-override');
});
it('rejects maxUploadSize override without master key', async () => {
await reconfigureServer({ maxUploadSize: '10b' });
const data = Buffer.alloc(100, 'c');
const file = new Parse.File('sdk-no-master.txt', data, 'text/plain');
try {
await file.save({ maxUploadSize: '1mb' });
fail('should have thrown');
} catch (error) {
expect(error.error).toBeDefined();
}
});
});
it('fires beforeSave trigger with request.stream = true on streaming upload', async () => {
let receivedStream;
let receivedData;
+60 -9
View File
@@ -98,6 +98,7 @@ export class FilesRouter {
router.post(
'/files/:filename',
this._earlyHeadersMiddleware(),
this._bodyParsingMiddleware(maxUploadSize),
Middlewares.handleParseHeaders,
Middlewares.handleParseSession,
@@ -234,17 +235,67 @@ export class FilesRouter {
}
}
_bodyParsingMiddleware(maxUploadSize) {
const rawParser = express.raw({
type: () => true,
limit: maxUploadSize,
});
return (req, res, next) => {
if (req.get('X-Parse-Upload-Mode') === 'stream') {
req._maxUploadSizeBytes = Utils.parseSizeToBytes(maxUploadSize);
/**
* Middleware that runs before body parsing to handle headers that must be
* resolved before the request body is consumed. Currently supports:
*
* - `X-Parse-File-Max-Upload-Size`: Overrides the server-wide `maxUploadSize`
* for this request. Requires the master key. The value uses the same format
* as the server option (e.g. `'50mb'`, `'1gb'`). Sets `req._maxUploadSizeOverride`
* (in bytes) for `_bodyParsingMiddleware` to use.
*/
_earlyHeadersMiddleware() {
return async (req, res, next) => {
const maxUploadSizeOverride = req.get('X-Parse-File-Max-Upload-Size');
if (!maxUploadSizeOverride) {
return next();
}
return rawParser(req, res, next);
const appId = req.get('X-Parse-Application-Id');
const config = Config.get(appId);
if (!config) {
const error = createSanitizedHttpError(403, 'Invalid application ID.', undefined);
res.status(error.status);
res.json({ error: error.message });
return;
}
const masterKey = await config.loadMasterKey();
if (req.get('X-Parse-Master-Key') !== masterKey) {
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
res.status(error.status);
res.json({ error: error.message });
return;
}
if (config.masterKeyIps?.length && !Middlewares.checkIp(req.ip, config.masterKeyIps, config.masterKeyIpsStore)) {
const error = createSanitizedHttpError(403, 'unauthorized: master key is required', config);
res.status(error.status);
res.json({ error: error.message });
return;
}
let parsedBytes;
try {
parsedBytes = Utils.parseSizeToBytes(maxUploadSizeOverride);
} catch {
return next(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`Invalid maxUploadSize override value: ${maxUploadSizeOverride}`
)
);
}
req._maxUploadSizeOverride = parsedBytes;
next();
};
}
_bodyParsingMiddleware(maxUploadSize) {
const defaultMaxBytes = Utils.parseSizeToBytes(maxUploadSize);
return (req, res, next) => {
if (req.get('X-Parse-Upload-Mode') === 'stream') {
req._maxUploadSizeBytes = req._maxUploadSizeOverride ?? defaultMaxBytes;
return next();
}
const limit = req._maxUploadSizeOverride ?? maxUploadSize;
return express.raw({ type: () => true, limit })(req, res, next);
};
}