feat: Create ErrorHandler to convert errors to Representations

This commit is contained in:
Joachim Van Herwegen
2021-06-03 16:26:55 +02:00
parent 3ef815ee6d
commit e1f95877da
18 changed files with 486 additions and 28 deletions

View File

@@ -0,0 +1,94 @@
import type {
RepresentationConverter,
RepresentationConverterArgs,
} from '../../storage/conversion/RepresentationConverter';
import { INTERNAL_ERROR } from '../../util/ContentTypes';
import { getStatusCode } from '../../util/errors/ErrorUtil';
import { toLiteral } from '../../util/TermUtil';
import { HTTP, XSD } from '../../util/Vocabularies';
import { BasicRepresentation } from '../representation/BasicRepresentation';
import type { Representation } from '../representation/Representation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { ErrorHandlerArgs } from './ErrorHandler';
import { ErrorHandler } from './ErrorHandler';
import type { ResponseDescription } from './response/ResponseDescription';
// Used by internal helper function
type PreparedArguments = {
statusCode: number;
conversionArgs: RepresentationConverterArgs;
};
/**
* Converts an error into a Representation of content type internal/error.
* Then feeds that representation into its converter to create a representation based on the given preferences.
*/
export class ConvertingErrorHandler extends ErrorHandler {
private readonly converter: RepresentationConverter;
private readonly showStackTrace: boolean;
public constructor(converter: RepresentationConverter, showStackTrace = false) {
super();
this.converter = converter;
this.showStackTrace = showStackTrace;
}
public async canHandle(input: ErrorHandlerArgs): Promise<void> {
const { conversionArgs } = this.prepareArguments(input);
await this.converter.canHandle(conversionArgs);
}
public async handle(input: ErrorHandlerArgs): Promise<ResponseDescription> {
const { statusCode, conversionArgs } = this.prepareArguments(input);
const converted = await this.converter.handle(conversionArgs);
return this.createResponse(statusCode, converted);
}
public async handleSafe(input: ErrorHandlerArgs): Promise<ResponseDescription> {
const { statusCode, conversionArgs } = this.prepareArguments(input);
const converted = await this.converter.handleSafe(conversionArgs);
return this.createResponse(statusCode, converted);
}
/**
* Prepares the arguments used by all functions.
*/
private prepareArguments({ error, preferences }: ErrorHandlerArgs): PreparedArguments {
const statusCode = getStatusCode(error);
const representation = this.toRepresentation(error, statusCode);
const identifier = { path: representation.metadata.identifier.value };
return { statusCode, conversionArgs: { identifier, representation, preferences }};
}
/**
* Creates a ResponseDescription based on the Representation.
*/
private createResponse(statusCode: number, converted: Representation): ResponseDescription {
return {
statusCode,
metadata: converted.metadata,
data: converted.data,
};
}
/**
* Creates a Representation based on the given error.
* Content type will be internal/error.
* The status code is used for metadata.
*/
private toRepresentation(error: Error, statusCode: number): Representation {
const metadata = new RepresentationMetadata(INTERNAL_ERROR);
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
if (!this.showStackTrace) {
delete error.stack;
}
return new BasicRepresentation([ error ], metadata, false);
}
}

View File

@@ -0,0 +1,13 @@
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
import type { ResponseDescription } from './response/ResponseDescription';
export interface ErrorHandlerArgs {
error: Error;
preferences: RepresentationPreferences;
}
/**
* Converts an error into a {@link ResponseDescription} based on the request preferences.
*/
export abstract class ErrorHandler extends AsyncHandler<ErrorHandlerArgs, ResponseDescription> {}

View File

@@ -0,0 +1,37 @@
import { getStatusCode } from '../../util/errors/ErrorUtil';
import { guardedStreamFrom } from '../../util/StreamUtil';
import { toLiteral } from '../../util/TermUtil';
import { HTTP, XSD } from '../../util/Vocabularies';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { ErrorHandlerArgs } from './ErrorHandler';
import { ErrorHandler } from './ErrorHandler';
import type { ResponseDescription } from './response/ResponseDescription';
/**
* Returns a simple text description of an error.
* This class is mostly a failsafe in case all other solutions fail.
*/
export class TextErrorHandler extends ErrorHandler {
private readonly showStackTrace: boolean;
public constructor(showStackTrace = false) {
super();
this.showStackTrace = showStackTrace;
}
public async handle({ error }: ErrorHandlerArgs): Promise<ResponseDescription> {
const statusCode = getStatusCode(error);
const metadata = new RepresentationMetadata('text/plain');
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
const text = typeof error.stack === 'string' && this.showStackTrace ?
`${error.stack}\n` :
`${error.name}: ${error.message}\n`;
return {
statusCode,
metadata,
data: guardedStreamFrom(text),
};
}
}

View File

@@ -1,7 +1,7 @@
import { getLoggerFor } from '../../../logging/LogUtil';
import type { HttpRequest } from '../../../server/HttpRequest';
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
import { HTTP } from '../../../util/Vocabularies';
import { SOLID_HTTP } from '../../../util/Vocabularies';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataParser } from './MetadataParser';
@@ -19,7 +19,7 @@ export class SlugParser extends MetadataParser {
throw new BadRequestHttpError('Request has multiple Slug headers');
}
this.logger.debug(`Request Slug is '${slug}'.`);
input.metadata.set(HTTP.slug, slug);
input.metadata.set(SOLID_HTTP.slug, slug);
}
}
}

