Repository: solid/community-server (ts)

Issue:
Logging non-native errors
I've been playing with the ldp registration and I was tripped up for a bit because I was getting an "Unknown error". After looking at the source, I found that error messages [are ignored for non-native errors](https://github.com/solid/community-server/blob/main/src/identity/util/FetchUtil.ts#L17), and that's why I was getting this message. Eventually I found the problem was caused by a self-signed certificate, because I forgot to set the `NODE_TLS_REJECT_UNAUTHORIZED` variable.

I'm not sure where the error came from, but logging it to console I saw it was a `FetchError` and it had a `message` property, but it didn't pass the `isNativeError` check and that's why it wasn't logged properly.

I wonder, wouldn't it be better to check if `message` exists within an object, instead of seeing if it's a native error or not? I know [there are some issues](https://stackoverflow.com/questions/41102060/typescript-extending-error-class) extending native errors, and that's why some libraries may throw non-native errors.

At the very least, I expected to see more information using the debug logging level, but in the end I couldn't solve it without going into the source code.

Pull request:
feat: Add better support for non-native errors
Closes #763.

The main changes are in ErrorUtil, most of the rest is based on those changes.

I also fixed some inconsistenties here and there in how we handled non-native errors. Sometimes they were re-thrown, sometimes a new error object got made instead. This PR should make everything around these a lot more consistent.

Patch:
diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts
index b3453599b..4bd3405f7 100644
--- a/src/identity/IdentityProviderHttpHandler.ts
+++ b/src/identity/IdentityProviderHttpHandler.ts
@@ -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 });
diff --git a/src/identity/interaction/email-password/EmailPasswordUtil.ts b/src/identity/interaction/email-password/EmailPasswordUtil.ts
index 3eab2dfd0..f4b555002 100644
--- a/src/identity/interaction/email-password/EmailPasswordUtil.ts
+++ b/src/identity/interaction/email-password/EmailPasswordUtil.ts
@@ -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);
   }
 }
 
diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
index c19aedee0..6135f8f3c 100644
--- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
+++ b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts
@@ -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,
         },
       });
diff --git a/src/identity/interaction/util/IdpRouteController.ts b/src/identity/interaction/util/IdpRouteController.ts
index aed56f47c..863bcc4f8 100644
--- a/src/identity/interaction/util/IdpRouteController.ts
+++ b/src/identity/interaction/util/IdpRouteController.ts
@@ -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);
       }
     }
   }
diff --git a/src/identity/util/FetchUtil.ts b/src/identity/util/FetchUtil.ts
index 2b92af2be..705f74982 100644
--- a/src/identity/util/FetchUtil.ts
+++ b/src/identity/util/FetchUtil.ts
@@ -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;
 }
diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts
index dd0114597..976854776 100644
--- a/src/ldp/AuthenticatedLdpHandler.ts
+++ b/src/ldp/AuthenticatedLdpHandler.ts
@@ -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 });
     }
   }
diff --git a/src/ldp/http/BasicResponseWriter.ts b/src/ldp/http/BasicResponseWriter.ts
index f71536ba1..679808c9f 100644
--- a/src/ldp/http/BasicResponseWriter.ts
+++ b/src/ldp/http/BasicResponseWriter.ts
@@ -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}`);
     }
   }
 
diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts
index cf8d60f11..b9028a6ff 100644
--- a/src/ldp/http/SparqlUpdateBodyParser.ts
+++ b/src/ldp/http/SparqlUpdateBodyParser.ts
@@ -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
diff --git a/src/server/BaseHttpServerFactory.ts b/src/server/BaseHttpServerFactory.ts
index 3b0cd048d..d840f57cd 100644
--- a/src/server/BaseHttpServerFactory.ts
+++ b/src/server/BaseHttpServerFactory.ts
@@ -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 {
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 439b0713c..adf4dbdba 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -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 container’s 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)}`);
         }
       }
     }));
diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts
index 91d99989a..9ef5f7302 100644
--- a/src/storage/accessors/SparqlDataAccessor.ts
+++ b/src/storage/accessors/SparqlDataAccessor.ts
@@ -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;
     }
   }
