feat: Add metadata to errors

This commit is contained in:
Joachim Van Herwegen
2023-07-25 14:10:46 +02:00
parent a333412e19
commit f373dff1d7
42 changed files with 455 additions and 419 deletions

View File

@@ -1,6 +1,7 @@
import type { TLSSocket } from 'tls';
import type { HttpRequest } from '../../../server/HttpRequest';
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
import { errorTermsToMetadata } from '../../../util/errors/HttpErrorUtil';
import { InternalServerError } from '../../../util/errors/InternalServerError';
import { parseForwarded } from '../../../util/HeaderUtil';
import type { IdentifierStrategy } from '../../../util/identifiers/IdentifierStrategy';
@@ -73,7 +74,7 @@ export class OriginalUrlExtractor extends TargetExtractor {
// Check if the configured IdentifierStrategy supports the identifier
if (!this.identifierStrategy.supportsIdentifier(identifier)) {
throw new InternalServerError(`The identifier ${identifier.path} is outside the configured identifier space.`,
{ errorCode: 'E0001', details: { path: identifier.path }});
{ errorCode: 'E0001', metadata: errorTermsToMetadata({ path: identifier.path }) });
}
return identifier;

View File

@@ -1,4 +1,5 @@
import type { HttpRequest } from '../../../server/HttpRequest';
import { parseContentType } from '../../../util/HeaderUtil';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataParser } from './MetadataParser';
@@ -9,7 +10,7 @@ export class ContentTypeParser extends MetadataParser {
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
const contentType = input.request.headers['content-type'];
if (contentType) {
input.metadata.contentType = contentType;
input.metadata.contentTypeObject = parseContentType(contentType);
}
}
}

View File

@@ -1,5 +1,5 @@
import type { HttpRequest } from '../../../server/HttpRequest';
import type { AcceptHeader } from '../../../util/HeaderUtil';
import type { AcceptHeader } from '../../../util/Header';
import {
parseAccept,
parseAcceptCharset,

View File

@@ -3,13 +3,9 @@ import type {
RepresentationConverterArgs,
} from '../../../storage/conversion/RepresentationConverter';
import { INTERNAL_ERROR } from '../../../util/ContentTypes';
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
import { toLiteral } from '../../../util/TermUtil';
import { HTTP, XSD } from '../../../util/Vocabularies';
import type { PreferenceParser } from '../../input/preferences/PreferenceParser';
import { BasicRepresentation } from '../../representation/BasicRepresentation';
import type { Representation } from '../../representation/Representation';
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import type { ResponseDescription } from '../response/ResponseDescription';
import type { ErrorHandlerArgs } from './ErrorHandler';
import { ErrorHandler } from './ErrorHandler';
@@ -64,11 +60,13 @@ export class ConvertingErrorHandler extends ErrorHandler {
* Prepares the arguments used by all functions.
*/
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
const statusCode = getStatusCode(error);
const representation = this.toRepresentation(error, statusCode);
if (!this.showStackTrace) {
delete error.stack;
}
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
const identifier = { path: representation.metadata.identifier.value };
const preferences = await this.preferenceParser.handle({ request });
return { statusCode, conversionArgs: { identifier, representation, preferences }};
return { statusCode: error.statusCode, conversionArgs: { identifier, representation, preferences }};
}
/**
@@ -81,20 +79,4 @@ export class ConvertingErrorHandler extends ErrorHandler {
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

@@ -1,9 +1,10 @@
import type { HttpRequest } from '../../../server/HttpRequest';
import type { HttpError } from '../../../util/errors/HttpError';
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
import type { ResponseDescription } from '../response/ResponseDescription';
export interface ErrorHandlerArgs {
error: Error;
error: HttpError;
request: HttpRequest;
}

View File

@@ -1,10 +1,6 @@
import { getLoggerFor } from '../../../logging/LogUtil';
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
import { guardedStreamFrom } from '../../../util/StreamUtil';
import { toLiteral } from '../../../util/TermUtil';
import { HTTP, XSD } from '../../../util/Vocabularies';
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import type { ResponseDescription } from '../response/ResponseDescription';
import type { ErrorHandlerArgs } from './ErrorHandler';
import { ErrorHandler } from './ErrorHandler';
@@ -32,17 +28,15 @@ export class SafeErrorHandler extends ErrorHandler {
this.logger.debug(`Recovering from error handler failure: ${createErrorMessage(error)}`);
}
const { error } = input;
const statusCode = getStatusCode(error);
const metadata = new RepresentationMetadata('text/plain');
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
error.metadata.contentType = 'text/plain';
const text = typeof error.stack === 'string' && this.showStackTrace ?
`${error.stack}\n` :
`${error.name}: ${error.message}\n`;
return {
statusCode,
metadata,
statusCode: error.statusCode,
metadata: error.metadata,
data: guardedStreamFrom(text),
};
}

View File

@@ -1,7 +1,6 @@
import { DataFactory } from 'n3';
import type { RedirectHttpError } from '../../../util/errors/RedirectHttpError';
import { SOLID_HTTP } from '../../../util/Vocabularies';
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { ResponseDescription } from './ResponseDescription';
/**
@@ -9,7 +8,7 @@ import { ResponseDescription } from './ResponseDescription';
*/
export class RedirectResponseDescription extends ResponseDescription {
public constructor(error: RedirectHttpError) {
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(error.location) });
super(error.statusCode, metadata);
error.metadata.set(SOLID_HTTP.terms.location, DataFactory.namedNode(error.location));
super(error.statusCode, error.metadata);
}
}

