Repository: solid/community-server
Pull request #3 (2020-05-28 09:07:11)

Add AuthenticatedLdpHandler
This one is pretty big/important since it's about the core class that is going to tie most of it together.

The flow of how it handles everything is straightforward (the idea is that it would receive a bunch of composite handlers thereby supporting multiple solutions).

The main point of discussion is probably how errors are integrated. The advantages of this solution for errors is that it both hides what the HTTP response will be from whoever throws the error, but also allows flexibility for later on extending the available errors. 

The idea would then be that the ResponseWriter takes both an operation and an optional error as input. If there is an error it responds based on that, if not it constructs a response based on what the operation looks like (e.g. 201 for a POST with the correct Location header). Since it is an AsyncHandler we could even have 2 ResponeWriters, one for succesful operations and one for errors.

joachimvh: I think I covered everything in the new push (except the lint stuff as mentioned). The main point of potential discussion is probably how the CompositeAsyncHandler handles errors now. Not sure what the best option there was.
joachimvh: This small change ended up being a bit bigger than expected. Now handlers have a `handleSafe` function which first calls `canHandle`. This has the added advantage that `CompositeAsyncHandler` can overwrite that function to be more efficient.

Patch:
diff --git a/jest.config.js b/jest.config.js
index 98aa9b990..345d28032 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -7,18 +7,22 @@ module.exports = {
   "transform": {
     "^.+\\.ts$": "ts-jest"
   },
-  "testRegex": "/test/.*\\.ts$",
+  "testRegex": "/test/.*\\.test\\.ts$",
   "moduleFileExtensions": [
     "ts",
     "js"
   ],
   "testEnvironment": "node",
   "collectCoverage": true,
-  // either we don't build the test files (but then eslint needs a separate tsconfig) or we do this
-  "testPathIgnorePatterns": [
-    ".*\\.d\\.ts"
-  ],
   "coveragePathIgnorePatterns": [
     "/node_modules/"
-  ]
+  ],
+  "coverageThreshold": {
+    "./src": {
+      "branches": 100,
+      "functions": 100,
+      "lines": 100,
+      "statements": 100
+    }
+  }
 };
diff --git a/package-lock.json b/package-lock.json
index e22fe56fa..ed93f0967 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5798,9 +5798,9 @@
       }
     },
     "ts-jest": {
-      "version": "25.5.1",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.1.tgz",
-      "integrity": "sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==",
+      "version": "26.0.0",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.0.0.tgz",
+      "integrity": "sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
@@ -5810,9 +5810,23 @@
         "lodash.memoize": "4.x",
         "make-error": "1.x",
         "micromatch": "4.x",
-        "mkdirp": "0.x",
-        "semver": "6.x",
+        "mkdirp": "1.x",
+        "semver": "7.x",
         "yargs-parser": "18.x"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+          "dev": true
+        },
+        "semver": {
+          "version": "7.3.2",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
+          "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
+          "dev": true
+        }
       }
     },
     "tslib": {
diff --git a/package.json b/package.json
index d836fa165..43b3f2f49 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
   },
   "husky": {
     "hooks": {
-      "pre-commit": "npm run lint && npm run build"
+      "pre-commit": "npm run build && npm run lint && npm run test"
     }
   },
   "files": [
@@ -34,7 +34,7 @@
     "eslint-plugin-tsdoc": "^0.2.4",
     "husky": "^4.2.5",
     "jest": "^26.0.1",
-    "ts-jest": "^25.5.1",
+    "ts-jest": "^26.0.0",
     "typescript": "^3.9.2"
   }
 }
diff --git a/src/authentication/CredentialsExtractor.ts b/src/authentication/CredentialsExtractor.ts
index c966f2ce0..28c1ef271 100644
--- a/src/authentication/CredentialsExtractor.ts
+++ b/src/authentication/CredentialsExtractor.ts
@@ -1,15 +1,9 @@
+import { AsyncHandler } from '../util/AsyncHandler';
 import { Credentials } from './Credentials';
 import { HttpRequest } from '../server/HttpRequest';
 
 /**
- * Responsible for extracting credentials.
+ * Responsible for extracting credentials from an incoming request.
+ * Will return `null` if no credentials were found.
  */
