frontend-app-api: attribute attachemnt data failures to the attached extension

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-01-23 13:45:54 +01:00
parent 24eb7d7933
commit 492503a244
3 changed files with 130 additions and 41 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/frontend-app-api': patch
---
Updated error reporting and app tree resolution logic to attribute errors to the correct extension and allow app startup to proceed more optimistically:
- If an attachment fails to provide the required input data, the error is now attributed to the attachment rather than the parent extension.
- Singleton extension inputs will now only forward attachment errors if the input is required.
@@ -1574,41 +1574,109 @@ describe('instantiateAppNodeTree', () => {
]);
});
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
const node = makeNode(
resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
singleton: createExtensionInput([otherDataRef], {
singleton: true,
}),
},
output: [],
factory: () => [],
}),
{ namespace: 'app' },
),
describe('with attachment failures', () => {
const inputCountRef = createExtensionDataRef<number>().with({
id: 'input-count',
});
const attachmentWithoutRequiredData = makeInstanceWithId(
simpleExtension,
undefined,
);
expect(
createAppNodeInstance({
apis: testApis,
attachments: new Map([
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
]),
node,
collector,
}),
).toBeUndefined();
expect(collector.collectErrors()).toEqual([
{
code: 'EXTENSION_INPUT_DATA_MISSING',
message:
"extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')",
context: { node, inputName: 'singleton' },
},
]);
it('should proceed if input is optional', () => {
const node = makeNode(
resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
singleton: createExtensionInput([otherDataRef], {
singleton: true,
optional: true,
}),
},
output: [inputCountRef],
factory: ({ inputs }) => [
inputCountRef(inputs.singleton ? 1 : 0),
],
}),
{ namespace: 'app' },
),
);
expect(
createAppNodeInstance({
apis: testApis,
attachments: new Map([
['singleton', [attachmentWithoutRequiredData]],
]),
node,
collector,
})?.getData(inputCountRef),
).toBe(0);
expect(collector.collectErrors()).toEqual([
{
code: 'EXTENSION_INPUT_DATA_MISSING',
message:
"extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')",
context: {
node: attachmentWithoutRequiredData,
inputName: 'singleton',
},
},
]);
});
it('should fail if input is required', () => {
const node = makeNode(
resolveExtensionDefinition(
createExtension({
name: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
singleton: createExtensionInput([otherDataRef], {
singleton: true,
}),
},
output: [inputCountRef],
factory: ({ inputs }) => [
inputCountRef(inputs.singleton ? 1 : 0),
],
}),
{ namespace: 'app' },
),
);
expect(
createAppNodeInstance({
apis: testApis,
attachments: new Map([
['singleton', [attachmentWithoutRequiredData]],
]),
node,
collector,
})?.getData(inputCountRef),
).toBeUndefined();
expect(collector.collectErrors()).toEqual([
{
code: 'EXTENSION_ATTACHMENT_MISSING',
message:
"input 'singleton' is required but it failed to be instantiated",
context: { inputName: 'singleton', node },
},
{
code: 'EXTENSION_INPUT_DATA_MISSING',
message:
"extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')",
context: {
node: attachmentWithoutRequiredData,
inputName: 'singleton',
},
},
]);
});
});
});
});
@@ -119,7 +119,7 @@ function resolveInputDataContainer(
.map(r => `'${r.id}'`)
.join(', ');
collector.report({
collector.child({ node: attachment }).report({
code: 'EXTENSION_INPUT_DATA_MISSING',
message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`,
});
@@ -271,12 +271,25 @@ function resolveV2Inputs(
}
return undefined;
}
return resolveInputDataContainer(
input.extensionData,
attachedNodes[0],
inputName,
collector,
);
try {
return resolveInputDataContainer(
input.extensionData,
attachedNodes[0],
inputName,
collector,
);
} catch (error) {
if (error === INSTANTIATION_FAILED) {
if (input.config.optional) {
return undefined;
}
collector.report({
code: 'EXTENSION_ATTACHMENT_MISSING',
message: `input '${inputName}' is required but it failed to be instantiated`,
});
}
throw error;
}
}
return mapWithFailures(attachedNodes, attachment =>