Add FetchMiddlewares.clarifyFailures and improve permission error handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fredrik Adelöw <freben@spotify.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/app-defaults': patch
|
||||
---
|
||||
|
||||
Added `FetchMiddlewares.clarifyFailures()` to the default fetch API middleware stack.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': minor
|
||||
---
|
||||
|
||||
Added `FetchMiddlewares.clarifyFailures()` which replaces the uninformative "TypeError: Failed to fetch" with a message that includes the request method and URL.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Wrapped extension permission authorization in a try/catch to surface errors as `ForwardedError` with a clear message.
|
||||
@@ -113,6 +113,7 @@ export const apis = [
|
||||
identityApi,
|
||||
config: configApi,
|
||||
}),
|
||||
FetchMiddlewares.clarifyFailures(),
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -438,6 +438,7 @@ export interface FetchMiddleware {
|
||||
|
||||
// @public
|
||||
export class FetchMiddlewares {
|
||||
static clarifyFailures(): FetchMiddleware;
|
||||
static injectIdentityAuth(options: {
|
||||
identityApi: IdentityApi;
|
||||
config?: Config;
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClarifyFailuresFetchMiddleware } from './ClarifyFailuresFetchMiddleware';
|
||||
|
||||
describe('ClarifyFailuresFetchMiddleware', () => {
|
||||
it('passes through successful responses', async () => {
|
||||
const response = new Response('ok');
|
||||
const inner = jest.fn().mockResolvedValue(response);
|
||||
const middleware = new ClarifyFailuresFetchMiddleware();
|
||||
const result = await middleware.apply(inner)('https://example.com/api');
|
||||
expect(result).toBe(response);
|
||||
expect(inner).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('replaces "Failed to fetch" TypeError with one that includes the URL', async () => {
|
||||
const inner = jest.fn().mockRejectedValue(new TypeError('Failed to fetch'));
|
||||
const middleware = new ClarifyFailuresFetchMiddleware();
|
||||
await expect(
|
||||
middleware.apply(inner)('https://example.com/api/catalog'),
|
||||
).rejects.toThrow(
|
||||
new TypeError('Failed to fetch: GET https://example.com/api/catalog'),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not modify other TypeErrors', async () => {
|
||||
const error = new TypeError('some other error');
|
||||
const inner = jest.fn().mockRejectedValue(error);
|
||||
const middleware = new ClarifyFailuresFetchMiddleware();
|
||||
await expect(
|
||||
middleware.apply(inner)('https://example.com/api'),
|
||||
).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it('does not modify non-TypeError errors', async () => {
|
||||
const error = new Error('Failed to fetch');
|
||||
const inner = jest.fn().mockRejectedValue(error);
|
||||
const middleware = new ClarifyFailuresFetchMiddleware();
|
||||
await expect(
|
||||
middleware.apply(inner)('https://example.com/api'),
|
||||
).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FetchMiddleware } from './types';
|
||||
|
||||
/**
|
||||
* Replaces the generic "TypeError: Failed to fetch" error with a more
|
||||
* informative message that includes the target URL.
|
||||
*/
|
||||
export class ClarifyFailuresFetchMiddleware implements FetchMiddleware {
|
||||
apply(next: typeof fetch): typeof fetch {
|
||||
return async (input, init) => {
|
||||
try {
|
||||
return await next(input as any, init);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError && e.message === 'Failed to fetch') {
|
||||
const request = new Request(input as any, init);
|
||||
throw new TypeError(
|
||||
`Failed to fetch: ${request.method} ${request.url}`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { ClarifyFailuresFetchMiddleware } from './ClarifyFailuresFetchMiddleware';
|
||||
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
|
||||
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
|
||||
import { FetchMiddleware } from './types';
|
||||
@@ -74,5 +75,13 @@ export class FetchMiddlewares {
|
||||
return IdentityAuthInjectorFetchMiddleware.create(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the generic "TypeError: Failed to fetch" with a more informative
|
||||
* message that includes some request details to ease debugging.
|
||||
*/
|
||||
static clarifyFailures(): FetchMiddleware {
|
||||
return new ClarifyFailuresFetchMiddleware();
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
EvaluatePermissionRequest,
|
||||
EvaluatePermissionResponse,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { assertError, ForwardedError } from '@backstage/errors';
|
||||
|
||||
export type ExtensionPredicateContext = {
|
||||
featureFlags: string[];
|
||||
@@ -84,17 +85,25 @@ export function createPredicateContextLoader(options: {
|
||||
let allowedPermissions: string[] = [];
|
||||
const permissionApi = options.apis.get(localPermissionApiRef);
|
||||
if (permissionApi) {
|
||||
const permissionNames = options.predicateReferences.permissions;
|
||||
const responses = await Promise.all(
|
||||
permissionNames.map(name =>
|
||||
permissionApi.authorize({
|
||||
permission: { name, type: 'basic', attributes: {} },
|
||||
}),
|
||||
),
|
||||
);
|
||||
allowedPermissions = permissionNames.filter(
|
||||
(_, i) => responses[i].result === 'ALLOW',
|
||||
);
|
||||
try {
|
||||
const permissionNames = options.predicateReferences.permissions;
|
||||
const responses = await Promise.all(
|
||||
permissionNames.map(name =>
|
||||
permissionApi.authorize({
|
||||
permission: { name, type: 'basic', attributes: {} },
|
||||
}),
|
||||
),
|
||||
);
|
||||
allowedPermissions = permissionNames.filter(
|
||||
(_, i) => responses[i].result === 'ALLOW',
|
||||
);
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
throw new ForwardedError(
|
||||
'Failed to authorize extension permissions',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user