From de11e087f8027d03b0dc04d3e7cfb80b09140cd8 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 26 Jan 2026 10:38:57 -0300 Subject: [PATCH 1/3] test: fix async assertions not being awaited in incoming integrations --- .../end-to-end/api/incoming-integrations.ts | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index a8986b5bddcdc..cb0029e47a1be 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -347,22 +347,20 @@ describe('[Incoming Integrations]', () => { .post(`/hooks/${integration._id}/${integration.token}`) .set('Content-Type', 'application/x-www-form-urlencoded') .send(`payload=${JSON.stringify(payload)}`) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; - }); - }); + .expect(200); + + const messagesResult = await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(messagesResult.body).to.have.property('success', true); + expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; }); }); @@ -444,52 +442,50 @@ describe('[Incoming Integrations]', () => { }); it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { - const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; + // Note: The script expects `request.content.text`, so we send `text` not `msg` + const payload = { text: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; await request .post(`/hooks/${withScript._id}/${withScript.token}`) .set('Content-Type', 'application/x-www-form-urlencoded') .send(`payload=${JSON.stringify(payload)}`) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; - }); - }); + .expect(200); + + const messagesResult = await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(messagesResult.body).to.have.property('success', true); + expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === payload.text)).to.be.true; }); - it('should send a message if the payload is a application/x-www-form-urlencoded JSON(when not set, default one) but theres no "payload" key, its just a string, the integration has a valid script', async () => { + it('should send a message if the payload is application/json and the integration has a valid script', async () => { const payload = { test: 'test' }; await request .post(`/hooks/${withScriptDefaultContentType._id}/${withScriptDefaultContentType.token}`) + .set('Content-Type', 'application/json') .send(JSON.stringify(payload)) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; - }); - }); + .expect(200); + + const messagesResult = await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(messagesResult.body).to.have.property('success', true); + expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; }); }); From 328b2a8e10f01beb005715bec59f9cadbd55012a Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 26 Jan 2026 10:50:43 -0300 Subject: [PATCH 2/3] fix: unwrap JSON from x-www-form-urlencoded payload in incoming webhooks --- apps/meteor/app/api/server/router.ts | 5 +---- .../meteor/app/integrations/server/api/api.ts | 19 ++++++++++++++++--- .../end-to-end/api/incoming-integrations.ts | 1 - 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 0fba18a206093..7b65fe81e9868 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -38,10 +38,7 @@ export class RocketChatAPIRouter< return async (c: HonoContext): Promise> => { const { req, res } = c; const queryParams = this.parseQueryParams(req); - const bodyParams = await this.parseBodyParams<{ bodyParamsOverride: Record }>({ - request: req, - extra: { bodyParamsOverride: c.var['bodyParams-override'] || {} }, - }); + const bodyParams = c.get('bodyParams-override') ?? (await this.parseBodyParams({ request: req })); const request = req.raw.clone(); diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 8e1a37c8e76fa..7e72862f70588 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -401,16 +401,29 @@ const middleware = async (c: Context, next: Next): Promise => { return next(); } + /** + * Slack/GitHub-style webhooks send JSON wrapped in a `payload` field with + * Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`). + * We unwrap it here so integrations receive the parsed JSON directly. + * + * Note: These webhooks only send the `payload` field with no additional form + * parameters, so we simply replace bodyParams with the parsed JSON. + */ if (body.payload) { - // need to compose the full payload in this weird way because body-parser thought it was a form - c.set('bodyParams-override', JSON.parse(body.payload)); + if (typeof body.payload === 'string') { + try { + c.set('bodyParams-override', JSON.parse(body.payload)); + } catch { + // Keep original without unwrapping + } + } return next(); } + incomingLogger.debug({ msg: 'Body received as application/x-www-form-urlencoded without the "payload" key, parsed as string', content, }); - c.set('bodyParams-override', JSON.parse(content)); } catch (e: any) { c.body(JSON.stringify({ success: false, error: e.message }), 400); } diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index cb0029e47a1be..5c4f4e074fa14 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -442,7 +442,6 @@ describe('[Incoming Integrations]', () => { }); it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { - // Note: The script expects `request.content.text`, so we send `text` not `msg` const payload = { text: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; await request From 15eee704cbbaad6c1b49684a1a71d4d75b4924b1 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 26 Jan 2026 18:46:33 -0300 Subject: [PATCH 3/3] add changeset --- .changeset/grumpy-suns-remember.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/grumpy-suns-remember.md diff --git a/.changeset/grumpy-suns-remember.md b/.changeset/grumpy-suns-remember.md new file mode 100644 index 0000000000000..58cb976fbb104 --- /dev/null +++ b/.changeset/grumpy-suns-remember.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/http-router': patch +'@rocket.chat/meteor': patch +--- + +Fixes incoming webhook integrations not receiving parsed JSON from x-www-form-urlencoded payload field.