diff --git a/src/storage/conversion/ConversionUtil.ts b/src/storage/conversion/ConversionUtil.ts
index 45953995d..0ded29faa 100644
--- a/src/storage/conversion/ConversionUtil.ts
+++ b/src/storage/conversion/ConversionUtil.ts
@@ -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);
+}
diff --git a/src/util/errors/ErrorUtil.ts b/src/util/errors/ErrorUtil.ts
index 3d2dbce8b..84e94716e 100644
--- a/src/util/errors/ErrorUtil.ts
+++ b/src/util/errors/ErrorUtil.ts
@@ -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.
  */
diff --git a/src/util/errors/HttpError.ts b/src/util/errors/HttpError.ts
index 9d213dc64..b5b206aa0 100644
--- a/src/util/errors/HttpError.ts
+++ b/src/util/errors/HttpError.ts
@@ -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';
   }
 }
diff --git a/src/util/handlers/WaterfallHandler.ts b/src/util/handlers/WaterfallHandler.ts
index cfd3bd7e0..d87a718fb 100644
--- a/src/util/handlers/WaterfallHandler.ts
+++ b/src/util/handlers/WaterfallHandler.ts
@@ -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)));
         }
       }
     }


Test patch:
diff --git a/test/unit/identity/interaction/email-password/EmailPasswordUtil.test.ts b/test/unit/identity/interaction/email-password/EmailPasswordUtil.test.ts
index e2e6f1999..096140f70 100644
--- a/test/unit/identity/interaction/email-password/EmailPasswordUtil.test.ts
+++ b/test/unit/identity/interaction/email-password/EmailPasswordUtil.test.ts
@@ -45,7 +45,7 @@ describe('EmailPasswordUtil', (): void => {
       const error = 'Error!';
       expect((): never => throwIdpInteractionError(error, prefilled)).toThrow(expect.objectContaining({
         statusCode: 500,
-        message: 'Unknown Error',
+        message: 'Unknown error: Error!',
         prefilled,
       }));
     });
diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
index a8c49dc7a..7effde8a5 100644
--- a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
+++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts
@@ -86,7 +86,7 @@ describe('A ResetPasswordHandler', (): void => {
   });
 
   it('has a default error for non-native errors.', async(): Promise<void> => {
-    const errorMessage = 'An unknown error occurred';
+    const errorMessage = 'Unknown error: not native';
     request = createPostFormRequest({ recordId, password: 'password!', confirmPassword: 'password!' });
     (accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native');
     await expect(handler.handle({ request, response })).resolves.toBeUndefined();
diff --git a/test/unit/identity/interaction/util/IdpRouteController.test.ts b/test/unit/identity/interaction/util/IdpRouteController.test.ts
index 49ebaec99..b72264967 100644
--- a/test/unit/identity/interaction/util/IdpRouteController.test.ts
+++ b/test/unit/identity/interaction/util/IdpRouteController.test.ts
@@ -74,7 +74,7 @@ describe('An IdpRouteController', (): void => {
     expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
     expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
       response,
-      props: { errorMessage: 'An unknown error occurred', prefilled: {}},
+      props: { errorMessage: 'Unknown error: apple!', prefilled: {}},
     });
   });
 
diff --git a/test/unit/identity/util/FetchUtil.test.ts b/test/unit/identity/util/FetchUtil.test.ts
index 83cd78246..4165e6626 100644
--- a/test/unit/identity/util/FetchUtil.test.ts
+++ b/test/unit/identity/util/FetchUtil.test.ts
@@ -22,19 +22,13 @@ describe('FetchUtil', (): void => {
 
     it('errors if there was an issue fetching.', async(): Promise<void> => {
       fetchMock.mockRejectedValueOnce(new Error('Invalid webId!'));
-      await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}: Invalid webId!`);
+      await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}`);
       expect(fetchMock).toHaveBeenCalledWith(url);
-
-      fetchMock.mockRejectedValueOnce('apple');
-      await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}: Unknown error`);
     });
 
     it('errors if there was an issue parsing the returned RDF.', async(): Promise<void> => {
       (datasetResponse.dataset as jest.Mock).mockRejectedValueOnce(new Error('Invalid RDF!'));
-      await expect(fetchDataset(url)).rejects.toThrow(`Could not parse RDF in ${url}: Invalid RDF!`);
-
-      (datasetResponse.dataset as jest.Mock).mockRejectedValueOnce('apple');
-      await expect(fetchDataset(url)).rejects.toThrow(`Could not parse RDF in ${url}: Unknown error`);
+      await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}`);
     });
 
     it('returns the resulting Dataset.', async(): Promise<void> => {
diff --git a/test/unit/ldp/http/BasicResponseWriter.test.ts b/test/unit/ldp/http/BasicResponseWriter.test.ts
index 41b01673b..91c07106b 100644
--- a/test/unit/ldp/http/BasicResponseWriter.test.ts
+++ b/test/unit/ldp/http/BasicResponseWriter.test.ts
@@ -25,12 +25,13 @@ describe('A BasicResponseWriter', (): void => {
   });
 
   it('requires the input to be a binary ResponseDescription.', async(): Promise<void> => {
-    await expect(writer.canHandle({ response, result: new Error('error') }))
-      .rejects.toThrow(NotImplementedHttpError);
     const metadata = new RepresentationMetadata(INTERNAL_QUADS);
     await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
       .rejects.toThrow(NotImplementedHttpError);
-    await expect(writer.canHandle({ response, result }))
+    metadata.contentType = 'text/turtle';
+    await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
+      .resolves.toBeUndefined();
+    await expect(writer.canHandle({ response, result: { statusCode: 201 }}))
       .resolves.toBeUndefined();
   });
 
diff --git a/test/unit/server/BaseHttpServerFactory.test.ts b/test/unit/server/BaseHttpServerFactory.test.ts
index a49ee6c64..5e1894a2a 100644
--- a/test/unit/server/BaseHttpServerFactory.test.ts
+++ b/test/unit/server/BaseHttpServerFactory.test.ts
@@ -88,7 +88,7 @@ describe('A BaseHttpServerFactory', (): void => {
       handler.handleSafe.mockRejectedValueOnce('apple');
 
       const res = await request(server).get('/').expect(500);
-      expect(res.text).toContain('Unknown error.');
+      expect(res.text).toContain('Unknown error: apple.');
     });
   });
 