-export interface CredentialsExtractor {
-  /**
-   * Extracts the credentials found in an HttpRequest.
-   *
-   * @param request - The incoming request.
-   * @returns A promise resolving to the credentials.
-   */
-  extractCredentials: (request: HttpRequest) => Promise<Credentials>;
-}
+export abstract class CredentialsExtractor extends AsyncHandler<HttpRequest, Credentials> {}
diff --git a/src/authorization/Authorizer.ts b/src/authorization/Authorizer.ts
index 1f185c916..c70c6f384 100644
--- a/src/authorization/Authorizer.ts
+++ b/src/authorization/Authorizer.ts
@@ -1,23 +1,25 @@
+import { AsyncHandler } from '../util/AsyncHandler';
 import { Credentials } from '../authentication/Credentials';
 import { PermissionSet } from '../ldp/permissions/PermissionSet';
-import { ResourceIdentifier } from '../ldp/http/ResourceIdentifier';
+import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 
 /**
- * Responsible for the permission verification.
+ * Verifies if the given credentials have access to the given permissions on the given resource.
+ * An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
  */
-export interface Authorizer {
+export abstract class Authorizer extends AsyncHandler<AuthorizerArgs> {}
+
+export interface AuthorizerArgs {
+  /**
+   * Credentials of the entity that wants to use the resource.
+   */
+  credentials: Credentials;
+  /**
+   * Identifier of the resource that will be read/modified.
+   */
+  identifier: ResourceIdentifier;
   /**
-   * Verifies if the given credentials have access to the given permissions on the given resource.
-   * @param credentials - Credentials of the entity that wants to use the resource.
-   * @param identifier - Identifier of the resource that will be read/modified.
-   * @param permissions - Permissions that are requested on the resource.
-   *
-   * @returns A promise resolving when the Authorizer is finished.
-   * An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
+   * Permissions that are requested on the resource.
    */
-  ensurePermissions: (
-    credentials: Credentials,
-    identifier: ResourceIdentifier,
-    permissions: PermissionSet,
-  ) => Promise<void>;
+  permissions: PermissionSet;
 }
diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts
new file mode 100644
index 000000000..fabe53a46
--- /dev/null
+++ b/src/ldp/AuthenticatedLdpHandler.ts
@@ -0,0 +1,128 @@
+import { Authorizer } from '../authorization/Authorizer';
+import { Credentials } from '../authentication/Credentials';
+import { CredentialsExtractor } from '../authentication/CredentialsExtractor';
+import { HttpHandler } from '../server/HttpHandler';
+import { HttpRequest } from '../server/HttpRequest';
+import { HttpResponse } from '../server/HttpResponse';
+import { Operation } from './operations/Operation';
+import { OperationHandler } from './operations/OperationHandler';
+import { PermissionSet } from './permissions/PermissionSet';
+import { PermissionsExtractor } from './permissions/PermissionsExtractor';
+import { RequestParser } from './http/RequestParser';
+import { ResponseWriter } from './http/ResponseWriter';
+
+/**
+ * Collection of handlers needed for {@link AuthenticatedLdpHandler} to function.
+ */
+export interface AuthenticatedLdpHandlerArgs {
+  /**
+   * Parses the incoming requests.
+   */
+  requestParser: RequestParser;
+  /**
+   * Extracts the credentials from the incoming request.
+   */
+  credentialsExtractor: CredentialsExtractor;
+  /**
+   * Extracts the required permissions from the generated Operation.
+   */
+  permissionsExtractor: PermissionsExtractor;
+  /**
+   * Verifies if the requested operation is allowed.
+   */
+  authorizer: Authorizer;
+  /**
+   * Executed the operation.
+   */
+  operationHandler: OperationHandler;
+  /**
+   * Writes out the response of the operation.
+   */
+  responseWriter: ResponseWriter;
+}
+
+/**
+ * The central manager that connects all the necessary handlers to go from an incoming request to an executed operation.
+ */
+export class AuthenticatedLdpHandler extends HttpHandler {
+  private readonly requestParser: RequestParser;
+
+  private readonly credentialsExtractor: CredentialsExtractor;
+
+  private readonly permissionsExtractor: PermissionsExtractor;
+
+  private readonly authorizer: Authorizer;
+
+  private readonly operationHandler: OperationHandler;
+
+  private readonly responseWriter: ResponseWriter;
+
+  /**
+   * Creates the handler.
+   * @param args - The handlers required. None of them are optional.
+   */
+  public constructor (args: AuthenticatedLdpHandlerArgs) {
+    super();
+    Object.assign(this, args);
+  }
+
+  /**
+   * Checks if the incoming request can be handled. The check is very non-restrictive and will usually be true.
+   * It is based on whether the incoming request can be parsed to an operation.
+   * @param input - Incoming request and response. Only the request will be used.
+   *
+   * @returns A promise resolving if this request can be handled, otherwise rejecting with an Error.
+   */
+  public async canHandle (input: { request: HttpRequest; response: HttpResponse }): Promise<void> {
+    return this.requestParser.canHandle(input.request);
+  }
+
+  /**
+   * Handles the incoming request and writes out the response.
+   * This includes the following steps:
+   *  - Parsing the request to an Operation.
+   *  - Extracting credentials from the request.
+   *  - Extracting the required permissions.
+   *  - Validating if this operation is allowed.
+   *  - Executing the operation.
+   *  - Writing out the response.
+   * @param input - The incoming request and response object to write to.
+   *
+   * @returns A promise resolving when the handling is finished.
+   */
+  public async handle (input: { request: HttpRequest; response: HttpResponse }): Promise<void> {
+    let err: Error;
+    let operation: Operation;
+
+    try {
+      operation = await this.runHandlers(input.request);
+    } catch (error) {
+      err = error;
+    }
+
+    const writeData = { response: input.response, operation, error: err };
+
+    return this.responseWriter.handleSafe(writeData);
+  }
+
+  /**
+   * Runs all handlers except writing the output to the response.
+   * This because any errors thrown here have an impact on the response.
+   * @param request - Incoming request.
+   *
+   * @returns A promise resolving to the generated Operation.
+   */
+  private async runHandlers (request: HttpRequest): Promise<Operation> {
+    const op: Operation = await this.requestParser.handleSafe(request);
+
+    const credentials: Credentials = await this.credentialsExtractor.handleSafe(request);
+
+    const permissions: PermissionSet = await this.permissionsExtractor.handleSafe(op);
+
+    await this.authorizer.handleSafe({ credentials, identifier: op.target, permissions });
+
+    await this.operationHandler.handleSafe(op);
+
+    return op;
+  }
+}
diff --git a/src/ldp/http/Patch.ts b/src/ldp/http/Patch.ts
index 62aa32c97..b5a13a73e 100644
--- a/src/ldp/http/Patch.ts
+++ b/src/ldp/http/Patch.ts
@@ -1,4 +1,4 @@
-import { Representation } from './Representation';
+import { Representation } from '../representation/Representation';
 
 /**
  * Represents the changes needed for a PATCH request.
diff --git a/src/ldp/http/RequestParser.ts b/src/ldp/http/RequestParser.ts
index 8c664fc43..90c82926a 100644
--- a/src/ldp/http/RequestParser.ts
+++ b/src/ldp/http/RequestParser.ts
@@ -5,4 +5,4 @@ import { Operation } from '../operations/Operation';
 /**
  * Converts an incoming HttpRequest to an Operation.
  */
-export type RequestParser = AsyncHandler<HttpRequest, Operation>;
+export abstract class RequestParser extends AsyncHandler<HttpRequest, Operation> {}
diff --git a/src/ldp/http/ResponseWriter.ts b/src/ldp/http/ResponseWriter.ts
new file mode 100644
index 000000000..c6e9769de
--- /dev/null
+++ b/src/ldp/http/ResponseWriter.ts
@@ -0,0 +1,9 @@
+import { AsyncHandler } from '../../util/AsyncHandler';
+import { HttpResponse } from '../../server/HttpResponse';
+import { Operation } from '../operations/Operation';
+
+/**
+ * Writes to the HttpResponse.
+ * Response depends on the operation result and potentially which errors was thrown.
+ */
+export type ResponseWriter = AsyncHandler<{ response: HttpResponse; operation: Operation; error?: Error }>;
diff --git a/src/ldp/operations/Operation.ts b/src/ldp/operations/Operation.ts
index 135586c17..29f3875f8 100644
--- a/src/ldp/operations/Operation.ts
+++ b/src/ldp/operations/Operation.ts
@@ -1,6 +1,6 @@
-import { Representation } from '../http/Representation';
-import { RepresentationPreferences } from '../http/RepresentationPreferences';
-import { ResourceIdentifier } from '../http/ResourceIdentifier';
+import { Representation } from '../representation/Representation';
+import { RepresentationPreferences } from '../representation/RepresentationPreferences';
+import { ResourceIdentifier } from '../representation/ResourceIdentifier';
 
 /**
  * A single REST operation.
diff --git a/src/ldp/operations/OperationHandler.ts b/src/ldp/operations/OperationHandler.ts
index 81e33f5c7..405be2f10 100644
--- a/src/ldp/operations/OperationHandler.ts
+++ b/src/ldp/operations/OperationHandler.ts
@@ -4,4 +4,4 @@ import { Operation } from './Operation';
 /**
  * Handler for a specific operation type.
  */
-export type OperationHandler = AsyncHandler<Operation>;
+export abstract class OperationHandler extends AsyncHandler<Operation> {}
diff --git a/src/ldp/permissions/PermissionsExtractor.ts b/src/ldp/permissions/PermissionsExtractor.ts
index eee29296b..76df3d2a2 100644
--- a/src/ldp/permissions/PermissionsExtractor.ts
+++ b/src/ldp/permissions/PermissionsExtractor.ts
@@ -5,4 +5,4 @@ import { PermissionSet } from './PermissionSet';
 /**
  * Verifies which permissions are requested on a given {@link Operation}.
  */
-export type PermissionsExtractor = AsyncHandler<Operation, PermissionSet>;
+export abstract class PermissionsExtractor extends AsyncHandler<Operation, PermissionSet> {}
diff --git a/src/ldp/http/BinaryRepresentation.ts b/src/ldp/representation/BinaryRepresentation.ts
similarity index 100%
rename from src/ldp/http/BinaryRepresentation.ts
rename to src/ldp/representation/BinaryRepresentation.ts
diff --git a/src/ldp/http/NamedRepresentation.ts b/src/ldp/representation/NamedRepresentation.ts
similarity index 100%
rename from src/ldp/http/NamedRepresentation.ts
rename to src/ldp/representation/NamedRepresentation.ts
diff --git a/src/ldp/http/QuadRepresentation.ts b/src/ldp/representation/QuadRepresentation.ts
similarity index 100%
rename from src/ldp/http/QuadRepresentation.ts
rename to src/ldp/representation/QuadRepresentation.ts
diff --git a/src/ldp/http/Representation.ts b/src/ldp/representation/Representation.ts
similarity index 100%
rename from src/ldp/http/Representation.ts
rename to src/ldp/representation/Representation.ts
diff --git a/src/ldp/http/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts
similarity index 100%
rename from src/ldp/http/RepresentationMetadata.ts
rename to src/ldp/representation/RepresentationMetadata.ts
diff --git a/src/ldp/http/RepresentationPreferences.ts b/src/ldp/representation/RepresentationPreferences.ts
similarity index 100%
rename from src/ldp/http/RepresentationPreferences.ts
rename to src/ldp/representation/RepresentationPreferences.ts
diff --git a/src/ldp/http/ResourceIdentifier.ts b/src/ldp/representation/ResourceIdentifier.ts
similarity index 100%
rename from src/ldp/http/ResourceIdentifier.ts
rename to src/ldp/representation/ResourceIdentifier.ts
diff --git a/src/server/HttpHandler.ts b/src/server/HttpHandler.ts
index ab1e2a294..36acef1f5 100644
--- a/src/server/HttpHandler.ts
+++ b/src/server/HttpHandler.ts
@@ -5,4 +5,4 @@ import { HttpResponse } from './HttpResponse';
 /**
  * An HTTP request handler.
  */
-export type HttpHandler = AsyncHandler<{ request: HttpRequest; response: HttpResponse }>;
+export abstract class HttpHandler extends AsyncHandler<{ request: HttpRequest; response: HttpResponse }> {}
diff --git a/src/storage/Conditions.ts b/src/storage/Conditions.ts
index 76513e763..73d840db4 100644
--- a/src/storage/Conditions.ts
+++ b/src/storage/Conditions.ts
@@ -1,4 +1,4 @@
-import { RepresentationMetadata } from '../ldp/http/RepresentationMetadata';
+import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 
 /**
  * The conditions of an HTTP conditional request.
diff --git a/src/storage/RepresentationConverter.ts b/src/storage/RepresentationConverter.ts
index 67519968b..333a01d7b 100644
--- a/src/storage/RepresentationConverter.ts
+++ b/src/storage/RepresentationConverter.ts
@@ -1,5 +1,5 @@
-import { Representation } from '../ldp/http/Representation';
-import { RepresentationPreferences } from '../ldp/http/RepresentationPreferences';
+import { Representation } from '../ldp/representation/Representation';
+import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
 
 /**
  * Allows converting from one resource representation to another.
diff --git a/src/storage/ResourceLocker.ts b/src/storage/ResourceLocker.ts
index 5265f1e12..8964fd050 100644
--- a/src/storage/ResourceLocker.ts
+++ b/src/storage/ResourceLocker.ts
@@ -1,5 +1,5 @@
 import { Lock } from './Lock';
-import { ResourceIdentifier } from '../ldp/http/ResourceIdentifier';
+import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 
 /**
  * Allows the locking of resources which is needed for non-atomic {@link ResourceStore}s.
diff --git a/src/storage/ResourceMapper.ts b/src/storage/ResourceMapper.ts
index 6467d1e26..195de965b 100644
--- a/src/storage/ResourceMapper.ts
+++ b/src/storage/ResourceMapper.ts
@@ -1,4 +1,4 @@
-import { RepresentationMetadata } from '../ldp/http/RepresentationMetadata';
+import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 
 /**
  * Supports mapping a file to an URL and back.
diff --git a/src/storage/ResourceStore.ts b/src/storage/ResourceStore.ts
index 853a29b98..dcdff55cc 100644
--- a/src/storage/ResourceStore.ts
+++ b/src/storage/ResourceStore.ts
@@ -1,8 +1,8 @@
 import { Conditions } from './Conditions';
 import { Patch } from '../ldp/http/Patch';
-import { Representation } from '../ldp/http/Representation';
-import { RepresentationPreferences } from '../ldp/http/RepresentationPreferences';
-import { ResourceIdentifier } from '../ldp/http/ResourceIdentifier';
+import { Representation } from '../ldp/representation/Representation';
+import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
+import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 
 /**
  * A ResourceStore represents a collection of resources.
diff --git a/src/util/AsyncHandler.ts b/src/util/AsyncHandler.ts
index 6c064a01e..f21212a2b 100644
--- a/src/util/AsyncHandler.ts
+++ b/src/util/AsyncHandler.ts
@@ -1,17 +1,35 @@
 /**
  * Simple interface for classes that can potentially handle a specific kind of data asynchronously.
  */
-export interface AsyncHandler<TInput, TOutput = void> {
+export abstract class AsyncHandler<TInput, TOutput = void> {
   /**
    * Checks if the input data can be handled by this class.
+   * Throws an error if it can't handle the data.
    * @param input - Input data that would be handled potentially.
-   * @returns A promise resolving to if this input can be handled.
+   *
+   * @returns A promise resolving if this input can be handled, rejecting with an Error if not.
    */
-  canHandle: (input: TInput) => Promise<boolean>;
+  public abstract canHandle (input: TInput): Promise<void>;
+
   /**
    * Handles the given input. This should only be done if the {@link canHandle} function returned `true`.
    * @param input - Input data that needs to be handled.
+   *
    * @returns A promise resolving when the handling is finished. Return value depends on the given type.
    */
-  handle: (input: TInput) => Promise<TOutput>;
+  public abstract handle (input: TInput): Promise<TOutput>;
+
+  /**
+   * Helper function that first runs the canHandle function followed by the handle function.
+   * Throws the error of the {@link canHandle} function if the data can't be handled,
+   * or returns the result of the {@link handle} function otherwise.
+   * @param data - The data to handle.
+   *
+   * @returns The result of the handle function of the handler.
+   */
+  public async handleSafe (data: TInput): Promise<TOutput> {
+    await this.canHandle(data);
+
+    return this.handle(data);
+  }
 }
diff --git a/src/util/CompositeAsyncHandler.ts b/src/util/CompositeAsyncHandler.ts
new file mode 100644
index 000000000..ee24919ea
--- /dev/null
+++ b/src/util/CompositeAsyncHandler.ts
@@ -0,0 +1,86 @@
+import { AsyncHandler } from './AsyncHandler';
+import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
+
+/**
+ * Handler that combines several other handlers,
+ * thereby allowing other classes that depend on a single handler to still use multiple.
+ */
+export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
+  private readonly handlers: AsyncHandler<TIn, TOut>[];
+
+  /**
+   * Creates a new CompositeAsyncHandler that stores the given handlers.
+   * @param handlers - Handlers over which it will run.
+   */
+  public constructor (handlers: AsyncHandler<TIn, TOut>[]) {
+    this.handlers = handlers;
+  }
+
+  /**
+   * Checks if any of the stored handlers can handle the given input.
+   * @param input - The data that would need to be handled.
+   *
+   * @returns A promise resolving if at least 1 handler supports to input, or rejecting if none do.
+   */
+  public async canHandle (input: TIn): Promise<void> {
+    await this.findHandler(input);
+  }
+
+  /**
+   * Finds a handler that supports the given input and then lets it handle the given data.
+   * @param input - The data that needs to be handled.
+   *
+   * @returns A promise corresponding to the handle call of a handler that supports the input.
+   * It rejects if no handlers support the given data.
+   */
+  public async handle (input: TIn): Promise<TOut> {
+    let handler: AsyncHandler<TIn, TOut>;
+
+    try {
+      handler = await this.findHandler(input);
+    } catch (error) {
+      throw new Error('All handlers failed. This might be the consequence of calling handle before canHandle.');
+    }
+
+    return handler.handle(input);
+  }
+
+  /**
+   * Identical to {@link AsyncHandler.handleSafe} but optimized for composite by only needing 1 canHandle call on members.
+   * @param input - The input data.
+   *
+   * @returns A promise corresponding to the handle call of a handler that supports the input.
+   * It rejects if no handlers support the given data.
+   */
+  public async handleSafe (input: TIn): Promise<TOut> {
+    const handler = await this.findHandler(input);
+
+    return handler.handle(input);
+  }
+
+  /**
+   * Finds a handler that can handle the given input data.
+   * Otherwise an error gets thrown.
+   *
+   * @param input - The input data.
+   *
+   * @returns A promise resolving to a handler that supports the data or otherwise rejecting.
+   */
+  private async findHandler (input: TIn): Promise<AsyncHandler<TIn, TOut>> {
+    const errors: Error[] = [];
+
+    for (const handler of this.handlers) {
+      try {
+        await handler.canHandle(input);
+
+        return handler;
+      } catch (error) {
+        errors.push(error);
+      }
+    }
+
+    const joined = errors.map((error: Error): string => error.message).join(', ');
+
+    throw new UnsupportedHttpError(`No handler supports the given input: [${joined}].`);
+  }
+}
diff --git a/src/util/errors/HttpError.ts b/src/util/errors/HttpError.ts
new file mode 100644
index 000000000..072aafbd1
--- /dev/null
+++ b/src/util/errors/HttpError.ts
@@ -0,0 +1,19 @@
+/**
+ * An abstract class for all errors that could be thrown by Solid.
+ * All errors inheriting from this should fix the status code thereby hiding the HTTP internals from other components.
+ */
+export abstract class HttpError extends Error {
+  public statusCode: number;
+
+  /**
+   * Creates a new HTTP error. Subclasses should call this with their fixed status code.
+   * @param statusCode - HTTP status code needed for the HTTP response.
+   * @param name - Error name. Useful for logging and stack tracing.
+   * @param message - Message to be thrown.
+   */
+  protected constructor (statusCode: number, name: string, message: string) {
+    super(message);
+    this.statusCode = statusCode;
+    this.name = name;
+  }
+}
diff --git a/src/util/errors/UnsupportedHttpError.ts b/src/util/errors/UnsupportedHttpError.ts
new file mode 100644
index 000000000..9aa17a27e
--- /dev/null
+++ b/src/util/errors/UnsupportedHttpError.ts
@@ -0,0 +1,15 @@
+import { HttpError } from './HttpError';
+
+/**
+ * An error thrown when incoming data is not supported.
+ * Probably because an {@link AsyncHandler} returns false on the canHandle call.
+ */
+export class UnsupportedHttpError extends HttpError {
+  /**
+   * Default message is 'The given input is not supported by the server configuration.'.
+   * @param message - Optional, more specific, message.
+   */
+  public constructor (message?: string) {
+    super(400, 'UnsupportedHttpError', message || 'The given input is not supported by the server configuration.');
+  }
+}


Test patch:
diff --git a/test/unit/ldp/AuthenticatedLdpHandler.test.ts b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
new file mode 100644
index 000000000..a9d2e5e97
--- /dev/null
+++ b/test/unit/ldp/AuthenticatedLdpHandler.test.ts
@@ -0,0 +1,65 @@
+import { Authorizer } from '../../../src/authorization/Authorizer';
+import { CredentialsExtractor } from '../../../src/authentication/CredentialsExtractor';
+import { OperationHandler } from '../../../src/ldp/operations/OperationHandler';
+import { PermissionsExtractor } from '../../../src/ldp/permissions/PermissionsExtractor';
+import { RequestParser } from '../../../src/ldp/http/RequestParser';
+import { ResponseWriter } from '../../../src/ldp/http/ResponseWriter';
+import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';
+import { AuthenticatedLdpHandler, AuthenticatedLdpHandlerArgs } from '../../../src/ldp/AuthenticatedLdpHandler';
+
+describe('An AuthenticatedLdpHandler', (): void => {
+  let args: AuthenticatedLdpHandlerArgs;
+  let responseFn: jest.Mock<Promise<void>, [any]>;
+
+  beforeEach(async (): Promise<void> => {
+    const requestParser: RequestParser = new StaticAsyncHandler(true, 'parser' as any);
+    const credentialsExtractor: CredentialsExtractor = new StaticAsyncHandler(true, 'credentials' as any);
+    const permissionsExtractor: PermissionsExtractor = new StaticAsyncHandler(true, 'permissions' as any);
+    const authorizer: Authorizer = new StaticAsyncHandler(true, 'authorizer' as any);
+    const operationHandler: OperationHandler = new StaticAsyncHandler(true, 'operation' as any);
+    const responseWriter: ResponseWriter = new StaticAsyncHandler(true, 'response' as any);
+
+    responseFn = jest.fn(async (input: any): Promise<void> => {
+      if (!input) {
+        throw new Error('error');
+      }
+    });
+    responseWriter.canHandle = responseFn;
+
+    args = { requestParser, credentialsExtractor, permissionsExtractor, authorizer, operationHandler, responseWriter };
+  });
+
+  it('can be created.', async (): Promise<void> => {
+    expect(new AuthenticatedLdpHandler(args)).toBeInstanceOf(AuthenticatedLdpHandler);
+  });
+
+  it('can check if it handles input.', async (): Promise<void> => {
+    const handler = new AuthenticatedLdpHandler(args);
+
+    await expect(handler.canHandle({ request: null, response: null })).resolves.toBeUndefined();
+  });
+
+  it('can handle input.', async (): Promise<void> => {
+    const handler = new AuthenticatedLdpHandler(args);
+
+    await expect(handler.handle({ request: 'request' as any, response: 'response' as any })).resolves.toEqual('response');
+    expect(responseFn).toHaveBeenCalledTimes(1);
+    expect(responseFn).toHaveBeenLastCalledWith({ response: 'response', operation: 'parser' as any });
+  });
+
+  it('sends an error to the output if a handler does not support the input.', async (): Promise<void> => {
+    args.requestParser = new StaticAsyncHandler(false, null);
+    const handler = new AuthenticatedLdpHandler(args);
+
+    await expect(handler.handle({ request: 'request' as any, response: null })).resolves.toEqual('response');
+    expect(responseFn).toHaveBeenCalledTimes(1);
+    expect(responseFn.mock.calls[0][0].error).toBeInstanceOf(Error);
+  });
+
+  it('errors if the response writer does not support the result.', async (): Promise< void> => {
+    args.responseWriter = new StaticAsyncHandler(false, null);
+    const handler = new AuthenticatedLdpHandler(args);
+
+    await expect(handler.handle({ request: 'request' as any, response: null })).rejects.toThrow(Error);
+  });
+});
diff --git a/test/unit/util/AsyncHandler.test.ts b/test/unit/util/AsyncHandler.test.ts
new file mode 100644
index 000000000..51513d87e
--- /dev/null
+++ b/test/unit/util/AsyncHandler.test.ts
@@ -0,0 +1,30 @@
+import { AsyncHandler } from '../../../src/util/AsyncHandler';
+import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';
+
+describe('An AsyncHandler', (): void => {
+  it('calls canHandle and handle when handleSafe is called.', async (): Promise<void> => {
+    const handlerTrue: AsyncHandler<any, any> = new StaticAsyncHandler(true, null);
+    const canHandleFn = jest.fn(async (input: any): Promise<void> => input);
+    const handleFn = jest.fn(async (input: any): Promise<any> => input);
+
+    handlerTrue.canHandle = canHandleFn;
+    handlerTrue.handle = handleFn;
+    await expect(handlerTrue.handleSafe('test')).resolves.toBe('test');
+    expect(canHandleFn).toHaveBeenCalledTimes(1);
+    expect(handleFn).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not call handle when canHandle errors during a handleSafe call.', async (): Promise<void> => {
+    const handlerFalse: AsyncHandler<any, any> = new StaticAsyncHandler(false, null);
+    const canHandleFn = jest.fn(async (): Promise<void> => {
+      throw new Error('test');
+    });
+    const handleFn = jest.fn(async (input: any): Promise<any> => input);
+
+    handlerFalse.canHandle = canHandleFn;
+    handlerFalse.handle = handleFn;
+    await expect(handlerFalse.handleSafe('test')).rejects.toThrow(Error);
+    expect(canHandleFn).toHaveBeenCalledTimes(1);
+    expect(handleFn).toHaveBeenCalledTimes(0);
+  });
+});
diff --git a/test/unit/util/CompositeAsyncHandler.test.ts b/test/unit/util/CompositeAsyncHandler.test.ts
new file mode 100644
index 000000000..f72dd8f53
--- /dev/null
+++ b/test/unit/util/CompositeAsyncHandler.test.ts
@@ -0,0 +1,76 @@
+import { AsyncHandler } from '../../../src/util/AsyncHandler';
+import { CompositeAsyncHandler } from '../../../src/util/CompositeAsyncHandler';
+import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';
+
+describe('A CompositeAsyncHandler', (): void => {
+  describe('with no handlers', (): void => {
+    it('can never handle data.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([]);
+
+      await expect(handler.canHandle(null)).rejects.toThrow(Error);
+    });
+
+    it('errors if its handle function is called.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([]);
+
+      await expect(handler.handle(null)).rejects.toThrow(Error);
+    });
+  });
+
+  describe('with multiple handlers', (): void => {
+    let handlerTrue: AsyncHandler<any, any>;
+    let handlerFalse: AsyncHandler<any, any>;
+    let canHandleFn: jest.Mock<Promise<void>, [any]>;
+    let handleFn: jest.Mock<Promise<void>, [any]>;
+
+    beforeEach(async (): Promise<void> => {
+      handlerTrue = new StaticAsyncHandler(true, null);
+      handlerFalse = new StaticAsyncHandler(false, null);
+
+      canHandleFn = jest.fn(async (input: any): Promise<any> => input);
+      handleFn = jest.fn(async (input: any): Promise<any> => input);
+      handlerTrue.canHandle = canHandleFn;
+      handlerTrue.handle = handleFn;
+    });
+
+    it('can handle data if a handler supports it.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]);
+
+      await expect(handler.canHandle(null)).resolves.toBeUndefined();
+    });
+
+    it('can not handle data if no handler supports it.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
+
+      await expect(handler.canHandle(null)).rejects.toThrow('[Not supported., Not supported.]');
+    });
+
+    it('handles data if a handler supports it.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]);
+
+      await expect(handler.handle('test')).resolves.toEqual('test');
+      expect(canHandleFn).toHaveBeenCalledTimes(1);
+      expect(handleFn).toHaveBeenCalledTimes(1);
+    });
+
+    it('errors if the handle function is called but no handler supports the data.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
+
+      await expect(handler.handle('test')).rejects.toThrow('All handlers failed.');
+    });
+
+    it('only calls the canHandle function once of its handlers when handleSafe is called.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]);
+
+      await expect(handler.handleSafe('test')).resolves.toEqual('test');
+      expect(canHandleFn).toHaveBeenCalledTimes(1);
+      expect(handleFn).toHaveBeenCalledTimes(1);
+    });
+
+    it('throws the same error as canHandle when calling handleSafe if no handler supports the data.', async (): Promise<void> => {
+      const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
+
+      await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported., Not supported.]');
+    });
+  });
+});
diff --git a/test/unit/util/errors/UnsupportedHttpError.test.ts b/test/unit/util/errors/UnsupportedHttpError.test.ts
new file mode 100644
index 000000000..c6f62d72a
--- /dev/null
+++ b/test/unit/util/errors/UnsupportedHttpError.test.ts
@@ -0,0 +1,17 @@
+import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
+
+describe('An UnsupportedHttpError', (): void => {
+  it('has status code 400.', async (): Promise<void> => {
+    const error = new UnsupportedHttpError('test');
+
+    expect(error.statusCode).toEqual(400);
+    expect(error.message).toEqual('test');
+    expect(error.name).toEqual('UnsupportedHttpError');
+  });
+
+  it('has a default message if none was provided.', async (): Promise<void> => {
+    const error = new UnsupportedHttpError();
+
+    expect(error.message).toEqual('The given input is not supported by the server configuration.');
+  });
+});
diff --git a/test/util/StaticAsyncHandler.ts b/test/util/StaticAsyncHandler.ts
new file mode 100644
index 000000000..617507f35
--- /dev/null
+++ b/test/util/StaticAsyncHandler.ts
@@ -0,0 +1,24 @@
+import { AsyncHandler } from '../../src/util/AsyncHandler';
+
+export class StaticAsyncHandler<TOut> extends AsyncHandler<any, TOut> {
+  private readonly canHandleStatic: boolean;
+
+  private readonly handleStatic: TOut;
+
+  public constructor (canHandleStatic: boolean, handleStatic: TOut) {
+    super();
+    this.canHandleStatic = canHandleStatic;
+    this.handleStatic = handleStatic;
+  }
+
+  public async canHandle (): Promise<void> {
+    if (this.canHandleStatic) {
+      return;
+    }
+    throw new Error('Not supported.');
+  }
+
+  public async handle (): Promise<TOut> {
+    return this.handleStatic;
+  }
+}