feat: Add better support for non-native errors

This commit is contained in:
Joachim Van Herwegen
2021-06-07 15:54:28 +02:00
parent cefc866109
commit 7cfb87e516
25 changed files with 111 additions and 98 deletions

View File

@@ -5,7 +5,7 @@ import type { RepresentationPreferences } from '../ldp/representation/Representa
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpHandlerInput } from '../server/HttpHandler';
import { HttpHandler } from '../server/HttpHandler';
import { assertNativeError, isNativeError } from '../util/errors/ErrorUtil';
import { assertError, createErrorMessage } from '../util/errors/ErrorUtil';
import type { IdentityProviderFactory } from './IdentityProviderFactory';
import type { InteractionHttpHandler } from './interaction/InteractionHttpHandler';
import type { InteractionPolicy } from './interaction/InteractionPolicy';
@@ -55,7 +55,7 @@ export class IdentityProviderHttpHandler extends HttpHandler {
try {
this.provider = await this.providerFactory.createProvider(this.interactionPolicy);
} catch (err: unknown) {
this.logger.error(`Failed to create Provider: ${isNativeError(err) ? err.message : 'Unknown error'}`);
this.logger.error(`Failed to create Provider: ${createErrorMessage(err)}`);
throw err;
}
}
@@ -75,7 +75,7 @@ export class IdentityProviderHttpHandler extends HttpHandler {
try {
await this.interactionHttpHandler.handle({ ...input, provider });
} catch (error: unknown) {
assertNativeError(error);
assertError(error);
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
const result = await this.errorHandler.handleSafe({ error, preferences });
await this.responseWriter.handleSafe({ response: input.response, result });

View File

@@ -1,5 +1,5 @@
import assert from 'assert';
import { isNativeError } from '../../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
import { HttpError } from '../../../util/errors/HttpError';
import { IdpInteractionError } from '../util/IdpInteractionError';
@@ -18,10 +18,8 @@ export function throwIdpInteractionError(error: unknown, prefilled: Record<strin
}
} else if (HttpError.isInstance(error)) {
throw new IdpInteractionError(error.statusCode, error.message, prefilled);
} else if (isNativeError(error)) {
throw new IdpInteractionError(500, error.message, prefilled);
} else {
throw new IdpInteractionError(500, 'Unknown Error', prefilled);
throw new IdpInteractionError(500, createErrorMessage(error), prefilled);
}
}

View File

@@ -3,7 +3,7 @@ import { getLoggerFor } from '../../../../logging/LogUtil';
import type { HttpHandlerInput } from '../../../../server/HttpHandler';
import { HttpHandler } from '../../../../server/HttpHandler';
import type { RenderHandler } from '../../../../server/util/RenderHandler';
import { isNativeError } from '../../../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../../../util/errors/ErrorUtil';
import { getFormDataRequestBody } from '../../util/FormDataUtil';
import { assertPassword } from '../EmailPasswordUtil';
import type { AccountStore } from '../storage/AccountStore';
@@ -53,11 +53,10 @@ export class ResetPasswordHandler extends HttpHandler {
},
});
} catch (err: unknown) {
const errorMessage: string = isNativeError(err) ? err.message : 'An unknown error occurred';
await this.renderHandler.handleSafe({
response: input.response,
props: {
errorMessage,
errorMessage: createErrorMessage(err),
recordId: prefilledRecordId,
},
});

View File

@@ -1,6 +1,6 @@
import type { HttpHandler } from '../../../server/HttpHandler';
import { RouterHandler } from '../../../server/util/RouterHandler';
import { isNativeError } from '../../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
import type { InteractionHttpHandlerInput } from '../InteractionHttpHandler';
import { IdpInteractionError } from './IdpInteractionError';
import type { IdpRenderHandler } from './IdpRenderHandler';
@@ -35,9 +35,8 @@ export class IdpRouteController extends RouterHandler {
try {
await this.handler.handleSafe(input);
} catch (err: unknown) {
const errorMessage = isNativeError(err) ? err.message : 'An unknown error occurred';
const prefilled = IdpInteractionError.isInstance(err) ? err.prefilled : {};
await this.render(input, errorMessage, prefilled);
await this.render(input, createErrorMessage(err), prefilled);
}
}
}

View File

