Repository: solid/community-server
Pull request #1054 (2021-11-09 12:18:18)

feat: Move OIDC library behaviour to separate path
#### Description
Makes it so all requests targeting the OIDC library use `/.oidc/*` paths. Now the `IdentityProviderHttpHandler` doesn't have to take that situation into account anymore. Does not remove that much checks, but does make the flow cleaner and removes some implicit assumptions, which might help when updating this class in the future.

This doesn't actually change anything about the server behaviour since those paths should not be hardcoded anywhere, but still targeting the 3.0.0 branch since it is mostly irrelevant for existing installations and will mostly be relevant when adding new 3.0.0 features.


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index ed1509d3f..34e935ece 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -44,6 +44,7 @@ module.exports = {
     // Problems with optional parameters
     '@typescript-eslint/no-unnecessary-condition': 'off',
     '@typescript-eslint/prefer-optional-chain': 'error',
+    '@typescript-eslint/promise-function-async': [ 'error', { checkArrowFunctions: false } ],
     '@typescript-eslint/space-before-function-paren': [ 'error', 'never' ],
     '@typescript-eslint/unbound-method': 'off',
     '@typescript-eslint/unified-signatures': 'off',
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100644
index 000000000..34415882a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,16 @@
+---
+name: "\U0001F41B Bug report"
+about: If something is not working as expected or crashes
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+#### Environment
+- Server version: *Output of `community-solid-server --version` for a global or `npx community-solid-server --version` for a local installation*
+- Node.js version: *Output of `node -v`*
+- npm version: *Output of `npm -v`*
+
+#### Description
+<!-- Please describe the exact problem as clearly as possible. Provide any error messages thrown. -->
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 000000000..fca4c3fec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,13 @@
+---
+name: "➕ Feature request"
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+#### Feature description:
+
+<!--In case you want to start a discussion about an idea, discussions are better suited for this https://github.com/solid/community-server/discussions -->
+<!--A clear and concise description of what you want to happen.-->
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 000000000..7add40a8a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,13 @@
+---
+name: "❓ Question"
+about: A general question
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+#### Question
+
+<!-- Questions are usually better suited for discussions, which can be found at https://github.com/solid/community-server/discussions -->
+<!-- In case you think this would better as an issue, please provide a clear and concisely formulated question.-->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..7ef89572c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+<!-- 
+Things to check before submitting a pull request:
+  * Label this PR with the correct semver label (if you have permission to do so).
+    - semver.patch: Backwards compatible bug fixes.
+    - semver.minor: Backwards compatible feature additions.
+    - semver.major: Breaking changes. This includes changing interfaces or configuration behaviour.
+  * Target the correct branch. Patch updates can target main, other changes should target the latest versions/* branch.
+  * Update the RELEASE_NOTES.md document in case of relevant feature or config changes.
+-->
+
+#### Related issues
+<!-- Reference any relevant issues here. Closing keywords only have an effect when targeting the main branch. -->
+
+#### Description
+<!-- Describe the relevant changes in this PR. Also add notes that might be relevant for code reviewers. -->
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1e7bae486..921d7ac1a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,7 +46,6 @@ jobs:
           - '14.x'
           - '16.0'
           - '16.x'
-          - '17.0'
           - '17.x'
     timeout-minutes: 10
     steps:
@@ -74,11 +73,9 @@ jobs:
         run: npm run test:deploy
 
   test-integration:
-    runs-on: ${{ matrix.operating-system }}
+    runs-on: ubuntu-latest
     strategy:
       matrix:
-        operating-system:
-          - ubuntu-latest
         node-version:
           - '12.x'
           - '14.x'
@@ -109,6 +106,29 @@ jobs:
       - name: Run integration tests
         run: npm run test:integration
 
+  test-integration-windows:
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        node-version:
+          - '12.x'
+          - '14.x'
+          - '16.x'
+    timeout-minutes: 10
+    steps:
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Ensure line endings are consistent
+        run: git config --global core.autocrlf input
+      - name: Check out repository
+        uses: actions/checkout@v2
+      - name: Install dependencies and run build scripts
+        run: npm ci
+      - name: Run integration tests
+        run: npm run test:integration
+
   coveralls:
     needs: test-unit
     runs-on: ubuntu-latest
diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml
index b61d7a04b..42060327e 100644
--- a/.github/workflows/schedule.yml
+++ b/.github/workflows/schedule.yml
@@ -14,7 +14,7 @@ jobs:
       matrix:
         branch:
           - 'main'
-          - 'versions/2.1.0'
+          - 'versions/3.0.0'
     timeout-minutes: 10
     steps:
       - name: Use Node.js 16.x
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 1656f83eb..70bb6acde 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -15,6 +15,9 @@ The following changes are relevant for v2 custom configs that replaced certain f
   - `/util/representation-conversion/default.json`
 - The IDP settings have changed to support the latest Solid-OIDC draft.
   - `/identity/handler/provider-factory/identity.json`
+- Requests targeting the OIDC library now use a separate handler.
+  - `/http/handler/default.json`
+  - `/identity/handler/default.json`
 
 ### Interface changes
 These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.
diff --git a/config/http/handler/default.json b/config/http/handler/default.json
index 01e0b34b1..acbcfb97d 100644
--- a/config/http/handler/default.json
+++ b/config/http/handler/default.json
@@ -1,7 +1,7 @@
 {
   "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld",
   "import": [
-    "files-scs:config/app/init/initializers/root.json"
+    "files-scs:config/http/handler/handlers/oidc.json"
   ],
   "@graph": [
     {
@@ -15,6 +15,7 @@
           "handlers": [
             { "@id": "urn:solid-server:default:StaticAssetHandler" },
             { "@id": "urn:solid-server:default:SetupHandler" },
+            { "@id": "urn:solid-server:default:OidcHandler" },
             { "@id": "urn:solid-server:default:AuthResourceHttpHandler" },
             { "@id": "urn:solid-server:default:IdentityProviderHandler" },
             { "@id": "urn:solid-server:default:LdpHandler" }
diff --git a/config/http/handler/handlers/oidc.json b/config/http/handler/handlers/oidc.json
new file mode 100644
index 000000000..e9cf0fa0f
--- /dev/null
+++ b/config/http/handler/handlers/oidc.json
@@ -0,0 +1,18 @@
+{
+  "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld",
+  "@graph": [
+    {
+      "comment": "Routes all OIDC related requests to the OIDC library.",
+      "@id": "urn:solid-server:default:OidcHandler",
+      "@type": "RouterHandler",
+      "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+      "args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
+      "args_allowedMethods": [ "*" ],
+      "args_allowedPathNames": [ "^/.oidc/.*", "^/\\.well-known/openid-configuration" ],
+      "args_handler": {
+        "@type": "OidcHttpHandler",
+        "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }
+      }
+    }
+  ]
+}
diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json
index f6104b5d3..c9b13dc4c 100644
--- a/config/identity/handler/default.json
+++ b/config/identity/handler/default.json
@@ -14,7 +14,7 @@
       "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
       "args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
       "args_allowedMethods": [ "*" ],
-      "args_allowedPathNames": [ "^/idp/.*", "^/\\.well-known/openid-configuration" ],
+      "args_allowedPathNames": [ "^/idp/.*" ],
       "args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
     },
     {
diff --git a/config/identity/handler/provider-factory/identity.json b/config/identity/handler/provider-factory/identity.json
index 5c8686ee9..cde397c61 100644
--- a/config/identity/handler/provider-factory/identity.json
+++ b/config/identity/handler/provider-factory/identity.json
@@ -10,6 +10,7 @@
       "@type": "IdentityProviderFactory",
       "args_adapterFactory": { "@id": "urn:solid-server:default:IdpAdapterFactory" },
       "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
+      "args_oidcPath": "/.oidc",
       "args_idpPath": "/idp",
       "args_storage": { "@id": "urn:solid-server:default:IdpKeyStorage" },
       "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
diff --git a/package-lock.json b/package-lock.json
index 307c415cc..14c2c49a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
         "@types/sparqljs": "^3.1.2",
         "@types/url-join": "^4.0.0",
         "@types/uuid": "^8.3.0",
-        "@types/ws": "^7.4.5",
+        "@types/ws": "^8.2.0",
         "@types/yargs": "^17.0.0",
         "arrayify-stream": "^1.0.0",
         "async-lock": "^1.3.0",
@@ -56,13 +56,13 @@
         "rdf-serialize": "^1.1.0",
         "redis": "^3.1.2",
         "redlock": "^4.2.0",
-        "sparqlalgebrajs": "^3.0.0",
+        "sparqlalgebrajs": "^4.0.1",
         "sparqljs": "^3.4.2",
         "url-join": "^4.0.1",
         "uuid": "^8.3.2",
         "winston": "^3.3.3",
         "winston-transport": "^4.4.0",
-        "ws": "^8.0.0",
+        "ws": "^8.2.3",
         "yargs": "^17.0.1"
       },
       "bin": {
@@ -3780,6 +3780,24 @@
         "sparqlalgebrajs": "^3.0.1"
       }
     },
+    "node_modules/@comunica/types/node_modules/sparqlalgebrajs": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz",
+      "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==",
+      "dependencies": {
+        "@rdfjs/types": "*",
+        "@types/sparqljs": "^3.1.2",
+        "fast-deep-equal": "^3.1.3",
+        "minimist": "^1.2.5",
+        "rdf-data-factory": "^1.1.0",
+        "rdf-isomorphic": "^1.3.0",
+        "rdf-string": "^1.6.0",
+        "sparqljs": "^3.4.2"
+      },
+      "bin": {
+        "sparqlalgebrajs": "bin/sparqlalgebrajs.js"
+      }
+    },
     "node_modules/@comunica/utils-datasource": {
       "version": "1.21.1",
       "resolved": "https://registry.npmjs.org/@comunica/utils-datasource/-/utils-datasource-1.21.1.tgz",
@@ -4886,9 +4904,9 @@
       "integrity": "sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ=="
     },
     "node_modules/@types/sparqljs": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.2.tgz",
-      "integrity": "sha512-tLfrnBuK37P2Bn8Fo7Qik95sBXYHw5D+gq3MMq1HVyoTpCWivwPnP0Mmd7Apamdc9eH3mLJwIZIETHCQ6HxMUw==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.3.tgz",
+      "integrity": "sha512-nmFgmR6ns4i8sg9fYu+293H+PMLKmDOZy34sgwgAeUEEiIqSs4guj5aCZRt3gq1g0yuKXkqrxLDq/684g7pGtQ==",
       "dependencies": {
         "rdf-js": "^4.0.2"
       }
@@ -4934,9 +4952,9 @@
       "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
     },
     "node_modules/@types/ws": {
-      "version": "7.4.5",
-      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.5.tgz",
-      "integrity": "sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
+      "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
       "dependencies": {
         "@types/node": "*"
       }
@@ -14164,18 +14182,18 @@
       "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig=="
     },
     "node_modules/sparqlalgebrajs": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz",
-      "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-4.0.1.tgz",
+      "integrity": "sha512-/YOn4+5JEawI29h9eFYb9rScCtiLIK4TG+MrXpbJlHDjL8W34+V/RQoXqeau4SFXtA60Vzc7LRokefguh+I37Q==",
       "dependencies": {
         "@rdfjs/types": "*",
-        "@types/sparqljs": "^3.1.2",
+        "@types/sparqljs": "^3.1.3",
         "fast-deep-equal": "^3.1.3",
         "minimist": "^1.2.5",
         "rdf-data-factory": "^1.1.0",
         "rdf-isomorphic": "^1.3.0",
         "rdf-string": "^1.6.0",
-        "sparqljs": "^3.4.2"
+        "sparqljs": "^3.5.1"
       },
       "bin": {
         "sparqlalgebrajs": "bin/sparqlalgebrajs.js"
@@ -14220,9 +14238,9 @@
       }
     },
     "node_modules/sparqljs": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz",
-      "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.5.1.tgz",
+      "integrity": "sha512-sHc6z7hNF3ACvXurKe8hT1sD52Fc0fN3uPLS6SQnXRV9CJl33GNAS4w5Dd3X3GgykUt9SlnjhI1QRKhLzun4qQ==",
       "dependencies": {
         "rdf-data-factory": "^1.0.4"
       },
@@ -15577,9 +15595,9 @@
       }
     },
     "node_modules/ws": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.0.0.tgz",
-      "integrity": "sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA==",
+      "version": "8.2.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
+      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
       "engines": {
         "node": ">=10.0.0"
       },
@@ -18574,6 +18592,23 @@
         "asynciterator": "^3.2.0",
         "immutable": "^3.8.2",
         "sparqlalgebrajs": "^3.0.1"
+      },
+      "dependencies": {
+        "sparqlalgebrajs": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz",
+          "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==",
+          "requires": {
+            "@rdfjs/types": "*",
+            "@types/sparqljs": "^3.1.2",
+            "fast-deep-equal": "^3.1.3",
+            "minimist": "^1.2.5",
+            "rdf-data-factory": "^1.1.0",
+            "rdf-isomorphic": "^1.3.0",
+            "rdf-string": "^1.6.0",
+            "sparqljs": "^3.4.2"
+          }
+        }
       }
     },
     "@comunica/utils-datasource": {
@@ -19578,9 +19613,9 @@
       "integrity": "sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ=="
     },
     "@types/sparqljs": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.2.tgz",
-      "integrity": "sha512-tLfrnBuK37P2Bn8Fo7Qik95sBXYHw5D+gq3MMq1HVyoTpCWivwPnP0Mmd7Apamdc9eH3mLJwIZIETHCQ6HxMUw==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/sparqljs/-/sparqljs-3.1.3.tgz",
+      "integrity": "sha512-nmFgmR6ns4i8sg9fYu+293H+PMLKmDOZy34sgwgAeUEEiIqSs4guj5aCZRt3gq1g0yuKXkqrxLDq/684g7pGtQ==",
       "requires": {
         "rdf-js": "^4.0.2"
       }
@@ -19626,9 +19661,9 @@
       "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
     },
     "@types/ws": {
-      "version": "7.4.5",
-      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.5.tgz",
-      "integrity": "sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
+      "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
       "requires": {
         "@types/node": "*"
       }
@@ -26661,18 +26696,18 @@
       "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig=="
     },
     "sparqlalgebrajs": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-3.0.3.tgz",
-      "integrity": "sha512-XFNhsO55bprayrM35h/jY0kzzuGc3oZ1On3kc+s7Un0BFQBXa046aLcMZFp4MYSvn7GtMe9eZ08ONFnBH5kEsQ==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/sparqlalgebrajs/-/sparqlalgebrajs-4.0.1.tgz",
+      "integrity": "sha512-/YOn4+5JEawI29h9eFYb9rScCtiLIK4TG+MrXpbJlHDjL8W34+V/RQoXqeau4SFXtA60Vzc7LRokefguh+I37Q==",
       "requires": {
         "@rdfjs/types": "*",
-        "@types/sparqljs": "^3.1.2",
+        "@types/sparqljs": "^3.1.3",
         "fast-deep-equal": "^3.1.3",
         "minimist": "^1.2.5",
         "rdf-data-factory": "^1.1.0",
         "rdf-isomorphic": "^1.3.0",
         "rdf-string": "^1.6.0",
-        "sparqljs": "^3.4.2"
+        "sparqljs": "^3.5.1"
       }
     },
     "sparqlee": {
@@ -26710,9 +26745,9 @@
       }
     },
     "sparqljs": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.4.2.tgz",
-      "integrity": "sha512-MmmZ6cMuvhf4Eh2FXX21dalgADUiZ9WN8XKMedwhTFg0r7W09/o8wvoZ8C4yA6FptnjjAjm+mGnxAEpkSRY3QQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/sparqljs/-/sparqljs-3.5.1.tgz",
+      "integrity": "sha512-sHc6z7hNF3ACvXurKe8hT1sD52Fc0fN3uPLS6SQnXRV9CJl33GNAS4w5Dd3X3GgykUt9SlnjhI1QRKhLzun4qQ==",
       "requires": {
         "rdf-data-factory": "^1.0.4"
       }
@@ -27816,9 +27851,9 @@
       }
     },
     "ws": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.0.0.tgz",
-      "integrity": "sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA==",
+      "version": "8.2.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
+      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
       "requires": {}
     },
     "xdg-basedir": {
diff --git a/package.json b/package.json
index 0b2684840..4580f267b 100644
--- a/package.json
+++ b/package.json
@@ -96,7 +96,7 @@
     "@types/sparqljs": "^3.1.2",
     "@types/url-join": "^4.0.0",
     "@types/uuid": "^8.3.0",
-    "@types/ws": "^7.4.5",
+    "@types/ws": "^8.2.0",
     "@types/yargs": "^17.0.0",
     "arrayify-stream": "^1.0.0",
     "async-lock": "^1.3.0",
@@ -122,13 +122,13 @@
     "rdf-serialize": "^1.1.0",
     "redis": "^3.1.2",
     "redlock": "^4.2.0",
-    "sparqlalgebrajs": "^3.0.0",
+    "sparqlalgebrajs": "^4.0.1",
     "sparqljs": "^3.4.2",
     "url-join": "^4.0.1",
     "uuid": "^8.3.2",
     "winston": "^3.3.3",
     "winston-transport": "^4.4.0",
-    "ws": "^8.0.0",
+    "ws": "^8.2.3",
     "yargs": "^17.0.1"
   },
   "devDependencies": {
diff --git a/src/authorization/permissions/SparqlPatchModesExtractor.ts b/src/authorization/permissions/SparqlPatchModesExtractor.ts
index cad78bc52..cae2798d6 100644
--- a/src/authorization/permissions/SparqlPatchModesExtractor.ts
+++ b/src/authorization/permissions/SparqlPatchModesExtractor.ts
@@ -40,25 +40,25 @@ export class SparqlPatchModesExtractor extends ModesExtractor {
     return Boolean((data as SparqlUpdatePatch).algebra);
   }
 
-  private isSupported(op: Algebra.Operation): boolean {
+  private isSupported(op: Algebra.Update): boolean {
     if (this.isDeleteInsert(op) || this.isNop(op)) {
       return true;
     }
     if (op.type === Algebra.types.COMPOSITE_UPDATE) {
-      return (op as Algebra.CompositeUpdate).updates.every((update): boolean => this.isSupported(update));
+      return op.updates.every((update): boolean => this.isSupported(update));
     }
     return false;
   }
 
-  private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert {
+  private isDeleteInsert(op: Algebra.Update): op is Algebra.DeleteInsert {
     return op.type === Algebra.types.DELETE_INSERT;
   }
 
-  private isNop(op: Algebra.Operation): op is Algebra.Nop {
+  private isNop(op: Algebra.Update): op is Algebra.Nop {
     return op.type === Algebra.types.NOP;
   }
 
-  private needsAppend(update: Algebra.Operation): boolean {
+  private needsAppend(update: Algebra.Update): boolean {
     if (this.isNop(update)) {
       return false;
     }
@@ -69,7 +69,7 @@ export class SparqlPatchModesExtractor extends ModesExtractor {
     return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.needsAppend(op));
   }
 
-  private needsWrite(update: Algebra.Operation): boolean {
+  private needsWrite(update: Algebra.Update): boolean {
     if (this.isNop(update)) {
       return false;
     }
diff --git a/src/http/UnsecureWebSocketsProtocol.ts b/src/http/UnsecureWebSocketsProtocol.ts
index 5bfe5040b..bafc520c3 100644
--- a/src/http/UnsecureWebSocketsProtocol.ts
+++ b/src/http/UnsecureWebSocketsProtocol.ts
@@ -1,5 +1,5 @@
 import { EventEmitter } from 'events';
-import type WebSocket from 'ws';
+import type { WebSocket } from 'ws';
 import { getLoggerFor } from '../logging/LogUtil';
 import type { HttpRequest } from '../server/HttpRequest';
 import { WebSocketHandler } from '../server/WebSocketHandler';
diff --git a/src/http/input/body/SparqlUpdateBodyParser.ts b/src/http/input/body/SparqlUpdateBodyParser.ts
index b9600769a..8b0d871e8 100644
--- a/src/http/input/body/SparqlUpdateBodyParser.ts
+++ b/src/http/input/body/SparqlUpdateBodyParser.ts
@@ -26,7 +26,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
     const sparql = await readableToString(request);
     let algebra: Algebra.Operation;
     try {
-      algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value });
+      algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value }) as Algebra.Update;
     } catch (error: unknown) {
       this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
       throw new BadRequestHttpError(createErrorMessage(error), { cause: error });
diff --git a/src/http/output/error/ConvertingErrorHandler.ts b/src/http/output/error/ConvertingErrorHandler.ts
index 278c7e45b..7bd6faecd 100644
--- a/src/http/output/error/ConvertingErrorHandler.ts
+++ b/src/http/output/error/ConvertingErrorHandler.ts
@@ -3,7 +3,7 @@ import type {
   RepresentationConverterArgs,
 } from '../../../storage/conversion/RepresentationConverter';
 import { INTERNAL_ERROR } from '../../../util/ContentTypes';
-import { getStatusCode } from '../../../util/errors/ErrorUtil';
+import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
 import { toLiteral } from '../../../util/TermUtil';
 import { HTTP, XSD } from '../../../util/Vocabularies';
 import { BasicRepresentation } from '../../representation/BasicRepresentation';
diff --git a/src/http/output/error/SafeErrorHandler.ts b/src/http/output/error/SafeErrorHandler.ts
index 7071c773d..9da9b095d 100644
--- a/src/http/output/error/SafeErrorHandler.ts
+++ b/src/http/output/error/SafeErrorHandler.ts
@@ -1,5 +1,6 @@
 import { getLoggerFor } from '../../../logging/LogUtil';
-import { createErrorMessage, getStatusCode } from '../../../util/errors/ErrorUtil';
+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';
diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts
index 5d9f416b2..5be0a772a 100644
--- a/src/identity/IdentityProviderHttpHandler.ts
+++ b/src/identity/IdentityProviderHttpHandler.ts
@@ -10,6 +10,7 @@ import { OperationHttpHandler } from '../server/OperationHttpHandler';
 import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
 import { APPLICATION_JSON } from '../util/ContentTypes';
 import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
+import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { joinUrl, trimTrailingSlashes } from '../util/PathUtil';
 import { addTemplateMetadata, cloneRepresentation } from '../util/ResourceUtil';
 import { readJsonStream } from '../util/StreamUtil';
@@ -77,8 +78,6 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
   private readonly controls: Record<string, string>;
 
   public constructor(args: IdentityProviderHttpHandlerArgs) {
-    // It is important that the RequestParser does not read out the Request body stream.
-    // Otherwise we can't pass it anymore to the OIDC library when needed.
     super();
     // Trimming trailing slashes so the relative URL starts with a slash after slicing this off
     this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath));
@@ -97,29 +96,19 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
   /**
    * Finds the matching route and resolves the operation.
    */
-  public async handle({ operation, request, response }: OperationHttpHandlerInput):
-  Promise<ResponseDescription | undefined> {
+  public async handle({ operation, request, response }: OperationHttpHandlerInput): Promise<ResponseDescription> {
     // This being defined means we're in an OIDC session
     let oidcInteraction: Interaction | undefined;
     try {
       const provider = await this.providerFactory.getProvider();
-      // This being defined means we're in an OIDC session
       oidcInteraction = await provider.interactionDetails(request, response);
     } catch {
       // Just a regular request
     }
 
-    // If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library
     const route = await this.findRoute(operation, oidcInteraction);
-
     if (!route) {
-      const provider = await this.providerFactory.getProvider();
-      this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
-      // Even though the typings do not indicate this, this is a Promise that needs to be awaited.
-      // Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response.
-      // eslint-disable-next-line @typescript-eslint/await-thenable
-      await provider.callback(request, response);
-      return;
+      throw new NotFoundHttpError();
     }
 
     // Cloning input data so it can be sent back in case of errors
@@ -149,7 +138,7 @@ export class IdentityProviderHttpHandler extends OperationHttpHandler {
    */
   private async findRoute(operation: Operation, oidcInteraction?: Interaction): Promise<InteractionRoute | undefined> {
     if (!operation.target.path.startsWith(this.baseUrl)) {
-      // This is either an invalid request or a call to the .well-known configuration
+      // This is an invalid request
       return;
     }
     const pathName = operation.target.path.slice(this.baseUrl.length);
diff --git a/src/identity/OidcHttpHandler.ts b/src/identity/OidcHttpHandler.ts
new file mode 100644
index 000000000..a8a3fe924
--- /dev/null
+++ b/src/identity/OidcHttpHandler.ts
@@ -0,0 +1,27 @@
+import { getLoggerFor } from '../logging/LogUtil';
+import type { HttpHandlerInput } from '../server/HttpHandler';
+import { HttpHandler } from '../server/HttpHandler';
+import type { ProviderFactory } from './configuration/ProviderFactory';
+
+/**
+ * HTTP handler that redirects all requests to the OIDC library.
+ */
+export class OidcHttpHandler extends HttpHandler {
+  protected readonly logger = getLoggerFor(this);
+
+  private readonly providerFactory: ProviderFactory;
+
+  public constructor(providerFactory: ProviderFactory) {
+    super();
+    this.providerFactory = providerFactory;
+  }
+
+  public async handle({ request, response }: HttpHandlerInput): Promise<void> {
+    const provider = await this.providerFactory.getProvider();
+    this.logger.debug(`Sending request to oidc-provider: ${request.url}`);
+    // Even though the typings do not indicate this, this is a Promise that needs to be awaited.
+    // Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response.
+    // eslint-disable-next-line @typescript-eslint/await-thenable
+    await provider.callback(request, response);
+  }
+}
diff --git a/src/identity/configuration/IdentityProviderFactory.ts b/src/identity/configuration/IdentityProviderFactory.ts
index 5ef2ff3e1..cec78ee1e 100644
--- a/src/identity/configuration/IdentityProviderFactory.ts
+++ b/src/identity/configuration/IdentityProviderFactory.ts
@@ -30,7 +30,11 @@ export interface IdentityProviderFactoryArgs {
    */
   baseUrl: string;
   /**
-   * Path of the IDP component in the server.
+   * Path for all requests targeting the OIDC library.
+   */
+  oidcPath: string;
+  /**
+   * The entry point for the custom IDP handlers of the server.
    * Should start with a slash.
    */
   idpPath: string;
@@ -62,6 +66,7 @@ export class IdentityProviderFactory implements ProviderFactory {
   private readonly config: Configuration;
   private readonly adapterFactory!: AdapterFactory;
   private readonly baseUrl!: string;
+  private readonly oidcPath!: string;
   private readonly idpPath!: string;
   private readonly storage!: KeyValueStorage<string, unknown>;
   private readonly errorHandler!: ErrorHandler;
@@ -107,6 +112,7 @@ export class IdentityProviderFactory implements ProviderFactory {
     // Allow provider to interpret reverse proxy headers
     const provider = new Provider(this.baseUrl, config);
     provider.proxy = true;
+
     return provider;
   }
 
@@ -210,11 +216,11 @@ export class IdentityProviderFactory implements ProviderFactory {
 
   /**
    * Creates the route string as required by the `oidc-provider` library.
-   * In case base URL is `http://test.com/foo/`, `idpPath` is `/idp` and `relative` is `device/auth`,
+   * In case base URL is `http://test.com/foo/`, `oidcPath` is `/idp` and `relative` is `device/auth`,
    * this would result in `/foo/idp/device/auth`.
    */
   private createRoute(relative: string): string {
-    return new URL(joinUrl(this.baseUrl, this.idpPath, relative)).pathname;
+    return new URL(joinUrl(this.baseUrl, this.oidcPath, relative)).pathname;
   }
 
   /**
diff --git a/src/index.ts b/src/index.ts
index 5352a0bf6..fc5667b28 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -165,6 +165,7 @@ export * from './identity/storage/WebIdAdapterFactory';
 
 // Identity
 export * from './identity/IdentityProviderHttpHandler';
+export * from './identity/OidcHttpHandler';
 
 // Init/Final
 export * from './init/final/Finalizable';
@@ -320,6 +321,7 @@ export * from './util/errors/ConflictHttpError';
 export * from './util/errors/ErrorUtil';
 export * from './util/errors/ForbiddenHttpError';
 export * from './util/errors/HttpError';
+export * from './util/errors/HttpErrorUtil';
 export * from './util/errors/InternalServerError';
 export * from './util/errors/MethodNotAllowedHttpError';
 export * from './util/errors/NotFoundHttpError';
diff --git a/src/server/AuthorizingHttpHandler.ts b/src/server/AuthorizingHttpHandler.ts
index 450d0026a..af8b56aff 100644
--- a/src/server/AuthorizingHttpHandler.ts
+++ b/src/server/AuthorizingHttpHandler.ts
@@ -58,7 +58,7 @@ export class AuthorizingHttpHandler extends OperationHttpHandler {
     this.operationHandler = args.operationHandler;
   }
 
-  public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription | undefined> {
+  public async handle(input: OperationHttpHandlerInput): Promise<ResponseDescription> {
     const { request, operation } = input;
     const credentials: CredentialSet = await this.credentialsExtractor.handleSafe(request);
     this.logger.verbose(`Extracted credentials: ${JSON.stringify(credentials)}`);
diff --git a/src/server/OperationHttpHandler.ts b/src/server/OperationHttpHandler.ts
index 8f685be35..31b4b1ddc 100644
--- a/src/server/OperationHttpHandler.ts
+++ b/src/server/OperationHttpHandler.ts
@@ -9,8 +9,6 @@ export interface OperationHttpHandlerInput extends HttpHandlerInput {
 
 /**
  * An HTTP handler that makes use of an already parsed Operation.
- * Can either return a ResponseDescription to be resolved by the calling class,
- * or undefined if this class handles the response itself.
  */
 export abstract class OperationHttpHandler
-  extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription | undefined> {}
+  extends AsyncHandler<OperationHttpHandlerInput, ResponseDescription> {}
diff --git a/src/server/WebSocketHandler.ts b/src/server/WebSocketHandler.ts
index ec7c40f5a..d968edabb 100644
--- a/src/server/WebSocketHandler.ts
+++ b/src/server/WebSocketHandler.ts
@@ -1,4 +1,4 @@
-import type WebSocket from 'ws';
+import type { WebSocket } from 'ws';
 import { AsyncHandler } from '../util/handlers/AsyncHandler';
 import type { HttpRequest } from './HttpRequest';
 
diff --git a/src/server/WebSocketServerFactory.ts b/src/server/WebSocketServerFactory.ts
index a154ff4cc..5826e1501 100644
--- a/src/server/WebSocketServerFactory.ts
+++ b/src/server/WebSocketServerFactory.ts
@@ -1,6 +1,6 @@
 import type { Server } from 'http';
 import type { Socket } from 'net';
-import type WebSocket from 'ws';
+import type { WebSocket } from 'ws';
 import { Server as WebSocketServer } from 'ws';
 import type { HttpRequest } from './HttpRequest';
 import type { HttpServerFactory } from './HttpServerFactory';
diff --git a/src/util/PromiseUtil.ts b/src/util/PromiseUtil.ts
index 7b72f82ee..8d38b3ae9 100644
--- a/src/util/PromiseUtil.ts
+++ b/src/util/PromiseUtil.ts
@@ -1,5 +1,7 @@
+import { createAggregateError } from './errors/HttpErrorUtil';
+
 // eslint-disable-next-line @typescript-eslint/no-empty-function
-const infinitePromise = new Promise<boolean>((): void => {});
+function noop(): void {}
 
 /**
  * A function that simulates the Array.some behaviour but on an array of Promises.
@@ -14,17 +16,36 @@ const infinitePromise = new Promise<boolean>((): void => {});
  * 2. throwing an error should be logically equivalent to returning false.
  */
 export async function promiseSome(predicates: Promise<boolean>[]): Promise<boolean> {
-  // These promises will only finish when their predicate returns true
-  const infinitePredicates = predicates.map(async(predicate): Promise<boolean> => predicate.then(
-    async(value): Promise<boolean> => value ? true : infinitePromise,
-    async(): Promise<boolean> => infinitePromise,
-  ));
+  return new Promise((resolve): void => {
+    function resolveIfTrue(value: boolean): void {
+      if (value) {
+        resolve(true);
+      }
+    }
+    Promise.all(predicates.map((predicate): Promise<void> => predicate.then(resolveIfTrue, noop)))
+      .then((): void => resolve(false), noop);
+  });
+}
 
-  // Returns after all predicates are resolved
-  const finalPromise = Promise.allSettled(predicates).then((results): boolean =>
-    results.some((result): boolean => result.status === 'fulfilled' && result.value));
+/**
+ * Obtains the values of all fulfilled promises.
+ * If there are rejections (and `ignoreErrors` is false), throws a combined error of all rejected promises.
+ */
+export async function allFulfilled<T>(promises: Promise<T> [], ignoreErrors = false): Promise<T[]> {
+  // Collect values and errors
+  const values: T[] = [];
+  const errors: Error[] = [];
+  for (const result of await Promise.allSettled(promises)) {
+    if (result.status === 'fulfilled') {
+      values.push(result.value);
+    } else if (!ignoreErrors) {
+      errors.push(result.reason);
+    }
+  }
 
-  // Either one of the infinitePredicates will return true,
-  // or finalPromise will return the result if none of them did or finalPromise was faster
-  return Promise.race([ ...infinitePredicates, finalPromise ]);
+  // Either throw or return
+  if (errors.length > 0) {
+    throw createAggregateError(errors);
+  }
+  return values;
 }
diff --git a/src/util/errors/ErrorUtil.ts b/src/util/errors/ErrorUtil.ts
index 84e94716e..405b29c80 100644
--- a/src/util/errors/ErrorUtil.ts
+++ b/src/util/errors/ErrorUtil.ts
@@ -1,5 +1,4 @@
 import { types } from 'util';
-import { HttpError } from './HttpError';
 
 /**
  * Checks if the input is an {@link Error}.
@@ -25,10 +24,3 @@ export function assertError(error: unknown): asserts error is Error {
 export function createErrorMessage(error: unknown): string {
   return isError(error) ? error.message : `Unknown error: ${error}`;
 }
-
-/**
- * Returns the HTTP status code corresponding to the error.
- */
-export function getStatusCode(error: Error): number {
-  return HttpError.isInstance(error) ? error.statusCode : 500;
-}
diff --git a/src/util/errors/HttpErrorUtil.ts b/src/util/errors/HttpErrorUtil.ts
new file mode 100644
index 000000000..a30bf64b1
--- /dev/null
+++ b/src/util/errors/HttpErrorUtil.ts
@@ -0,0 +1,38 @@
+import { BadRequestHttpError } from './BadRequestHttpError';
+import { createErrorMessage } from './ErrorUtil';
+import { HttpError } from './HttpError';
+import { InternalServerError } from './InternalServerError';
+
+/**
+ * Returns the HTTP status code corresponding to the error.
+ */
+export function getStatusCode(error: Error): number {
+  return HttpError.isInstance(error) ? error.statusCode : 500;
+}
+
+/**
+ * Combines a list of errors into a single HttpErrors.
+ * Status code depends on the input errors. If they all share the same status code that code will be re-used.
+ * If they are all within the 4xx range, 400 will be used, otherwise 500.
+ *
+ * @param errors - Errors to combine.
+ * @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages.
+ */
+export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'):
+HttpError {
+  const httpErrors = errors.map((error): HttpError =>
+    HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
+  const joined = httpErrors.map((error: Error): string => error.message).join(', ');
+  const message = `${messagePrefix} [${joined}]`;
+
+  // Check if all errors have the same status code
+  if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) {
+    return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message);
+  }
+
+  // Find the error range (4xx or 5xx)
+  if (httpErrors.some((error): boolean => error.statusCode >= 500)) {
+    return new InternalServerError(message);
+  }
+  return new BadRequestHttpError(message);
+}
diff --git a/src/util/handlers/HandlerUtil.ts b/src/util/handlers/HandlerUtil.ts
index efb61ea52..dfc436cf8 100644
--- a/src/util/handlers/HandlerUtil.ts
+++ b/src/util/handlers/HandlerUtil.ts
@@ -1,36 +1,7 @@
-import { BadRequestHttpError } from '../errors/BadRequestHttpError';
 import { createErrorMessage, isError } from '../errors/ErrorUtil';
-import { HttpError } from '../errors/HttpError';
-import { InternalServerError } from '../errors/InternalServerError';
+import { createAggregateError } from '../errors/HttpErrorUtil';
 import type { AsyncHandler } from './AsyncHandler';
 
-/**
- * Combines a list of errors into a single HttpErrors.
- * Status code depends on the input errors. If they all share the same status code that code will be re-used.
- * If they are all within the 4xx range, 400 will be used, otherwise 500.
- *
- * @param errors - Errors to combine.
- * @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages.
- */
-export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'):
-HttpError {
-  const httpErrors = errors.map((error): HttpError =>
-    HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
-  const joined = httpErrors.map((error: Error): string => error.message).join(', ');
-  const message = `${messagePrefix} [${joined}]`;
-
-  // Check if all errors have the same status code
-  if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) {
-    return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message);
-  }
-
-  // Find the error range (4xx or 5xx)
-  if (httpErrors.some((error): boolean => error.statusCode >= 500)) {
-    return new InternalServerError(message);
-  }
-  return new BadRequestHttpError(message);
-}
-
 /**
  * Finds a handler that can handle the given input data.
  * Otherwise an error gets thrown.
diff --git a/src/util/handlers/ParallelHandler.ts b/src/util/handlers/ParallelHandler.ts
index 3d78fb88b..b517257bb 100644
--- a/src/util/handlers/ParallelHandler.ts
+++ b/src/util/handlers/ParallelHandler.ts
@@ -12,12 +12,10 @@ export class ParallelHandler<TIn = void, TOut = void> extends AsyncHandler<TIn,
   }
 
   public async canHandle(input: TIn): Promise<void> {
-    // eslint-disable-next-line @typescript-eslint/promise-function-async
     await Promise.all(this.handlers.map((handler): Promise<void> => handler.canHandle(input)));
   }
 
   public async handle(input: TIn): Promise<TOut[]> {
-    // eslint-disable-next-line @typescript-eslint/promise-function-async
     return Promise.all(this.handlers.map((handler): Promise<TOut> => handler.handle(input)));
   }
 }
diff --git a/src/util/handlers/UnionHandler.ts b/src/util/handlers/UnionHandler.ts
index c165686fb..2a63656ca 100644
--- a/src/util/handlers/UnionHandler.ts
+++ b/src/util/handlers/UnionHandler.ts
@@ -1,31 +1,41 @@
+import { allFulfilled } from '../PromiseUtil';
 import { AsyncHandler } from './AsyncHandler';
-import { createAggregateError, filterHandlers, findHandler } from './HandlerUtil';
+import { filterHandlers, findHandler } from './HandlerUtil';
 
 // Helper types to make sure the UnionHandler has the same in/out types as the AsyncHandler type it wraps
-type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
+type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
 type InType<T extends AsyncHandler<any, any>> = Parameters<T['handle']>[0];
-type OutType<T extends AsyncHandler<any, any>> = ThenArg<ReturnType<T['handle']>>;
-type HandlerType<T extends AsyncHandler> = AsyncHandler<InType<T>, OutType<T>>;
+type OutType<T extends AsyncHandler<any, any>> = Awaited<ReturnType<T['handle']>>;
 
 /**
  * Utility handler that allows combining the results of multiple handlers into one.
- * Will run all the handlers and then call the abstract `combine` function with the results,
- * which should return the output of the class.
- *
- * If `requireAll` is true, the handler will fail if any of the handlers do not support the input.
- * If `requireAll` is false, only the handlers that support the input will be called,
- * only if all handlers reject the input will this handler reject as well.
- * With `requireAll` set to false, the length of the input array
- * for the `combine` function is variable (but always at least 1).
+ * Will run the handlers and then call the abstract `combine` function with the results,
+ * which then generates the handler's output.
  */
 export abstract class UnionHandler<T extends AsyncHandler<any, any>> extends AsyncHandler<InType<T>, OutType<T>> {
   protected readonly handlers: T[];
   private readonly requireAll: boolean;
+  private readonly ignoreErrors: boolean;
 
-  protected constructor(handlers: T[], requireAll = false) {
+  /**
+   * Creates a new `UnionHandler`.
+   *
+   * When `requireAll` is false or `ignoreErrors` is true,
+   * the length of the input to `combine` can vary;
+   * otherwise, it is exactly the number of handlers.
+   *
+   * @param handlers - The handlers whose output is to be combined.
+   * @param requireAll - If true, will fail if any of the handlers do not support the input.
+                         If false, only the handlers that support the input will be called;
+   *                     will fail only if none of the handlers can handle the input.
+   * @param ignoreErrors - If true, ignores handlers that fail by omitting their output;
+   *                       if false, fails when any handlers fail.
+   */
+  public constructor(handlers: T[], requireAll = false, ignoreErrors = !requireAll) {
     super();
     this.handlers = handlers;
     this.requireAll = requireAll;
+    this.ignoreErrors = ignoreErrors;
   }
 
   public async canHandle(input: InType<T>): Promise<void> {
@@ -38,57 +48,21 @@ export abstract class UnionHandler<T extends AsyncHandler<any, any>> extends Asy
   }
 
   public async handle(input: InType<T>): Promise<OutType<T>> {
-    let handlers: HandlerType<T>[];
-    if (this.requireAll) {
-      // Handlers were already checked in canHandle
-      // eslint-disable-next-line prefer-destructuring
-      handlers = this.handlers;
-    } else {
-      handlers = await filterHandlers(this.handlers, input);
-    }
-
-    const results = await Promise.all(
-      handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)),
-    );
-
-    return this.combine(results);
-  }
-
-  public async handleSafe(input: InType<T>): Promise<OutType<T>> {
-    let handlers: HandlerType<T>[];
-    if (this.requireAll) {
-      await this.allCanHandle(input);
-      // eslint-disable-next-line prefer-destructuring
-      handlers = this.handlers;
-    } else {
-      // This will error if no handler supports the input
-      handlers = await filterHandlers(this.handlers, input);
-    }
-
-    const results = await Promise.all(
-      handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)),
-    );
-
-    return this.combine(results);
+    const handlers = this.requireAll ? this.handlers : await filterHandlers(this.handlers, input);
+    const results = handlers.map((handler): Promise<OutType<T>> => handler.handle(input));
+    return this.combine(await allFulfilled(results, this.ignoreErrors));
   }
 
   /**
    * Checks if all handlers can handle the input.
    * If not, throw an error based on the errors of the failed handlers.
    */
-  private async allCanHandle(input: InType<T>): Promise<void> {
-    const results = await Promise.allSettled(this.handlers.map(async(handler): Promise<HandlerType<T>> => {
-      await handler.canHandle(input);
-      return handler;
-    }));
-    if (results.some(({ status }): boolean => status === 'rejected')) {
-      const errors = results.map((result): Error => (result as PromiseRejectedResult).reason);
-      throw createAggregateError(errors);
-    }
+  protected async allCanHandle(input: InType<T>): Promise<void> {
+    await allFulfilled(this.handlers.map((handler): Promise<void> => handler.canHandle(input)));
   }
 
   /**
-   * Combine the results of the handlers into a single output.
+   * Combines the results of the handlers into a single output.
    */
   protected abstract combine(results: OutType<T>[]): Promise<OutType<T>>;
 }


Test patch:
diff --git a/test/integration/Identity.test.ts b/test/integration/Identity.test.ts
index ccd4b925d..36468643c 100644
--- a/test/integration/Identity.test.ts
+++ b/test/integration/Identity.test.ts
@@ -378,7 +378,7 @@ describe('A Solid server with IDP', (): void => {
     });
 
     it('should return correct error output.', async(): Promise<void> => {
-      const res = await fetch(`${baseUrl}idp/auth`);
+      const res = await fetch(`${baseUrl}.oidc/auth`);
       expect(res.status).toBe(400);
       await expect(res.text()).resolves.toContain('InvalidRequest: invalid_request');
     });
diff --git a/test/integration/Setup.test.ts b/test/integration/Setup.test.ts
index 8cec739c4..b4be03ef3 100644
--- a/test/integration/Setup.test.ts
+++ b/test/integration/Setup.test.ts
@@ -51,7 +51,7 @@ describe('A Solid server with setup', (): void => {
 
     // Root access disabled
     res = await fetch(baseUrl);
-    expect(res.status).toBe(403);
+    expect(res.status).toBe(401);
 
     // Registration still possible
     const registerParams = { email, podName, password, confirmPassword: password, createWebId: true };
diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts
index 7fa961cf3..2c678af4b 100644
--- a/test/integration/WebSocketsProtocol.test.ts
+++ b/test/integration/WebSocketsProtocol.test.ts
@@ -1,5 +1,5 @@
 import fetch from 'cross-fetch';
-import WebSocket from 'ws';
+import { WebSocket } from 'ws';
 import type { App } from '../../src/init/App';
 import { getPort } from '../util/Util';
 import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
diff --git a/test/integration/config/server-without-auth.json b/test/integration/config/server-without-auth.json
index 963980f47..60a5b38c0 100644
--- a/test/integration/config/server-without-auth.json
+++ b/test/integration/config/server-without-auth.json
@@ -4,7 +4,7 @@
     "files-scs:config/app/main/default.json",
     "files-scs:config/app/init/initialize-root.json",
     "files-scs:config/app/setup/disabled.json",
-    "files-scs:config/http/handler/default.json",
+    "files-scs:config/http/handler/simple.json",
     "files-scs:config/http/middleware/websockets.json",
     "files-scs:config/http/server-factory/websockets.json",
     "files-scs:config/http/static/default.json",
@@ -26,9 +26,5 @@
     "files-scs:config/util/variables/default.json"
   ],
   "@graph": [
-    {
-      "@id": "urn:solid-server:default:IdentityProviderHandler",
-      "@type": "UnsupportedAsyncHandler"
-    }
   ]
 }
diff --git a/test/unit/authentication/UnionCredentialsExtractor.test.ts b/test/unit/authentication/UnionCredentialsExtractor.test.ts
index 1b69cb886..427f6ba40 100644
--- a/test/unit/authentication/UnionCredentialsExtractor.test.ts
+++ b/test/unit/authentication/UnionCredentialsExtractor.test.ts
@@ -43,4 +43,11 @@ describe('A UnionCredentialsExtractor', (): void => {
       [CredentialGroup.public]: {},
     });
   });
+
+  it('skips erroring handlers.', async(): Promise<void> => {
+    extractors[0].handle.mockRejectedValueOnce(new Error('error'));
+    await expect(extractor.handle(request)).resolves.toEqual({
+      [CredentialGroup.public]: {},
+    });
+  });
 });
diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts
index d11f8f367..4c246040c 100644
--- a/test/unit/identity/IdentityProviderHttpHandler.test.ts
+++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts
@@ -17,6 +17,7 @@ import type {
   RepresentationConverter,
   RepresentationConverterArgs,
 } from '../../../src/storage/conversion/RepresentationConverter';
+import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { joinUrl } from '../../../src/util/PathUtil';
 import { guardedStreamFrom, readableToString } from '../../../src/util/StreamUtil';
 import { CONTENT_TYPE, SOLID_HTTP, SOLID_META } from '../../../src/util/Vocabularies';
@@ -46,7 +47,6 @@ describe('An IdentityProviderHttpHandler', (): void => {
     };
 
     provider = {
-      callback: jest.fn(),
       interactionDetails: jest.fn(),
     } as any;
 
@@ -113,11 +113,9 @@ describe('An IdentityProviderHttpHandler', (): void => {
     handler = new IdentityProviderHttpHandler(args);
   });
 
-  it('calls the provider if there is no matching route.', async(): Promise<void> => {
+  it('throws a 404 if there is no matching route.', async(): Promise<void> => {
     operation.target.path = joinUrl(baseUrl, 'invalid');
-    await expect(handler.handle({ request, response, operation })).resolves.toBeUndefined();
-    expect(provider.callback).toHaveBeenCalledTimes(1);
-    expect(provider.callback).toHaveBeenLastCalledWith(request, response);
+    await expect(handler.handle({ request, response, operation })).rejects.toThrow(NotFoundHttpError);
   });
 
   it('creates Representations for InteractionResponseResults.', async(): Promise<void> => {
diff --git a/test/unit/identity/OidcHttpHandler.test.ts b/test/unit/identity/OidcHttpHandler.test.ts
new file mode 100644
index 000000000..61be21c83
--- /dev/null
+++ b/test/unit/identity/OidcHttpHandler.test.ts
@@ -0,0 +1,31 @@
+import type { Provider } from 'oidc-provider';
+import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory';
+import { OidcHttpHandler } from '../../../src/identity/OidcHttpHandler';
+import type { HttpRequest } from '../../../src/server/HttpRequest';
+import type { HttpResponse } from '../../../src/server/HttpResponse';
+
+describe('An OidcHttpHandler', (): void => {
+  const request: HttpRequest = {} as any;
+  const response: HttpResponse = {} as any;
+  let provider: jest.Mocked<Provider>;
+  let providerFactory: jest.Mocked<ProviderFactory>;
+  let handler: OidcHttpHandler;
+
+  beforeEach(async(): Promise<void> => {
+    provider = {
+      callback: jest.fn(),
+    } as any;
+
+    providerFactory = {
+      getProvider: jest.fn().mockResolvedValue(provider),
+    };
+
+    handler = new OidcHttpHandler(providerFactory);
+  });
+
+  it('sends all requests to the OIDC library.', async(): Promise<void> => {
+    await expect(handler.handle({ request, response })).resolves.toBeUndefined();
+    expect(provider.callback).toHaveBeenCalledTimes(1);
+    expect(provider.callback).toHaveBeenLastCalledWith(request, response);
+  });
+});
diff --git a/test/unit/identity/configuration/IdentityProviderFactory.test.ts b/test/unit/identity/configuration/IdentityProviderFactory.test.ts
index 3c8a0bcb0..6cb3c9024 100644
--- a/test/unit/identity/configuration/IdentityProviderFactory.test.ts
+++ b/test/unit/identity/configuration/IdentityProviderFactory.test.ts
@@ -12,23 +12,24 @@ jest.mock('oidc-provider', (): any => ({
 }));
 
 const routes = {
-  authorization: '/foo/idp/auth',
-  check_session: '/foo/idp/session/check',
-  code_verification: '/foo/idp/device',
-  device_authorization: '/foo/idp/device/auth',
-  end_session: '/foo/idp/session/end',
-  introspection: '/foo/idp/token/introspection',
-  jwks: '/foo/idp/jwks',
-  pushed_authorization_request: '/foo/idp/request',
-  registration: '/foo/idp/reg',
-  revocation: '/foo/idp/token/revocation',
-  token: '/foo/idp/token',
-  userinfo: '/foo/idp/me',
+  authorization: '/foo/oidc/auth',
+  check_session: '/foo/oidc/session/check',
+  code_verification: '/foo/oidc/device',
+  device_authorization: '/foo/oidc/device/auth',
+  end_session: '/foo/oidc/session/end',
+  introspection: '/foo/oidc/token/introspection',
+  jwks: '/foo/oidc/jwks',
+  pushed_authorization_request: '/foo/oidc/request',
+  registration: '/foo/oidc/reg',
+  revocation: '/foo/oidc/token/revocation',
+  token: '/foo/oidc/token',
+  userinfo: '/foo/oidc/me',
 };
 
 describe('An IdentityProviderFactory', (): void => {
   let baseConfig: Configuration;
   const baseUrl = 'http://test.com/foo/';
+  const oidcPath = '/oidc';
   const idpPath = '/idp';
   const webId = 'http://alice.test.com/card#me';
   let adapterFactory: jest.Mocked<AdapterFactory>;
@@ -59,6 +60,7 @@ describe('An IdentityProviderFactory', (): void => {
     factory = new IdentityProviderFactory(baseConfig, {
       adapterFactory,
       baseUrl,
+      oidcPath,
       idpPath,
       storage,
       errorHandler,
@@ -70,6 +72,7 @@ describe('An IdentityProviderFactory', (): void => {
     expect((): any => new IdentityProviderFactory(baseConfig, {
       adapterFactory,
       baseUrl,
+      oidcPath,
       idpPath: 'idp',
       storage,
       errorHandler,
@@ -127,6 +130,7 @@ describe('An IdentityProviderFactory', (): void => {
     factory = new IdentityProviderFactory(baseConfig, {
       adapterFactory,
       baseUrl,
+      oidcPath,
       idpPath,
       storage,
       errorHandler,
@@ -148,6 +152,7 @@ describe('An IdentityProviderFactory', (): void => {
     const factory2 = new IdentityProviderFactory(baseConfig, {
       adapterFactory,
       baseUrl,
+      oidcPath,
       idpPath,
       storage,
       errorHandler,
diff --git a/test/unit/server/WebSocketServerFactory.test.ts b/test/unit/server/WebSocketServerFactory.test.ts
index 4e422aa0a..7605cf4cd 100644
--- a/test/unit/server/WebSocketServerFactory.test.ts
+++ b/test/unit/server/WebSocketServerFactory.test.ts
@@ -1,6 +1,6 @@
 import type { Server } from 'http';
 import request from 'supertest';
-import WebSocket from 'ws';
+import { WebSocket } from 'ws';
 import { BaseHttpServerFactory } from '../../../src/server/BaseHttpServerFactory';
 import type { HttpHandlerInput } from '../../../src/server/HttpHandler';
 import { HttpHandler } from '../../../src/server/HttpHandler';
diff --git a/test/unit/storage/patch/SparqlUpdatePatcher.test.ts b/test/unit/storage/patch/SparqlUpdatePatcher.test.ts
index 01386217a..6bb035a1f 100644
--- a/test/unit/storage/patch/SparqlUpdatePatcher.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatcher.test.ts
@@ -2,6 +2,7 @@ import 'jest-rdf';
 import { namedNode, quad } from '@rdfjs/data-model';
 import arrayifyStream from 'arrayify-stream';
 import type { Quad } from 'rdf-js';
+import type { Algebra } from 'sparqlalgebrajs';
 import { translate } from 'sparqlalgebrajs';
 import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
 import type { Representation } from '../../../../src/http/representation/Representation';
@@ -15,7 +16,7 @@ import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
 function getPatch(query: string): SparqlUpdatePatch {
   const prefixedQuery = `prefix : <http://test.com/>\n${query}`;
   return {
-    algebra: translate(prefixedQuery, { quads: true }),
+    algebra: translate(prefixedQuery, { quads: true }) as Algebra.Update,
     data: guardedStreamFrom(prefixedQuery),
     metadata: new RepresentationMetadata(),
     binary: true,
diff --git a/test/unit/util/errors/ErrorUtil.test.ts b/test/unit/util/errors/ErrorUtil.test.ts
index be242152a..49eaf7c36 100644
--- a/test/unit/util/errors/ErrorUtil.test.ts
+++ b/test/unit/util/errors/ErrorUtil.test.ts
@@ -1,5 +1,4 @@
-import { assertError, createErrorMessage, getStatusCode, isError } from '../../../../src/util/errors/ErrorUtil';
-import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+import { assertError, createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil';
 
 describe('ErrorUtil', (): void => {
   describe('#isError', (): void => {
@@ -39,14 +38,4 @@ describe('ErrorUtil', (): void => {
       expect(createErrorMessage('apple')).toBe('Unknown error: apple');
     });
   });
-
-  describe('#getStatusCode', (): void => {
-    it('returns the corresponding status code for HttpErrors.', async(): Promise<void> => {
-      expect(getStatusCode(new NotFoundHttpError())).toBe(404);
-    });
-
-    it('returns 500 for other errors.', async(): Promise<void> => {
-      expect(getStatusCode(new Error('404'))).toBe(500);
-    });
-  });
 });
diff --git a/test/unit/util/errors/HttpErrorUtil.test.ts b/test/unit/util/errors/HttpErrorUtil.test.ts
new file mode 100644
index 000000000..d257bf9fb
--- /dev/null
+++ b/test/unit/util/errors/HttpErrorUtil.test.ts
@@ -0,0 +1,50 @@
+import { HttpError } from '../../../../src/util/errors/HttpError';
+import { createAggregateError, getStatusCode } from '../../../../src/util/errors/HttpErrorUtil';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
+
+describe('ErrorUtil', (): void => {
+  describe('createAggregateError', (): void => {
+    const error401 = new HttpError(401, 'UnauthorizedHttpError');
+    const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');
+    const error501 = new HttpError(501, 'NotImplementedHttpError');
+    const error = new Error('noStatusCode');
+
+    it('throws an error with matching status code if all errors have the same.', async(): Promise<void> => {
+      expect(createAggregateError([ error401, error401 ])).toMatchObject({
+        statusCode: 401,
+        name: 'UnauthorizedHttpError',
+      });
+    });
+
+    it('throws an InternalServerError if one of the errors has status code 5xx.', async(): Promise<void> => {
+      expect(createAggregateError([ error401, error501 ])).toMatchObject({
+        statusCode: 500,
+        name: 'InternalServerError',
+      });
+    });
+
+    it('throws an BadRequestHttpError if all handlers have 4xx status codes.', async(): Promise<void> => {
+      expect(createAggregateError([ error401, error415 ])).toMatchObject({
+        statusCode: 400,
+        name: 'BadRequestHttpError',
+      });
+    });
+
+    it('interprets non-HTTP errors as internal errors.', async(): Promise<void> => {
+      expect(createAggregateError([ error ])).toMatchObject({
+        statusCode: 500,
+        name: 'InternalServerError',
+      });
+    });
+  });
+
+  describe('#getStatusCode', (): void => {
+    it('returns the corresponding status code for HttpErrors.', async(): Promise<void> => {
+      expect(getStatusCode(new NotFoundHttpError())).toBe(404);
+    });
+
+    it('returns 500 for other errors.', async(): Promise<void> => {
+      expect(getStatusCode(new Error('404'))).toBe(500);
+    });
+  });
+});
diff --git a/test/unit/util/handlers/HandlerUtil.test.ts b/test/unit/util/handlers/HandlerUtil.test.ts
index 9cef18305..718d03900 100644
--- a/test/unit/util/handlers/HandlerUtil.test.ts
+++ b/test/unit/util/handlers/HandlerUtil.test.ts
@@ -1,44 +1,8 @@
-import { HttpError } from '../../../../src/util/errors/HttpError';
 import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
-import { createAggregateError, filterHandlers, findHandler } from '../../../../src/util/handlers/HandlerUtil';
+import { filterHandlers, findHandler } from '../../../../src/util/handlers/HandlerUtil';
 import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
 
 describe('HandlerUtil', (): void => {
-  describe('createAggregateError', (): void => {
-    const error401 = new HttpError(401, 'UnauthorizedHttpError');
-    const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');
-    const error501 = new HttpError(501, 'NotImplementedHttpError');
-    const error = new Error('noStatusCode');
-
-    it('throws an error with matching status code if all errors have the same.', async(): Promise<void> => {
-      expect(createAggregateError([ error401, error401 ])).toMatchObject({
-        statusCode: 401,
-        name: 'UnauthorizedHttpError',
-      });
-    });
-
-    it('throws an InternalServerError if one of the errors has status code 5xx.', async(): Promise<void> => {
-      expect(createAggregateError([ error401, error501 ])).toMatchObject({
-        statusCode: 500,
-        name: 'InternalServerError',
-      });
-    });
-
-    it('throws an BadRequestHttpError if all handlers have 4xx status codes.', async(): Promise<void> => {
-      expect(createAggregateError([ error401, error415 ])).toMatchObject({
-        statusCode: 400,
-        name: 'BadRequestHttpError',
-      });
-    });
-
-    it('interprets non-HTTP errors as internal errors.', async(): Promise<void> => {
-      expect(createAggregateError([ error ])).toMatchObject({
-        statusCode: 500,
-        name: 'InternalServerError',
-      });
-    });
-  });
-
   describe('findHandler', (): void => {
     let handlerTrue: AsyncHandler<any, any>;
     let handlerFalse: AsyncHandler<any, any>;
diff --git a/test/unit/util/handlers/UnionHandler.test.ts b/test/unit/util/handlers/UnionHandler.test.ts
index 27a803054..fc409e3d6 100644
--- a/test/unit/util/handlers/UnionHandler.test.ts
+++ b/test/unit/util/handlers/UnionHandler.test.ts
@@ -2,10 +2,6 @@ import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
 import { UnionHandler } from '../../../../src/util/handlers/UnionHandler';
 
 class SimpleUnionHandler extends UnionHandler<AsyncHandler<any, string>> {
-  public constructor(handlers: AsyncHandler<any, any>[], requireAll?: boolean) {
-    super(handlers, requireAll);
-  }
-
   protected async combine(results: string[]): Promise<string> {
     return results.join('');
   }
@@ -61,4 +57,25 @@ describe('A UnionHandler', (): void => {
     handlers[0].canHandle.mockRejectedValue(new Error('bad request'));
     await expect(handler.handle(input)).resolves.toBe('ab');
   });
+
+  it('requires all handlers to succeed if requireAll is true.', async(): Promise<void> => {
+    handler = new SimpleUnionHandler(handlers, true);
+
+    handlers[0].handle.mockRejectedValue(new Error('bad request'));
+    await expect(handler.handleSafe(input)).rejects.toThrow('bad request');
+  });
+
+  it('does not require all handlers to succeed if ignoreErrors is true.', async(): Promise<void> => {
+    handler = new SimpleUnionHandler(handlers, true, true);
+
+    handlers[0].handle.mockRejectedValueOnce(new Error('bad request'));
+    await expect(handler.handleSafe(input)).resolves.toBe('b');
+
+    handlers[1].handle.mockRejectedValueOnce(new Error('bad request'));
+    await expect(handler.handleSafe(input)).resolves.toBe('a');
+
+    handlers[0].handle.mockRejectedValueOnce(new Error('bad request'));
+    handlers[1].handle.mockRejectedValueOnce(new Error('bad request'));
+    await expect(handler.handleSafe(input)).resolves.toBe('');
+  });
 });