mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add error causes to error serializations
This commit is contained in:
parent
7505f07f2f
commit
0245b31e0c
@ -62,6 +62,7 @@ export class ConvertingErrorHandler extends ErrorHandler {
|
|||||||
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
|
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
|
||||||
if (!this.showStackTrace) {
|
if (!this.showStackTrace) {
|
||||||
delete error.stack;
|
delete error.stack;
|
||||||
|
delete (error as any).cause;
|
||||||
}
|
}
|
||||||
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
|
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
|
||||||
const identifier = { path: representation.metadata.identifier.value };
|
const identifier = { path: representation.metadata.identifier.value };
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||||
import type { HttpError } from '../../util/errors/HttpError';
|
import { isError } from '../../util/errors/ErrorUtil';
|
||||||
|
import { HttpError } from '../../util/errors/HttpError';
|
||||||
import { extractErrorTerms } from '../../util/errors/HttpErrorUtil';
|
import { extractErrorTerms } from '../../util/errors/HttpErrorUtil';
|
||||||
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
||||||
import { getSingleItem } from '../../util/StreamUtil';
|
import { getSingleItem } from '../../util/StreamUtil';
|
||||||
@ -19,24 +20,48 @@ export class ErrorToJsonConverter extends BaseTypedRepresentationConverter {
|
|||||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
const error = await getSingleItem(representation.data) as HttpError;
|
const error = await getSingleItem(representation.data) as HttpError;
|
||||||
|
|
||||||
const result: Record<string, any> = {
|
const result = this.errorToJson(error);
|
||||||
|
|
||||||
|
// Update the content-type to JSON
|
||||||
|
return new BasicRepresentation(JSON.stringify(result), representation.metadata, APPLICATION_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private errorToJson(error: unknown): unknown {
|
||||||
|
if (!isError(error)) {
|
||||||
|
// Try to see if we can make valid JSON, empty object if there is an error.
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(error));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, unknown> = {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
statusCode: error.statusCode,
|
|
||||||
errorCode: error.errorCode,
|
|
||||||
details: extractErrorTerms(error.metadata),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (error.stack) {
|
||||||
|
result.stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HttpError.isInstance(error)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.statusCode = error.statusCode;
|
||||||
|
result.errorCode = error.errorCode;
|
||||||
|
result.details = extractErrorTerms(error.metadata);
|
||||||
|
|
||||||
// OAuth errors responses require additional fields
|
// OAuth errors responses require additional fields
|
||||||
if (OAuthHttpError.isInstance(error)) {
|
if (OAuthHttpError.isInstance(error)) {
|
||||||
Object.assign(result, error.mandatoryFields);
|
Object.assign(result, error.mandatoryFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.stack) {
|
if (error.cause) {
|
||||||
result.stack = error.stack;
|
result.cause = this.errorToJson(error.cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the content-type to JSON
|
return result;
|
||||||
return new BasicRepresentation(JSON.stringify(result), representation.metadata, APPLICATION_JSON);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,8 +75,8 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the main template, embedding the rendered error description
|
// Render the main template, embedding the rendered error description
|
||||||
const { name, message, stack } = error;
|
const { name, message, stack, cause } = error;
|
||||||
const contents = { name, message, stack, description };
|
const contents = { name, message, stack, description, cause };
|
||||||
const rendered = await this.templateEngine
|
const rendered = await this.templateEngine
|
||||||
.handleSafe({ contents, template: { templateFile: this.mainTemplatePath }});
|
.handleSafe({ contents, template: { templateFile: this.mainTemplatePath }});
|
||||||
|
|
||||||
|
@ -16,3 +16,15 @@ _No further details available._
|
|||||||
{{ stack }}
|
{{ stack }}
|
||||||
```
|
```
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if cause}}
|
||||||
|
## Cause
|
||||||
|
{{#if cause.message}}
|
||||||
|
{{ cause.message }}
|
||||||
|
{{/if}}
|
||||||
|
{{#if cause.stack}}
|
||||||
|
```
|
||||||
|
{{ cause.stack }}
|
||||||
|
```
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
@ -18,7 +18,7 @@ import literal = DataFactory.literal;
|
|||||||
|
|
||||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
||||||
|
|
||||||
async function expectValidArgs(args: RepresentationConverterArgs, stack?: string): Promise<void> {
|
async function expectValidArgs(args: RepresentationConverterArgs, stack?: string, cause?: Error): Promise<void> {
|
||||||
expect(args.preferences).toBe(preferences);
|
expect(args.preferences).toBe(preferences);
|
||||||
expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
|
expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
|
||||||
.toEqualRdfTerm(literal(404, XSD.terms.integer));
|
.toEqualRdfTerm(literal(404, XSD.terms.integer));
|
||||||
@ -30,11 +30,13 @@ async function expectValidArgs(args: RepresentationConverterArgs, stack?: string
|
|||||||
const resultError = errorArray[0];
|
const resultError = errorArray[0];
|
||||||
expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
|
expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
|
||||||
expect(resultError.stack).toBe(stack);
|
expect(resultError.stack).toBe(stack);
|
||||||
|
expect(resultError.cause).toBe(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('A ConvertingErrorHandler', (): void => {
|
describe('A ConvertingErrorHandler', (): void => {
|
||||||
// The error object can get modified by the handler
|
// The error object can get modified by the handler
|
||||||
let error: HttpError;
|
let error: HttpError;
|
||||||
|
const cause = new Error('cause');
|
||||||
let stack: string | undefined;
|
let stack: string | undefined;
|
||||||
const request = {} as HttpRequest;
|
const request = {} as HttpRequest;
|
||||||
let converter: jest.Mocked<RepresentationConverter>;
|
let converter: jest.Mocked<RepresentationConverter>;
|
||||||
@ -42,7 +44,7 @@ describe('A ConvertingErrorHandler', (): void => {
|
|||||||
let handler: ConvertingErrorHandler;
|
let handler: ConvertingErrorHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
error = new NotFoundHttpError('not here');
|
error = new NotFoundHttpError('not here', { cause });
|
||||||
({ stack } = error);
|
({ stack } = error);
|
||||||
converter = {
|
converter = {
|
||||||
canHandle: jest.fn(),
|
canHandle: jest.fn(),
|
||||||
@ -89,7 +91,7 @@ describe('A ConvertingErrorHandler', (): void => {
|
|||||||
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
||||||
expect(converter.handle).toHaveBeenCalledTimes(1);
|
expect(converter.handle).toHaveBeenCalledTimes(1);
|
||||||
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||||
await expectValidArgs(args, stack);
|
await expectValidArgs(args, stack, cause);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
|
it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
|
||||||
@ -98,10 +100,10 @@ describe('A ConvertingErrorHandler', (): void => {
|
|||||||
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
||||||
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||||
await expectValidArgs(args, stack);
|
await expectValidArgs(args, stack, cause);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
|
it('hides the stack trace and cause if the option is disabled.', async(): Promise<void> => {
|
||||||
handler = new ConvertingErrorHandler(converter, preferenceParser);
|
handler = new ConvertingErrorHandler(converter, preferenceParser);
|
||||||
const prom = handler.handle({ error, request });
|
const prom = handler.handle({ error, request });
|
||||||
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
|
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
|
||||||
|
@ -99,4 +99,66 @@ describe('An ErrorToJsonConverter', (): void => {
|
|||||||
details: {},
|
details: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can handle non-error causes.', async(): Promise<void> => {
|
||||||
|
const error = new BadRequestHttpError('error text', { cause: 'not an error' });
|
||||||
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
|
await expect(prom).resolves.toBeDefined();
|
||||||
|
const result = await prom;
|
||||||
|
expect(result.binary).toBe(true);
|
||||||
|
expect(result.metadata.contentType).toBe('application/json');
|
||||||
|
await expect(readJsonStream(result.data)).resolves.toEqual({
|
||||||
|
name: 'BadRequestHttpError',
|
||||||
|
message: 'error text',
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'H400',
|
||||||
|
stack: error.stack,
|
||||||
|
details: {},
|
||||||
|
cause: 'not an error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores non-error causes that cannot be parsed.', async(): Promise<void> => {
|
||||||
|
const error = new BadRequestHttpError('error text', { cause: BigInt(5) });
|
||||||
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
|
await expect(prom).resolves.toBeDefined();
|
||||||
|
const result = await prom;
|
||||||
|
expect(result.binary).toBe(true);
|
||||||
|
expect(result.metadata.contentType).toBe('application/json');
|
||||||
|
await expect(readJsonStream(result.data)).resolves.toEqual({
|
||||||
|
name: 'BadRequestHttpError',
|
||||||
|
message: 'error text',
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'H400',
|
||||||
|
stack: error.stack,
|
||||||
|
details: {},
|
||||||
|
cause: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle non-HTTP errors as cause.', async(): Promise<void> => {
|
||||||
|
const cause = new Error('error');
|
||||||
|
const error = new BadRequestHttpError('error text', { cause });
|
||||||
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
|
await expect(prom).resolves.toBeDefined();
|
||||||
|
const result = await prom;
|
||||||
|
expect(result.binary).toBe(true);
|
||||||
|
expect(result.metadata.contentType).toBe('application/json');
|
||||||
|
await expect(readJsonStream(result.data)).resolves.toEqual({
|
||||||
|
name: 'BadRequestHttpError',
|
||||||
|
message: 'error text',
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'H400',
|
||||||
|
stack: error.stack,
|
||||||
|
details: {},
|
||||||
|
cause: {
|
||||||
|
name: 'Error',
|
||||||
|
message: 'error',
|
||||||
|
stack: cause.stack,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ describe('An ErrorToTemplateConverter', (): void => {
|
|||||||
const extension = '.html';
|
const extension = '.html';
|
||||||
const contentType = 'text/html';
|
const contentType = 'text/html';
|
||||||
const errorCode = 'E0001';
|
const errorCode = 'E0001';
|
||||||
|
const cause = new Error('cause');
|
||||||
let templateEngine: jest.Mocked<TemplateEngine>;
|
let templateEngine: jest.Mocked<TemplateEngine>;
|
||||||
let converter: ErrorToTemplateConverter;
|
let converter: ErrorToTemplateConverter;
|
||||||
const preferences = {};
|
const preferences = {};
|
||||||
@ -47,7 +48,7 @@ describe('An ErrorToTemplateConverter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls the template engine with all HTTP error fields.', async(): Promise<void> => {
|
it('calls the template engine with all HTTP error fields.', async(): Promise<void> => {
|
||||||
const error = new BadRequestHttpError('error text');
|
const error = new BadRequestHttpError('error text', { cause });
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
templateEngine.handleSafe.mockRejectedValueOnce(new Error('error-specific template not found'));
|
templateEngine.handleSafe.mockRejectedValueOnce(new Error('error-specific template not found'));
|
||||||
@ -63,7 +64,7 @@ describe('An ErrorToTemplateConverter', (): void => {
|
|||||||
template: { templatePath: '/templates/codes', templateFile: 'H400.html' },
|
template: { templatePath: '/templates/codes', templateFile: 'H400.html' },
|
||||||
});
|
});
|
||||||
expect(templateEngine.handleSafe).toHaveBeenNthCalledWith(2, {
|
expect(templateEngine.handleSafe).toHaveBeenNthCalledWith(2, {
|
||||||
contents: { name: 'BadRequestHttpError', message: 'error text', stack: error.stack },
|
contents: { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, cause: error.cause },
|
||||||
template: { templateFile: mainTemplatePath },
|
template: { templateFile: mainTemplatePath },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user