@@ -2,7 +2,7 @@ import fetch from '@rdfjs/fetch';
import type { DatasetResponse } from '@rdfjs/fetch-lite';
import type { Dataset } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { isNativeError } from '../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
const logger = getLoggerFor('FetchUtil');
@@ -14,17 +14,16 @@ export async function fetchDataset(url: string): Promise<Dataset> {
try {
rawResponse = (await fetch(url)) as DatasetResponse<Dataset>;
} catch (err: unknown) {
const errorMessage = `Cannot fetch ${url}: ${isNativeError(err) ? err.message : 'Unknown error'}`;
logger.error(errorMessage);
throw new Error(errorMessage);
logger.error(`Cannot fetch ${url}: ${createErrorMessage(err)}`);
throw new Error(`Cannot fetch ${url}`);
}
let dataset: Dataset;
try {
dataset = await rawResponse.dataset();
} catch (err: unknown) {
const errorMessage = `Could not parse RDF in ${url}: ${isNativeError(err) ? err.message : 'Unknown error'}`;
logger.error(errorMessage);
throw new Error(errorMessage);
logger.error(`Could not parse RDF in ${url}: ${createErrorMessage(err)}`);
// Keeping the error message the same to prevent leaking possible information about intranet
throw new Error(`Cannot fetch ${url}`);
}
return dataset;
}

View File

@@ -6,7 +6,7 @@ import type { HttpHandlerInput } from '../server/HttpHandler';
import { HttpHandler } from '../server/HttpHandler';
import type { HttpRequest } from '../server/HttpRequest';
import type { HttpResponse } from '../server/HttpResponse';
import { assertNativeError } from '../util/errors/ErrorUtil';
import { assertError } from '../util/errors/ErrorUtil';
import type { ErrorHandler } from './http/ErrorHandler';
import type { RequestParser } from './http/RequestParser';
import type { ResponseDescription } from './http/response/ResponseDescription';
@@ -103,7 +103,7 @@ export class AuthenticatedLdpHandler extends HttpHandler {
try {
writeData = { response: input.response, result: await this.runHandlers(input.request) };
} catch (error: unknown) {
assertNativeError(error);
assertError(error);
// We don't know the preferences yet at this point
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
const result = await this.errorHandler.handleSafe({ error, preferences });
@@ -129,7 +129,7 @@ export class AuthenticatedLdpHandler extends HttpHandler {
try {
return await this.handleOperation(request, operation);
} catch (error: unknown) {
assertNativeError(error);
assertError(error);
return await this.errorHandler.handleSafe({ error, preferences: operation.preferences });
}
}

View File

@@ -1,7 +1,6 @@
import { getLoggerFor } from '../../logging/LogUtil';
import type { HttpResponse } from '../../server/HttpResponse';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { isNativeError } from '../../util/errors/ErrorUtil';
import { isInternalContentType } from '../../storage/conversion/ConversionUtil';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { pipeSafely } from '../../util/StreamUtil';
import type { MetadataWriter } from './metadata/MetadataWriter';
@@ -20,9 +19,10 @@ export class BasicResponseWriter extends ResponseWriter {
this.metadataWriter = metadataWriter;
}
public async canHandle(input: { response: HttpResponse; result: ResponseDescription | Error }): Promise<void> {
if (isNativeError(input.result) || input.result.metadata?.contentType === INTERNAL_QUADS) {
throw new NotImplementedHttpError('Only successful binary responses are supported');
public async canHandle(input: { response: HttpResponse; result: ResponseDescription }): Promise<void> {
const contentType = input.result.metadata?.contentType;
if (isInternalContentType(contentType)) {
throw new NotImplementedHttpError(`Cannot serialize the internal content type ${contentType}`);
}
}

View File

@@ -3,7 +3,7 @@ import { translate } from 'sparqlalgebrajs';
import { getLoggerFor } from '../../logging/LogUtil';
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { isNativeError } from '../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import { guardedStreamFrom, readableToString } from '../../util/StreamUtil';
import type { BodyParserArgs } from './BodyParser';
@@ -29,10 +29,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value });
} catch (error: unknown) {
this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
if (isNativeError(error)) {
throw new BadRequestHttpError(error.message);
}
throw new BadRequestHttpError();
throw new BadRequestHttpError(createErrorMessage(error));
}
// Prevent body from being requested again

View File

@@ -4,7 +4,7 @@ import { createServer as createHttpServer } from 'http';
import { createServer as createHttpsServer } from 'https';
import { URL } from 'url';
import { getLoggerFor } from '../logging/LogUtil';
import { isNativeError } from '../util/errors/ErrorUtil';
import { isError } from '../util/errors/ErrorUtil';
import { guardStream } from '../util/GuardedStream';
import type { HttpHandler } from './HttpHandler';
import type { HttpServerFactory } from './HttpServerFactory';
@@ -67,8 +67,8 @@ export class BaseHttpServerFactory implements HttpServerFactory {
await this.handler.handleSafe({ request: guardStream(request), response });
} catch (error: unknown) {
let errMsg: string;
if (!isNativeError(error)) {
errMsg = 'Unknown error.\n';
if (!isError(error)) {
errMsg = `Unknown error: ${error}.\n`;
} else if (this.options.showStackTrace && error.stack) {
errMsg = `${error.stack}\n`;
} else {

View File

@@ -11,7 +11,7 @@ import { getLoggerFor } from '../logging/LogUtil';
import { INTERNAL_QUADS } from '../util/ContentTypes';
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
import { isNativeError } from '../util/errors/ErrorUtil';
import { createErrorMessage } from '../util/errors/ErrorUtil';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
@@ -344,10 +344,7 @@ export class DataAccessorBasedStore implements ResourceStore {
quads = await parseQuads(representation.data, { format: contentType, baseIRI: identifier.value });
}
} catch (error: unknown) {
if (isNativeError(error)) {
throw new BadRequestHttpError(`Can only create containers with RDF data. ${error.message}`);
}
throw error;
throw new BadRequestHttpError(`Can only create containers with RDF data. ${createErrorMessage(error)}`);
}
// Solid, §5.3: "Servers MUST NOT allow HTTP POST, PUT and PATCH to update a containers containment triples;
@@ -481,8 +478,7 @@ export class DataAccessorBasedStore implements ResourceStore {
deleted.push(identifier);
} catch (error: unknown) {
if (!NotFoundHttpError.isInstance(error)) {
const errorMsg = isNativeError(error) ? error.message : error;
this.logger.error(`Problem deleting auxiliary resource ${identifier.path}: ${errorMsg}`);
this.logger.error(`Error deleting auxiliary resource ${identifier.path}: ${createErrorMessage(error)}`);
}
}
}));