@@ -107,7 +107,7 @@ describe('A BaseHttpServerFactory', (): void => {
       server.close();
     });
 
-    it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
+    it('does not print the stack if that option is disabled.', async(): Promise<void> => {
       const error = new Error('dummyError');
       handler.handleSafe.mockRejectedValueOnce(error);
 
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 75d423958..9b8f2d3b8 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -20,7 +20,6 @@ import { NotImplementedHttpError } from '../../../src/util/errors/NotImplemented
 import type { Guarded } from '../../../src/util/GuardedStream';
 import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
 import { trimTrailingSlashes } from '../../../src/util/PathUtil';
-import * as quadUtil from '../../../src/util/QuadUtil';
 import { guardedStreamFrom } from '../../../src/util/StreamUtil';
 import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
 import quad = DataFactory.quad;
@@ -236,16 +235,6 @@ describe('A DataAccessorBasedStore', (): void => {
       await expect(store.addResource(resourceID, representation)).rejects.toThrow(BadRequestHttpError);
     });
 
-    it('passes the result along if the MetadataController throws a non-Error.', async(): Promise<void> => {
-      const resourceID = { path: root };
-      const mock = jest.spyOn(quadUtil, 'parseQuads').mockImplementationOnce(async(): Promise<any> => {
-        throw 'apple';
-      });
-      representation.metadata.add(RDF.type, LDP.terms.Container);
-      await expect(store.addResource(resourceID, representation)).rejects.toBe('apple');
-      mock.mockRestore();
-    });
-
     it('can write resources.', async(): Promise<void> => {
       const resourceID = { path: root };
       representation.metadata.removeAll(RDF.type);
@@ -584,7 +573,7 @@ describe('A DataAccessorBasedStore', (): void => {
       expect(accessor.data[`${root}resource.dummy`]).not.toBeUndefined();
       expect(logger.error).toHaveBeenCalledTimes(1);
       expect(logger.error).toHaveBeenLastCalledWith(
-        'Problem deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',
+        'Error deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',
       );
     });
 
@@ -607,7 +596,7 @@ describe('A DataAccessorBasedStore', (): void => {
       expect(accessor.data[`${root}resource.dummy`]).not.toBeUndefined();
       expect(logger.error).toHaveBeenCalledTimes(1);
       expect(logger.error).toHaveBeenLastCalledWith(
-        'Problem deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',
+        'Error deleting auxiliary resource http://test.com/resource.dummy: Unknown error: auxiliary error!',
       );
     });
   });
diff --git a/test/unit/storage/conversion/ConversionUtil.test.ts b/test/unit/storage/conversion/ConversionUtil.test.ts
index 379c99297..1e66ce351 100644
--- a/test/unit/storage/conversion/ConversionUtil.test.ts
+++ b/test/unit/storage/conversion/ConversionUtil.test.ts
@@ -4,7 +4,7 @@ import {
   getBestPreference,
   getConversionTarget,
   getTypeWeight,
-  getWeightedPreferences,
+  getWeightedPreferences, isInternalContentType,
   matchesMediaPreferences,
   matchesMediaType,
 } from '../../../../src/storage/conversion/ConversionUtil';
@@ -144,4 +144,13 @@ describe('ConversionUtil', (): void => {
       expect(matchesMediaType('text/plain', 'text/turtle')).toBeFalsy();
     });
   });
