mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
fix(core): Create an XML parser for each webhook request body (backport to release-candidate/2.44.x) (#41112)
Co-authored-by: Mike Repeć <mike.repec@n8n.io>
This commit is contained in:
parent
16b49eb412
commit
c52f1817b3
|
|
@ -2,9 +2,9 @@ import type { Request, Response } from 'express';
|
|||
import { mock } from 'vitest-mock-extended';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { BadRequestError } from '@n8n/errors';
|
||||
import { BadRequestError, UnprocessableRequestError } from '@n8n/errors';
|
||||
|
||||
import { rawBodyReader } from '../body-parser';
|
||||
import { parseBody, rawBodyReader } from '../body-parser';
|
||||
|
||||
/** Build a request backed by a real readable stream carrying `payload`. */
|
||||
const mockRequest = (payload = '', headers: Record<string, string> = {}) => {
|
||||
|
|
@ -57,3 +57,36 @@ describe('rawBodyReader', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBody', () => {
|
||||
const xmlRequest = (body: string) =>
|
||||
({
|
||||
contentType: 'text/xml',
|
||||
encoding: 'utf-8',
|
||||
rawBody: Buffer.from(body),
|
||||
readRawBody: async () => {},
|
||||
}) as unknown as Request;
|
||||
|
||||
// Bodies over 10,000 chars make xml2js parse in chunks across ticks.
|
||||
const largeXml = (id: string) =>
|
||||
`<?xml version="1.0"?><Envelope><Body>${'<Item><Name>x</Name></Item>'.repeat(1000)}<Id>${id}</Id></Body></Envelope>`;
|
||||
|
||||
it('parses concurrent large XML bodies independently', async () => {
|
||||
const requests = ['a', 'b', 'c'].map((id) => xmlRequest(largeXml(id)));
|
||||
|
||||
await Promise.all(requests.map(parseBody));
|
||||
|
||||
expect(requests.map((req) => req.body.envelope.body.id)).toEqual(['a', 'b', 'c']);
|
||||
|
||||
const after = xmlRequest('<root><id>after</id></root>');
|
||||
await parseBody(after);
|
||||
expect(after.body).toEqual({ root: { id: 'after' } });
|
||||
});
|
||||
|
||||
it('rejects an XML body that fails inside an element handler', async () => {
|
||||
// An element named `_` collides with the text key and throws while closing the tag.
|
||||
await expect(parseBody(xmlRequest('<a><_>x</_></a>'))).rejects.toThrow(
|
||||
UnprocessableRequestError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ import { createGunzip, createInflate } from 'zlib';
|
|||
|
||||
import { BadRequestError, UnprocessableRequestError } from '@n8n/errors';
|
||||
|
||||
const xmlParser = new XmlParser({
|
||||
async: true,
|
||||
normalize: true, // Trim whitespace inside text nodes
|
||||
normalizeTags: true, // Transform tags to lowercase
|
||||
explicitArray: false, // Only put properties in array if length > 1
|
||||
tagNameProcessors: [sanitizeXmlName],
|
||||
attrNameProcessors: [sanitizeXmlName],
|
||||
});
|
||||
// One parser per request: xml2js keeps parse state on the instance, and async
|
||||
// mode spreads large bodies over several ticks.
|
||||
const createXmlParser = () =>
|
||||
new XmlParser({
|
||||
async: true,
|
||||
normalize: true, // Trim whitespace inside text nodes
|
||||
normalizeTags: true, // Transform tags to lowercase
|
||||
explicitArray: false, // Only put properties in array if length > 1
|
||||
tagNameProcessors: [sanitizeXmlName],
|
||||
attrNameProcessors: [sanitizeXmlName],
|
||||
});
|
||||
|
||||
const payloadSizeMax = Container.get(GlobalConfig).endpoints.payloadSizeMax;
|
||||
|
||||
|
|
@ -97,7 +100,7 @@ export const parseBody = async (req: Request) => {
|
|||
req.body = jsonParse(stripBom(rawBody.toString(encoding)));
|
||||
} else if (contentType?.endsWith('/xml') || contentType?.endsWith('+xml')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
req.body = await xmlParser.parseStringPromise(rawBody.toString(encoding));
|
||||
req.body = await createXmlParser().parseStringPromise(rawBody.toString(encoding));
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
req.body = parseQueryString(stripBom(rawBody.toString(encoding)), undefined, undefined, {
|
||||
maxKeys: 1000,
|
||||
|
|
|
|||
13
patches/xml2js@0.6.2.patch
Normal file
13
patches/xml2js@0.6.2.patch
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
diff --git a/lib/parser.js b/lib/parser.js
|
||||
index 7fa2fc5813450bcc5ff3199e2bfd613409a6087b..50c3ca2673b514a25ac9b74b241eef4c8bc41d88 100644
|
||||
--- a/lib/parser.js
|
||||
+++ b/lib/parser.js
|
||||
@@ -96,7 +96,7 @@
|
||||
err = error1;
|
||||
if (!this.saxParser.errThrown) {
|
||||
this.saxParser.errThrown = true;
|
||||
- return this.emit(err);
|
||||
+ return this.emit('error', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1090,6 +1090,7 @@ patchedDependencies:
|
|||
v-code-diff: 21588de80e591bbc1e5a068d9bce311db5254686443652945d2c7887fdafe9d9
|
||||
vue-tsc@2.2.8: e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f
|
||||
vuedraggable@4.1.0: eaa2c80f80cdc4293b0f1c9c0410823960f839fc15f155c8cb23666d5950e049
|
||||
xml2js@0.6.2: 3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3
|
||||
z-vue-scan: bbd573be7e456224f369aafb4d66374523c4847964a4655a6a1654fd40e8b0f8
|
||||
|
||||
importers:
|
||||
|
|
@ -5200,7 +5201,7 @@ importers:
|
|||
version: 8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
xml2js:
|
||||
specifier: 'catalog:'
|
||||
version: 0.6.2
|
||||
version: 0.6.2(patch_hash=3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3)
|
||||
xmllint-wasm:
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
|
|
@ -7948,7 +7949,7 @@ importers:
|
|||
version: 3.12.2
|
||||
xml2js:
|
||||
specifier: 'catalog:'
|
||||
version: 0.6.2
|
||||
version: 0.6.2(patch_hash=3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3)
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
|
|
@ -8573,7 +8574,7 @@ importers:
|
|||
version: 11.1.1
|
||||
xml2js:
|
||||
specifier: 'catalog:'
|
||||
version: 0.6.2
|
||||
version: 0.6.2(patch_hash=3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3)
|
||||
devDependencies:
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
|
|
@ -36636,7 +36637,7 @@ snapshots:
|
|||
bluebird: 3.7.2
|
||||
crlf-normalize: 1.0.19(ts-toolbelt@9.6.0)
|
||||
tslib: 2.8.1
|
||||
xml2js: 0.6.2
|
||||
xml2js: 0.6.2(patch_hash=3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3)
|
||||
transitivePeerDependencies:
|
||||
- ts-toolbelt
|
||||
|
||||
|
|
@ -45381,7 +45382,7 @@ snapshots:
|
|||
sax: 1.2.4
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xml2js@0.6.2:
|
||||
xml2js@0.6.2(patch_hash=3efbf117e53f23a62e3e3faa4d02fef8e73d5359bfd50a24e94d32d6f1257bf3):
|
||||
dependencies:
|
||||
sax: 1.2.4
|
||||
xmlbuilder: 11.0.1
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ patchedDependencies:
|
|||
'@chat-adapter/teams@4.37.0': patches/@chat-adapter__teams@4.37.0.patch
|
||||
'@chat-adapter/whatsapp@4.37.0': patches/@chat-adapter__whatsapp@4.37.0.patch
|
||||
'@icetee/ftp@0.3.15': patches/@icetee__ftp@0.3.15.patch
|
||||
xml2js@0.6.2: patches/xml2js@0.6.2.patch
|
||||
|
||||
allowBuilds:
|
||||
'@biomejs/biome': false
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user