View File

@@ -19,7 +19,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti
import { getLoggerFor } from '../../logging/LogUtil';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { isNativeError } from '../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
@@ -300,9 +300,7 @@ export class SparqlDataAccessor implements DataAccessor {
try {
return guardStream(await this.fetcher.fetchTriples(this.endpoint, query));
} catch (error: unknown) {
if (isNativeError(error)) {
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`);
}
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${createErrorMessage(error)}`);
throw error;
}
}
@@ -317,9 +315,7 @@ export class SparqlDataAccessor implements DataAccessor {
try {
return await this.fetcher.fetchUpdate(this.endpoint, query);
} catch (error: unknown) {
if (isNativeError(error)) {
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`);
}
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${createErrorMessage(error)}`);
throw error;
}
}

View File

@@ -156,3 +156,15 @@ export function matchesMediaType(mediaA: string, mediaB: string): boolean {
}
return subTypeA === subTypeB;
}
/**
* Checks if the given content type is an internal content type such as internal/quads.
* Response will be `false` if the input type is undefined.
*
* Do not use this for media ranges.
*
* @param contentType - Type to check.
*/
export function isInternalContentType(contentType?: string): boolean {
return typeof contentType !== 'undefined' && matchesMediaType(contentType, INTERNAL_ALL);
}

View File

@@ -4,20 +4,28 @@ import { HttpError } from './HttpError';
/**
* Checks if the input is an {@link Error}.
*/
export function isNativeError(error: any): error is Error {
return types.isNativeError(error);
export function isError(error: any): error is Error {
return types.isNativeError(error) ||
(error &&
typeof error.name === 'string' &&
typeof error.message === 'string' &&
(typeof error.stack === 'undefined' || typeof error.stack === 'string'));
}
/**
* 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)) {
export function assertError(error: unknown): asserts error is Error {
if (!isError(error)) {
throw error;
}
}
export function createErrorMessage(error: unknown): string {
return isError(error) ? error.message : `Unknown error: ${error}`;
}
/**
* Returns the HTTP status code corresponding to the error.
*/

View File

@@ -1,4 +1,4 @@
import { isNativeError } from './ErrorUtil';
import { isError } from './ErrorUtil';
/**
* A class for all errors that could be thrown by Solid.
@@ -21,6 +21,6 @@ export class HttpError extends Error {
}
public static isInstance(error: any): error is HttpError {
return isNativeError(error) && typeof (error as any).statusCode === 'number';
return isError(error) && typeof (error as any).statusCode === 'number';
}
}

View File

@@ -1,6 +1,6 @@
import { getLoggerFor } from '../../logging/LogUtil';
import { BadRequestHttpError } from '../errors/BadRequestHttpError';
import { isNativeError } from '../errors/ErrorUtil';
import { createErrorMessage } from '../errors/ErrorUtil';
import { HttpError } from '../errors/HttpError';
import { InternalServerError } from '../errors/InternalServerError';
import type { AsyncHandler } from './AsyncHandler';
@@ -87,10 +87,8 @@ export class WaterfallHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
} catch (error: unknown) {
if (HttpError.isInstance(error)) {
errors.push(error);
} else if (isNativeError(error)) {
errors.push(new InternalServerError(error.message));
} else {
errors.push(new InternalServerError('Unknown error'));
errors.push(new InternalServerError(createErrorMessage(error)));
}
}
}