feat: Store original target in error metadata

This commit is contained in:
Joachim Van Herwegen 2024-04-03 15:46:01 +02:00
parent 486241f3d4
commit 419312ee5f
5 changed files with 99 additions and 14 deletions

View File

@ -7,20 +7,26 @@
"@type": "SafeErrorHandler", "@type": "SafeErrorHandler",
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }, "showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
"errorHandler": { "errorHandler": {
"@type": "WaterfallHandler", "@id": "urn:solid-server:default:TargetExtractorErrorHandler",
"handlers": [ "@type": "TargetExtractorErrorHandler",
{ "targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
"comment": "Internally redirects are created by throwing a specific error, this handler converts them to the correct response.", "errorHandler": {
"@type": "RedirectingErrorHandler" "@id": "urn:solid-server:default:WaterfallErrorHandler",
}, "@type": "WaterfallHandler",
{ "handlers": [
"comment": "Converts an Error object into a representation for an HTTP response.", {
"@type": "ConvertingErrorHandler", "comment": "Internally redirects are created by throwing a specific error, this handler converts them to the correct response.",
"converter": { "@id": "urn:solid-server:default:UiEnabledConverter" }, "@type": "RedirectingErrorHandler"
"preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" }, },
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" } {
} "comment": "Converts an Error object into a representation for an HTTP response.",
] "@type": "ConvertingErrorHandler",
"converter": { "@id": "urn:solid-server:default:UiEnabledConverter" },
"preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" },
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
}
]
}
} }
} }
] ]

View File

@ -0,0 +1,30 @@
import { DataFactory } from 'n3';
import { SOLID_ERROR } from '../../../util/Vocabularies';
import type { TargetExtractor } from '../../input/identifier/TargetExtractor';
import type { ResponseDescription } from '../response/ResponseDescription';
import type { ErrorHandlerArgs } from './ErrorHandler';
import { ErrorHandler } from './ErrorHandler';
/**
* Adds metadata to an error to indicate what the identifier was of the resource originally being targeted.
*/
export class TargetExtractorErrorHandler extends ErrorHandler {
protected readonly errorHandler: ErrorHandler;
protected readonly targetExtractor: TargetExtractor;
public constructor(errorHandler: ErrorHandler, targetExtractor: TargetExtractor) {
super();
this.errorHandler = errorHandler;
this.targetExtractor = targetExtractor;
}
public async canHandle(input: ErrorHandlerArgs): Promise<void> {
return this.errorHandler.canHandle(input);
}
public async handle(input: ErrorHandlerArgs): Promise<ResponseDescription> {
const target = await this.targetExtractor.handleSafe(input);
input.error.metadata.add(SOLID_ERROR.terms.target, DataFactory.namedNode(target.path));
return this.errorHandler.handle(input);
}
}

View File

@ -100,6 +100,7 @@ export * from './http/output/error/ConvertingErrorHandler';
export * from './http/output/error/ErrorHandler'; export * from './http/output/error/ErrorHandler';
export * from './http/output/error/RedirectingErrorHandler'; export * from './http/output/error/RedirectingErrorHandler';
export * from './http/output/error/SafeErrorHandler'; export * from './http/output/error/SafeErrorHandler';
export * from './http/output/error/TargetExtractorErrorHandler';
// HTTP/Output/Metadata // HTTP/Output/Metadata
export * from './http/output/metadata/AllowAcceptHeaderWriter'; export * from './http/output/metadata/AllowAcceptHeaderWriter';

View File

@ -283,6 +283,7 @@ export const SOLID_ERROR = createVocabulary(
'errorCode', 'errorCode',
'errorResponse', 'errorResponse',
'stack', 'stack',
'target',
); );
// Used to pass parameters to error templates // Used to pass parameters to error templates

View File

@ -0,0 +1,47 @@
import type { TargetExtractor } from '../../../../../src/http/input/identifier/TargetExtractor';
import type { ErrorHandler, ErrorHandlerArgs } from '../../../../../src/http/output/error/ErrorHandler';
import { TargetExtractorErrorHandler } from '../../../../../src/http/output/error/TargetExtractorErrorHandler';
import type { ResourceIdentifier } from '../../../../../src/http/representation/ResourceIdentifier';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { SOLID_ERROR } from '../../../../../src/util/Vocabularies';
describe('A TargetExtractorErrorHandler', (): void => {
const identifier: ResourceIdentifier = { path: 'http://example.com/foo' };
let input: ErrorHandlerArgs;
let source: jest.Mocked<ErrorHandler>;
let targetExtractor: jest.Mocked<TargetExtractor>;
let handler: TargetExtractorErrorHandler;
beforeEach(async(): Promise<void> => {
input = {
request: 'request' as any,
error: new NotFoundHttpError(),
};
source = {
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('response'),
} satisfies Partial<ErrorHandler> as any;
targetExtractor = {
handleSafe: jest.fn().mockResolvedValue(identifier),
} satisfies Partial<TargetExtractor> as any;
handler = new TargetExtractorErrorHandler(source, targetExtractor);
});
it('can handle input its source can handle.', async(): Promise<void> => {
await expect(handler.canHandle(input)).resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenLastCalledWith(input);
const error = new Error('bad data');
source.canHandle.mockRejectedValueOnce(error);
await expect(handler.canHandle(input)).rejects.toThrow(error);
});
it('adds the identifier as metadata.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toBe('response');
expect(input.error.metadata.get(SOLID_ERROR.terms.target)?.value).toEqual(identifier.path);
expect(targetExtractor.handleSafe).toHaveBeenLastCalledWith(input);
});
});