From cd19ae783f680c07fa09f1b2ceb6c71c78bdf383 Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:44:32 -0400 Subject: [PATCH 1/2] fix: Query on /installations and /audiences fails with "Invalid key name: 0" when the request is sent as POST with _method=GET --- spec/AudienceRouter.spec.js | 24 +++++++++++ spec/InstallationsRouter.spec.js | 69 ++++++++++++++++++++++++++++++ src/Routers/AudiencesRouter.js | 1 + src/Routers/ClassesRouter.js | 30 ++++++++++--- src/Routers/InstallationsRouter.js | 1 + 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 96b8f2459f..154de89361 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -96,6 +96,30 @@ describe('AudiencesRouter', () => { }); }); + it('uses find condition from a where string in request.body', async () => { + const config = Config.get('test'); + await rest.create(config, auth.master(config), '_Audience', { + name: 'Android Users', + query: '{ "test": "android" }', + }); + await rest.create(config, auth.master(config), '_Audience', { + name: 'Iphone Users', + query: '{ "test": "ios" }', + }); + + const router = new AudiencesRouter(); + const res = await router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: JSON.stringify({ query: '{ "test": "android" }' }) }, + query: {}, + info: {}, + }); + + expect(res.response.results.length).toEqual(1); + expect(res.response.results[0].name).toEqual('Android Users'); + }); + it('query installations with limit = 0', done => { const config = Config.get('test'); const androidAudienceRequest = { diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 1ccad5013d..4278dd8845 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -1,6 +1,7 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); +const httpRequest = require('../lib/request'); const InstallationsRouter = require('../lib/Routers/InstallationsRouter').InstallationsRouter; describe('InstallationsRouter', () => { @@ -244,4 +245,72 @@ describe('InstallationsRouter', () => { done(); }); }); + + it('uses find condition from a where string in request.body', async () => { + const config = Config.get('test'); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', + }); + + const router = new InstallationsRouter(); + const res = await router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: JSON.stringify({ deviceType: 'android' }) }, + query: {}, + info: {}, + }); + + expect(res.response.results.length).toEqual(1); + expect(res.response.results[0].deviceType).toEqual('android'); + }); + + it('rejects an invalid where string in request.body', async () => { + const config = Config.get('test'); + const router = new InstallationsRouter(); + expect(() => + router.handleFind({ + config: config, + auth: auth.master(config), + body: { where: 'not json' }, + query: {}, + info: {}, + }) + ).toThrowError(Parse.Error, 'where parameter is not valid JSON'); + }); + + it('finds installations when the client sends the find as POST with _method=GET', async () => { + const config = Config.get('test'); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }); + await rest.create(config, auth.nobody(config), '_Installation', { + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', + }); + + // A client that exceeds the maximum URL length sends the find as a POST + // with a urlencoded body, so `where` arrives as a string. + const response = await httpRequest({ + method: 'POST', + url: 'http://localhost:8378/1/installations', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `_method=GET&where=${encodeURIComponent( + JSON.stringify({ installationId: { $in: ['12345678-abcd-abcd-abcd-123456789abc'] } }) + )}`, + }); + + expect(response.data.results.length).toEqual(1); + expect(response.data.results[0].deviceType).toEqual('android'); + }); }); diff --git a/src/Routers/AudiencesRouter.js b/src/Routers/AudiencesRouter.js index dde95345da..089d0e0446 100644 --- a/src/Routers/AudiencesRouter.js +++ b/src/Routers/AudiencesRouter.js @@ -10,6 +10,7 @@ export class AudiencesRouter extends ClassesRouter { handleFind(req) { const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); + ClassesRouter.decodeWhere(body); return rest .find( diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index b527131a39..fddd380f62 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -29,13 +29,7 @@ export class ClassesRouter extends PromiseRouter { if (body.redirectClassNameForKey) { options.redirectClassNameForKey = String(body.redirectClassNameForKey); } - if (typeof body.where === 'string') { - try { - body.where = JSON.parse(body.where); - } catch { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'where parameter is not valid JSON'); - } - } + ClassesRouter.decodeWhere(body); return rest .find( req.config, @@ -157,6 +151,28 @@ export class ClassesRouter extends PromiseRouter { return json; } + /** + * Decodes a `where` that arrives as a JSON string instead of an object. + * + * A client that exceeds the maximum URL length sends a find as + * `POST` + `_method=GET` with a urlencoded body, so `where` reaches the + * router as a string rather than being decoded by `JSONFromQuery`. Every + * router that serves a find has to decode it, otherwise the string is passed + * to the query layer and iterated character by character. + * + * Mutates `body` in place and returns the decoded `where`. + */ + static decodeWhere(body) { + if (typeof body.where === 'string') { + try { + body.where = JSON.parse(body.where); + } catch { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'where parameter is not valid JSON'); + } + } + return body.where; + } + static optionsFromBody(body, defaultLimit) { const allowConstraints = [ 'skip', diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 91861a7ba2..fe99cba7a7 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -12,6 +12,7 @@ export class InstallationsRouter extends ClassesRouter { handleFind(req) { const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query)); const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); + ClassesRouter.decodeWhere(body); return rest .find( req.config, From ca75b1fed69921f7843dc20b921d9b02dc352626 Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:21:30 -0400 Subject: [PATCH 2/2] Assert INVALID_JSON error code for invalid where string --- spec/InstallationsRouter.spec.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 4278dd8845..4d3df95d00 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -273,15 +273,22 @@ describe('InstallationsRouter', () => { it('rejects an invalid where string in request.body', async () => { const config = Config.get('test'); const router = new InstallationsRouter(); - expect(() => - router.handleFind({ + let error; + try { + await router.handleFind({ config: config, auth: auth.master(config), body: { where: 'not json' }, query: {}, info: {}, - }) - ).toThrowError(Parse.Error, 'where parameter is not valid JSON'); + }); + fail('find should have been rejected'); + return; + } catch (e) { + error = e; + } + expect(error.code).toEqual(Parse.Error.INVALID_JSON); + expect(error.message).toEqual('where parameter is not valid JSON'); }); it('finds installations when the client sends the find as POST with _method=GET', async () => {