View File

@@ -1,8 +1,7 @@
import { DataFactory, Store } from 'n3';
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { ContentType, parseContentType } from '../../util/HeaderUtil';
import { ContentType, SIMPLE_MEDIA_RANGE } from '../../util/Header';
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
import type { ResourceIdentifier } from './ResourceIdentifier';
@@ -67,18 +66,18 @@ export class RepresentationMetadata {
* @param identifier - Identifier of the resource relevant to this metadata.
* @param contentType - Override for the content type of the representation.
*/
public constructor(identifier?: MetadataIdentifier, contentType?: string);
public constructor(identifier?: MetadataIdentifier, contentType?: string | ContentType);
/**
* @param metadata - Starts as a copy of the input metadata.
* @param contentType - Override for the content type of the representation.
*/
public constructor(metadata?: RepresentationMetadata, contentType?: string);
public constructor(metadata?: RepresentationMetadata, contentType?: string | ContentType);
/**
* @param contentType - The content type of the representation.
*/
public constructor(contentType?: string);
public constructor(contentType?: string | ContentType);
/**
* @param metadata - Metadata values (defaulting to content type if a string)
@@ -86,8 +85,8 @@ export class RepresentationMetadata {
public constructor(metadata?: RepresentationMetadata | MetadataRecord | string);
public constructor(
input?: MetadataIdentifier | RepresentationMetadata | MetadataRecord | string,
overrides?: MetadataRecord | string,
input?: MetadataIdentifier | RepresentationMetadata | MetadataRecord | ContentType | string,
overrides?: MetadataRecord | string | ContentType,
) {
this.store = new Store();
if (isResourceIdentifier(input)) {
@@ -105,6 +104,8 @@ export class RepresentationMetadata {
if (overrides) {
if (typeof overrides === 'string') {
this.contentType = overrides;
} else if (overrides instanceof ContentType) {
this.contentTypeObject = overrides;
} else {
this.setOverrides(overrides);
}
@@ -313,7 +314,8 @@ export class RepresentationMetadata {
}
if (terms.length > 1) {
this.logger.error(`Multiple results for ${predicate.value}`);
throw new InternalServerError(
// We can not use an `InternalServerError` here as otherwise errors and metadata files would depend on each other
throw new Error(
`Multiple results for ${predicate.value}`,
);
}
@@ -344,7 +346,16 @@ export class RepresentationMetadata {
}
if (typeof input === 'string') {
input = parseContentType(input);
// Simple check to estimate if this is a simple content type.
// If not, mention that the `contentTypeObject` should be used instead.
// Not calling `parseContentType` here as that would cause a dependency loop with `HttpError`.
if (!SIMPLE_MEDIA_RANGE.test(input)) {
// Not using an HttpError as HttpError depends on metadata
throw new Error(
'Only simple content types can be set by string. Use the `contentTypeObject` function for complexer types.',
);
}
input = new ContentType(input);
}
for (const [ key, value ] of Object.entries(input.parameters)) {