Keep displayOptions out of collection children

n8n resolves every parameter against its displayOptions when it builds a
Workflow, before any execution exists. A displayOptions on a child of a
collection or fixedCollection made that resolution loop, so image runs
answered HTTP 500 in under a second and left no execution record at all.

Drop those four displayOptions and say in each description when the field
applies, then build the Workflow twice in the validation test - with every
collection present but empty - so this cannot come back.
This commit is contained in:
ed0ss 2026-10-23 00:29:39 +03:00
parent 777ca4d749
commit e490c1dc83
2 changed files with 45 additions and 8 deletions

View file

@ -390,8 +390,8 @@ export class Teamorouter implements INodeType {
name: 'binaryProperty', name: 'binaryProperty',
type: 'string', type: 'string',
default: 'data', default: 'data',
displayOptions: { show: { source: ['binary'] } }, description:
description: 'Name of the binary property that holds the image', 'Name of the binary property that holds the image. Used when Source is Binary File.',
}, },
{ {
displayName: 'Image URL', displayName: 'Image URL',
@ -399,8 +399,8 @@ export class Teamorouter implements INodeType {
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'https://example.com/cat.png', placeholder: 'https://example.com/cat.png',
displayOptions: { show: { source: ['url'] } }, description:
description: 'Publicly reachable image link, or a data:image/...;base64,... URL', 'Publicly reachable image link, or a data:image/...;base64,... URL. Used when Source is Image URL.',
}, },
], ],
}, },
@ -596,7 +596,6 @@ export class Teamorouter implements INodeType {
name: 'inputFidelity', name: 'inputFidelity',
type: 'options', type: 'options',
default: 'high', default: 'high',
displayOptions: { show: { operation: ['edit'] } },
options: [ options: [
{ {
name: 'High', name: 'High',
@ -609,19 +608,18 @@ export class Teamorouter implements INodeType {
description: 'Allow the model to reinterpret the source image more freely', description: 'Allow the model to reinterpret the source image more freely',
}, },
], ],
description: 'How closely the edit follows the source image', description: 'How closely the edit follows the source image. Only used when editing an image.',
}, },
{ {
displayName: 'Moderation', displayName: 'Moderation',
name: 'moderation', name: 'moderation',
type: 'options', type: 'options',
default: 'auto', default: 'auto',
displayOptions: { show: { operation: ['generate'] } },
options: [ options: [
{ name: 'Auto', value: 'auto', description: 'Standard content filter' }, { name: 'Auto', value: 'auto', description: 'Standard content filter' },
{ name: 'Low', value: 'low', description: 'Relaxed content filter' }, { name: 'Low', value: 'low', description: 'Relaxed content filter' },
], ],
description: 'Content filter applied by TeamoRouter', description: 'Content filter applied by TeamoRouter. Only used when generating an image.',
}, },
{ {
displayName: 'Number of Images', displayName: 'Number of Images',

View file

@ -105,6 +105,45 @@ for (const { file, instance } of loaded.nodes) {
} }
} }
// n8n builds a Workflow object — resolving every parameter against its displayOptions — before an
// execution exists, and it builds it again from the parameters it wrote back. If that resolution
// loops the webhook request dies with "There was a problem executing the workflow" and no run at all.
// Building with every collection present but empty is the shape that used to break.
const { Workflow } = require('n8n-workflow');
for (const { instance } of loaded.nodes) {
const containers = instance.description.properties
.filter((property) => property.type === 'collection' || property.type === 'fixedCollection')
.map((property) => property.name);
const resources = instance.description.properties.find((property) => property.name === 'resource')
?.options ?? [{ value: undefined }];
const build = (parameters) =>
new Workflow({
id: 'validation',
nodes: [
{
id: 'validation-node',
name: instance.description.displayName,
type: instance.description.name,
typeVersion: instance.description.version,
position: [0, 0],
parameters: JSON.parse(JSON.stringify(parameters)),
},
],
connections: {},
active: false,
nodeTypes: { getByNameAndVersion: () => instance, getByName: () => instance },
});
for (const { value: resource } of resources) {
const parameters = { resource, ...Object.fromEntries(containers.map((name) => [name, {}])) };
// The parameters of the first pass are the ones n8n stores and reads back.
const firstPass = build(parameters);
build(firstPass.nodes[instance.description.displayName].parameters);
}
}
const chatModel = loaded.nodes.find(({ instance }) => instance.description.name === 'lmChatTeamRouter').instance; const chatModel = loaded.nodes.find(({ instance }) => instance.description.name === 'lmChatTeamRouter').instance;
assert.deepEqual(chatModel.description.inputs, [], 'chat model sub-node must not have a main input'); assert.deepEqual(chatModel.description.inputs, [], 'chat model sub-node must not have a main input');
assert.ok( assert.ok(