+
+  describe('#isInternalContentType', (): void => {
+    it('only returns true on internal types.', async(): Promise<void> => {
+      expect(isInternalContentType('internal/quads')).toBeTruthy();
+
+      expect(isInternalContentType()).toBeFalsy();
+      expect(isInternalContentType('text/turtle')).toBeFalsy();
+    });
+  });
 });
diff --git a/test/unit/util/errors/ErrorUtil.test.ts b/test/unit/util/errors/ErrorUtil.test.ts
index 8214c699e..be242152a 100644
--- a/test/unit/util/errors/ErrorUtil.test.ts
+++ b/test/unit/util/errors/ErrorUtil.test.ts
@@ -1,24 +1,42 @@
-import { assertNativeError, getStatusCode, isNativeError } from '../../../../src/util/errors/ErrorUtil';
+import { assertError, createErrorMessage, getStatusCode, isError } from '../../../../src/util/errors/ErrorUtil';
 import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
 
 describe('ErrorUtil', (): void => {
-  describe('#isNativeError', (): void => {
+  describe('#isError', (): void => {
     it('returns true on native errors.', async(): Promise<void> => {
-      expect(isNativeError(new Error('error'))).toBe(true);
+      expect(isError(new Error('error'))).toBe(true);
+    });
+
+    it('returns true on error-like objects.', async(): Promise<void> => {
+      expect(isError({ name: 'name', message: 'message', stack: 'stack' })).toBe(true);
+    });
+
+    it('returns true on errors without a stack.', async(): Promise<void> => {
+      expect(isError({ name: 'name', message: 'message' })).toBe(true);
     });
 
     it('returns false on other values.', async(): Promise<void> => {
-      expect(isNativeError('apple')).toBe(false);
+      expect(isError('apple')).toBe(false);
     });
   });
 
-  describe('#assertNativeError', (): void => {
+  describe('#assertError', (): void => {
     it('returns undefined on native errors.', async(): Promise<void> => {
-      expect(assertNativeError(new Error('error'))).toBeUndefined();
+      expect(assertError(new Error('error'))).toBeUndefined();
     });
 
     it('throws on other values.', async(): Promise<void> => {
-      expect((): void => assertNativeError('apple')).toThrow('apple');
+      expect((): void => assertError('apple')).toThrow('apple');
+    });
+  });
+
+  describe('#createErrorMessage', (): void => {
+    it('returns the given message for normal Errors.', async(): Promise<void> => {
+      expect(createErrorMessage(new Error('error msg'))).toBe('error msg');
+    });
+
+    it('tries to put the object in a string .', async(): Promise<void> => {
+      expect(createErrorMessage('apple')).toBe('Unknown error: apple');
     });
   });
 
diff --git a/test/unit/util/handlers/WaterfallHandler.test.ts b/test/unit/util/handlers/WaterfallHandler.test.ts
index 1bb20f4b2..3a8f3eeb3 100644
--- a/test/unit/util/handlers/WaterfallHandler.test.ts
+++ b/test/unit/util/handlers/WaterfallHandler.test.ts
@@ -53,7 +53,7 @@ describe('A WaterfallHandler', (): void => {
       };
       const handler = new WaterfallHandler([ handlerFalse, handlerFalse ]);
 
-      await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error, Unknown error]');
+      await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error: apple, Unknown error: apple]');
     });
 
     it('handles data if a handler supports it.', async(): Promise<void> => {