View File

@@ -1,5 +1,5 @@
import { DataFactory } from 'n3';
import { HTTP } from '../../../util/Vocabularies';
import { SOLID_HTTP } from '../../../util/Vocabularies';
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
import { ResponseDescription } from './ResponseDescription';
@@ -9,7 +9,7 @@ import { ResponseDescription } from './ResponseDescription';
*/
export class CreatedResponseDescription extends ResponseDescription {
public constructor(location: ResourceIdentifier) {
const metadata = new RepresentationMetadata({ [HTTP.location]: DataFactory.namedNode(location.path) });
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(location.path) });
super(201, metadata);
}
}

View File

@@ -26,7 +26,7 @@ import {
} from '../util/PathUtil';
import { parseQuads } from '../util/QuadUtil';
import { addResourceMetadata } from '../util/ResourceUtil';
import { CONTENT_TYPE, DC, HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
import { CONTENT_TYPE, DC, SOLID_HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
import type { DataAccessor } from './accessors/DataAccessor';
import type { ResourceStore } from './ResourceStore';
@@ -407,8 +407,8 @@ export class DataAccessorBasedStore implements ResourceStore {
Promise<ResourceIdentifier> {
// Get all values needed for naming the resource
const isContainer = this.isNewContainer(metadata);
const slug = metadata.get(HTTP.slug)?.value;
metadata.removeAll(HTTP.slug);
const slug = metadata.get(SOLID_HTTP.slug)?.value;
metadata.removeAll(SOLID_HTTP.slug);
let newID: ResourceIdentifier = this.createURI(container, isContainer, slug);
@@ -439,7 +439,7 @@ export class DataAccessorBasedStore implements ResourceStore {
if (this.hasContainerType(metadata.getAll(RDF.type))) {
return true;
}
const slug = suffix ?? metadata.get(HTTP.slug)?.value;
const slug = suffix ?? metadata.get(SOLID_HTTP.slug)?.value;
return Boolean(slug && isContainerPath(slug));
}

View File

@@ -0,0 +1,37 @@
import arrayifyStream from 'arrayify-stream';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { DC, SOLID_ERROR } from '../../util/Vocabularies';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/**
* Converts an error object into quads by creating a triple for each of name/message/stack.
*/
export class ErrorToQuadConverter extends TypedRepresentationConverter {
public constructor() {
super(INTERNAL_ERROR, INTERNAL_QUADS);
}
public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
const errors = await arrayifyStream(representation.data);
if (errors.length !== 1) {
throw new InternalServerError('Only single errors are supported.');
}
const error = errors[0] as Error;
// A metadata object makes it easier to add triples due to the utility functions
const data = new RepresentationMetadata(identifier);
data.add(DC.terms.title, error.name);
data.add(DC.terms.description, error.message);
if (error.stack) {
data.add(SOLID_ERROR.terms.stack, error.stack);
}
// Update the content-type to quads
return new BasicRepresentation(data.quads(), representation.metadata, INTERNAL_QUADS, false);
}
}

View File

@@ -8,3 +8,4 @@ export const APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlenco
// Internal content types (not exposed over HTTP)
export const INTERNAL_ALL = 'internal/*';
export const INTERNAL_QUADS = 'internal/quads';
export const INTERNAL_ERROR = 'internal/error';

View File

@@ -75,16 +75,17 @@ export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
);
export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/',
'description',
'modified',
'title',
);
export const FOAF = createUriAndTermNamespace('http://xmlns.com/foaf/0.1/',
'Agent',
);
export const HTTP = createUriAndTermNamespace('urn:solid:http:',
'location',
'slug',
export const HTTP = createUriAndTermNamespace('http://www.w3.org/2011/http#',
'statusCodeNumber',
);
export const LDP = createUriAndTermNamespace('http://www.w3.org/ns/ldp#',
@@ -112,6 +113,21 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
'type',
);
export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
'oidcIssuer',
'oidcIssuerRegistrationToken',
'oidcRegistration',
);
export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
'stack',
);
export const SOLID_HTTP = createUriAndTermNamespace('urn:npm:solid:community-server:http:',
'location',
'slug',
);
export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
'preferredNamespacePrefix',
);
@@ -121,12 +137,6 @@ export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#'
'integer',
);
export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
'oidcIssuer',
'oidcIssuerRegistrationToken',
'oidcRegistration',
);
// Alias for commonly used types
export const CONTENT_TYPE = MA.format;
export const CONTENT_TYPE_TERM = MA.terms.format;

View File

@@ -1,4 +1,5 @@
import { types } from 'util';
import { HttpError } from './HttpError';
/**
* Checks if the input is an {@link Error}.
@@ -6,3 +7,20 @@ import { types } from 'util';
export function isNativeError(error: any): error is Error {
return types.isNativeError(error);
}
/**
* Asserts that the input is a native error.
* If not the input will be re-thrown.
*/
export function assertNativeError(error: any): asserts error is Error {
if (!isNativeError(error)) {
throw error;
}
}
/**
* Returns the HTTP status code corresponding to the error.
*/
export function getStatusCode(error: Error): number {
return HttpError.isInstance(error) ? error.statusCode : 500;
}