Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/grumpy-suns-remember.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 1 addition & 4 deletions apps/meteor/app/api/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ export class RocketChatAPIRouter<
return async (c: HonoContext): Promise<ResponseSchema<TypedOptions>> => {
const { req, res } = c;
const queryParams = this.parseQueryParams(req);
const bodyParams = await this.parseBodyParams<{ bodyParamsOverride: Record<string, any> }>({
request: req,
extra: { bodyParamsOverride: c.var['bodyParams-override'] || {} },
});
const bodyParams = c.get('bodyParams-override') ?? (await this.parseBodyParams({ request: req }));

const request = req.raw.clone();

Expand Down
19 changes: 16 additions & 3 deletions apps/meteor/app/integrations/server/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,29 @@ const middleware = async (c: Context, next: Next): Promise<void> => {
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);
}
Expand Down
95 changes: 45 additions & 50 deletions apps/meteor/tests/end-to-end/api/incoming-integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});

Expand Down Expand Up @@ -444,52 +442,49 @@ 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()}` };
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;
});
});

Expand Down
Loading