New Upstream Release - undertow

Ready changes

Summary

Merged new upstream version: 2.2.24+ds (was: 2.2.22).

Diff

diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..d5316d3
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+@undertow-io/codeowners
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..628e317
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,224 @@
+Contributing Guide
+==================
+
+Bug fixes and documentation improvements are welcome! If you want to contribute, I suggest you have a look at our [Jira project](https://issues.redhat.com/projects/UNDERTOW "Undertow Jira") and get in touch with us via [Zulip chat](https://wildfly.zulipchat.com/#narrow/stream/174183-undertow "#undertow").
+
+
+PRs must be submitted to master branch (soon to be [renamed to main](https://issues.redhat.com/browse/UNDERTOW-2043)) and they should:
+- state clearly what they do (see more [here](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html))
+- point to associated Jira (see more on [link par aa sesscao do Jira])
+- contain a test case, unless existing tests already verify the code added by the PR
+- have a license header in all new files, with current year’s number
+- pass CI (except for known failures, we are working on fixing those, tracked by [UNDERTOW-1523](https://issues.redhat.com/browse/UNDERTOW-1523))
+
+If your PR is incomplete, the reviewer might request you add the missing bits or add them for you if that is simple enough (for
+that to be possible, though, you need to check the [Allow edits from maintainers](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) box)
+
+We expect all contributors and users to follow our [Code of Conduct](CODE_OF_CONDUCT.md) when communicating through project channels. These include, but are not limited to: chat, issues, code.
+
+# Issues
+
+Undertow uses JIRA to manage issues. All issues can be found [here](https://issues.redhat.com/projects/Undertow/issues).
+
+To create a new issue, comment on an existing issue, or assign an issue to yourself, you'll need to first [create a JIRA account](https://issues.redhat.com/).
+
+## Good First Issues
+
+Want to contribute to Undertow but aren't quite sure where to start? Check out our issues with the `good-first-issue` label. These are a triaged set of issues that are great for getting started on our project. These can be found [here](https://issues.redhat.com/issues/?jql=project%20%3D%20Undertow%20%20and%20labels%20in%20(%22good-first-issue%22)).
+
+Once you have selected an issue you would like to work on, make sure it's not already assigned to someone else. To assign an issue to yourself, simply click on "Start Progress". This will automatically assign the issue to you.
+
+## Discussing your Planned Changes
+
+If you want feedback, you can discuss your planned changes in any of the following ways: 
+* add comments to the issue ticket at [Undertow Jira](https://issues.jboss.org/browse/UNDERTOW)
+* Undertow Dev Google Group
+* Undertow [Zulip chat](https://wildfly.zulipchat.com/#narrow/stream/174183-undertow "#undertow").
+* or simply create a draft PR and poing in the PR description that you would like feedback on the proposal before getting to the
+final solution
+
+
+PR Review Process
+--------------------------------------------
+
+PR reviewers will take into account the following aspects when reviewing your PR:
+- correctness: the code must be correct
+- performance impact: if there are negative performance impacts in the code, careful consideration must be taken whereas the impact could be eliminated and, in case it cannot, if the new code should be accepted
+- code style: keep your code style consistent with the classes you are editing, such as variable names, ordering of methods, etc
+- scope of the fix: this is a very important factor. Sometimes, the fix should be applied to a broader range of classes, such as a bug that repeats itself in other parts of the code. Other times, the PR solves a bug only partially, because the bug has a broader impact than initially evaluated.
+- is the proposed fix the best approach for the Jira at hand?
+- backwards compatibility: we must prevent any PR that breaks compatibility with previous versions. If the PR| does so, it could still be okay, but this should be clearly documented it will probably be discussed by the project maintainers before being merged
+- security impact: it is critical to evaluate if the PR has any sort of security impact, preventing the addition of exploitable flaws.
+
+Your PR will be classified by the reviewer with one or more of the following labels: **bug fix**, **enhancement**, **new feature/API change**, and **dependency upgrade**.
+
+Besides the classifications labels, a series of labels are going to be added to your PR while it is under review. This is the full list of labels and what they mean:
+- **waiting CI check**  PR is ready to be merged, but we are waiting for CI results. The use of this label is optional.
+- **waiting PR update**  reviewer has requested changes to the PR
+- **failed CI**  a new failure was introduced to CI
+- **question** reviewer has asked one or more questions to the contributor, so the PR can be better assessed
+- **under verification**  reviewer will perform some extra verifications before giving their feedback (usually this means running reproducers, reviewing specs, and the like)
+- **waiting peer review** PR has been reviewed but is waiting on a second review before being merged  (as the changes affects core classes or adds a new feature)
+- **next release** PR is in the payload of the next release based on master branch
+- **maintenance branch** this tag is used for PRs submitted to maintenance branches only, and is included here just for completeness. Maintainers will take care of backporting
+submittted fixes to the maintenance branches when needed 
+
+#GitHub Quickstart
+
+If this is your first time contributing to a GitHub project, you can follow the next steps to get up to speed when contributing to
+Undertow. Regardless of your level of experience, though, we kindly ask you that PRs are always rebased before being submitted or
+updated.
+
+## One time setup
+
+### Create a GitHub account
+
+If you don't have one already, head to https://github.com/
+
+### Fork Undertow
+
+Fork https://github.com/undertow-io/undertow into your GitHub account.
+
+### Clone your newly forked repository onto your local machine
+
+```bash
+git clone git@github.com:[your username]/undertow.git
+cd console
+```
+
+### Add a remote reference to upstream
+
+This makes it easy to pull down changes in the project over time
+
+```bash
+git remote add upstream git://github.com/undertow-io/undertow.git
+```
+
+## Development Process
+
+This is the typical process you would follow to submit any changes to Undertow.
+
+### Pulling updates from upstream
+
+```bash
+git pull --rebase upstream main
+```
+
+> Note that --rebase will automatically move your local commits, if you have
+> any, on top of the latest branch you pull from.
+> If you don't have any commits it is safe to leave off, but for safety it
+> doesn't hurt to use it each time just in case you have a commit you've
+> forgotten about!
+
+### Create a simple topic branch to isolate your work (recommended)
+
+```bash
+git checkout -b my_cool_feature
+```
+
+If you have a Jira number for the fix, having the Jira name in the branch is very useful to keep track of your changes:
+
+```bash
+git checkout -b UNDERTOW-XXXX
+```
+or
+```bash
+git checkout -b UNDERTOW-XXXX-my_cool_feature
+```
+
+
+### Make the changes
+
+Make whatever code changes, including new tests to verify your change, and make sure the project builds without errors:
+
+```bash
+mvn clean verify
+```
+
+> If you're making non code changes, the above step is not required.
+
+### Commit changes
+
+Add whichever files were changed into 'staging' before performing a commit:
+
+```bash
+git commit -v
+```
+The `-v` parameter is advisable if you want to check when writing the commit message all the changes that are included in your
+commit.
+
+### Rebase changes against master
+
+Once all your commits for the issue have been made against your local topic branch, we need to rebase it against branch master in upstream to ensure that your commits are added on top of the current state of master. This will make it easier to incorporate your changes into the master branch, especially if there has been any significant time passed since you rebased at the beginning.
+
+```bash
+git pull --rebase upstream master
+```
+
+### Push to your repo
+
+Now that you've sync'd your topic branch with upstream, it's time to push it to your GitHub repo.
+
+```bash
+git push origin UNDERTOW-XXXX-my_cool_feature
+```
+
+### Getting your changes merged into upstream, a pull request
+
+Now your updates are in your GitHub repo, you will need to notify the project that you have code/docs for inclusion.
+
+* Send a pull request, by clicking the pull request link while in your repository fork
+* After review a maintainer will merge your pull request, update/resolve associated issues, and reply when complete
+* Lastly, switch back to branch master from your topic branch and pull the updates
+
+```bash
+git checkout master
+git pull upstream master
+```
+
+* You may also choose to update your origin on GitHub as well
+
+```bash
+git push origin
+```
+
+#### Updating a PR
+
+After you get feedback from reviewers and the community, you might need to update your PR before it is merged.
+If the original commit is too big, you can do an incremental, new commit, to facilitate review of the changes in the PR.
+
+However, if you want to edit your previous commit, you can easily do so by amending it:
+
+
+```bash
+git commit --amend -v
+```
+
+Don't forget to add the changes you want incorporated to your commit before amending it.
+
+If your PR contains more than one commit and you need to edit a commit that is not the latest, it cannot be amended as above,
+but you can use the interactive magic rebase. The command below allows you to amend, merge, delete or simply reword the latest
+X commits in the current branch (replace X by the correct number for your case):
+
+```bash
+git rebase -i HEAD~X
+```
+
+Then just follow the instructions to indicate the changes you need to do.
+
+It is a good practice to create a backup of your original branch in case you end up doing a mistake. That way, you can just
+reload your original fix (the GitHub remote origin account containing the PR can serve this purpose, as long as you don't
+overwrite it with a broken branch).
+
+Once you are satisfied if your commits, run the tests again with `mvn clean verify`. Finally, check the changes your are going
+to push to origin are really okay with:
+
+```bash
+git log -p
+```
+
+Only then, you can push it to origin. If you edited a commit that was already in the PR, you will need to force the push with `-f`:
+
+```bash
+git push origin --foce UNDERTOW-XXXX-my_cool_feature
+```
\ No newline at end of file
diff --git a/README.md b/README.md
index f1f0aee..6b70943 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ Undertow
 Undertow is a Java web server based on non-blocking IO. It consists of a few different parts:
 
 - A core HTTP server that supports both blocking and non-blocking IO
-- A Servlet 4.0/5.0 implementation
+- A Servlet 4.0/5.0/6.0 implementation
 - A JSR-356/Jakarta 2.0 compliant Web Socket implementation
 
 Website: http://undertow.io
@@ -17,40 +17,6 @@ Undertow Dev Group: https://groups.google.com/g/undertow-dev/
 
 Zulip Chat: https://wildfly.zulipchat.com stream [#undertow](https://wildfly.zulipchat.com/#narrow/stream/174183-undertow)
 
-Contributing to Undertow - PR Review Process
---------------------------------------------
-
-Bug fixes and documentation improvements are welcome! If you want to contribute and are not sure where to start, I suggest you have a look at our [Jira project](https://issues.redhat.com/projects/UNDERTOW "Undertow Jira") and get in touch with us via [Zulip chat](https://wildfly.zulipchat.com/#narrow/stream/174183-undertow "#undertow").
-
-PRs must be submitted to master branch (soon to be [renamed to main](https://issues.redhat.com/browse/UNDERTOW-2043)) and they should:
-- state clearly what they do
-- point to associated Jira
-- contain a test case, unless existing tests already verify the code added by the PR
-- have a license header in all new files, with current year’s number
-- pass CI (except for known failures, we are working on fixing those, tracked by [UNDERTOW-1523](https://issues.redhat.com/browse/UNDERTOW-1523))
-
-If your PR is incomplete, the reviewer might request you add the missing bits or add them for you if that is simple enough.
-
-PR reviewers will take into account the following aspects when reviewing your PR:
-- correctness: the code must be correct
-- performance impact: if there are negative performance impacts in the code, careful consideration must be taken whereas the impact could be eliminated and, in case it cannot, if the new code should be accepted
-- code style: keep your code style consistent with the classes you are editing, such as variable names, ordering of methods, etc
-- scope of the fix: this is a very important factor. Sometimes, the fix should be applied to a broader range of classes, such as a bug that repeats itself in other parts of the code. Other times, the PR solves a bug only partially, because the bug has a broader impact than initially evaluated.
-- is the proposed fix the best approach for the Jira at hand?
-- backwards compatibility: we must prevent any PR that breaks compatibility with previous versions
-- security impact: it is critical to evaluate if the PR has any sort of security impact, preventing the addition of exploitable flaws.
-
-Your PR will be classified by the reviewer with one or more of the following labels: **bug fix**, **enhancement**, **new feature/API change**, and **dependency upgrade**.
-
-Besides the classifications labels, a series of labels are going to be added to your PR while it is under review. This is the full list of labels and what they mean:
-- **waiting CI check**  PR is ready to be merged, but we are waiting for CI results. The use of this label is optional.
-- **waiting PR update**  reviewer has requested changes to the PR
-- **failed CI**  a new failure was introduced to CI
-- **question** reviewer has asked one or more questions to the contributor, so the PR can be better assessed
-- **under verification**  reviewer will perform some extra verifications before giving their feedback (usually this means running reproducers, reviewing specs, and the like)
-- **waiting peer review** PR has been reviewed but is waiting on a second review before being merged  (as the changes affects core classes or adds a new feature)
-- **next release** PR is in the payload of the next release
-
 Notifying Security Relevant Bugs
 --------------------------------
 
diff --git a/SECURITY.md b/SECURITY.md
index f528f10..b8b88c6 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -5,12 +5,14 @@
 The following versions of Undertow are supported with security updates:
 
 | Version | Supported          |
-| ------- | ------------------ |
-| < 2.0   | :x:                |
-| 2.0.x   | :white_check_mark: |
-| 2.1.x   | :x:                |
-| > 2.2.x | :white_check_mark: |
+|---------|--------------------|
+| < 2.2.x | :x:                |
+| 2.2.x   | :white_check_mark: |
+| 2.3.x   | :white_check_mark: |
+
 
 ## Reporting a Vulnerability
 
-If you find a vulnerability, send an email to secalert@redhat.com. To expedite things, you can copy Flavia Rainone (frainone@redhat.com).
+If you find a vulnerability, please send an email to secalert@redhat.com. To expedite things, you can copy Flavia Rainone (frainone@redhat.com).
+This will ensure the bug is properly handled without causing unnecessary negative impacts for the Undertow's user base.
+You can find more information about the security procedures at [this page](https://access.redhat.com/security/team/contact "Security Contacts and Procedures").
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 0ae9dc9..9da6169 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -25,11 +25,11 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <artifactId>undertow-benchmarks</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Benchmarks</name>
 
diff --git a/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncoders.java b/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncoders.java
new file mode 100644
index 0000000..b878717
--- /dev/null
+++ b/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncoders.java
@@ -0,0 +1,166 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2023 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package io.undertow.benchmarks;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class AsciiEncoders {
+
+    public interface BufferFlusher {
+        void flushBuffer(ByteBuffer buffer) throws IOException;
+    }
+
+    public interface AsciiEncoder {
+
+        int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException;
+
+    }
+
+    public enum BatchFixedBufferOffsetAsciiEncoder implements AsciiEncoder {
+        Instance;
+
+        @Override
+        public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
+            int i = start;
+            while (i < end) {
+                final int bufferPos = buffer.position();
+                final int bufferRemaining = buffer.remaining();
+                final int sRemaining = end - i;
+                final int remaining = Math.min(sRemaining, bufferRemaining);
+                final int written = setAsciiBE(buffer, bufferPos, chars, i, remaining);
+                i += written;
+                buffer.position(bufferPos + written);
+                if (!buffer.hasRemaining()) {
+                    flusher.flushBuffer(buffer);
+                }
+                // we have given up with the fast path? slow path NOW!
+                if (written < remaining) {
+                    return i;
+                }
+            }
+            return i;
+        }
+
+        private static int setAsciiBE(ByteBuffer buffer, int out, char[] chars, int off, int len) {
+            final int longRounds = len >>> 3;
+            for (int i = 0; i < longRounds; i++) {
+                final long batch1 = (((long) chars[off]) << 48) |
+                        (((long) chars[off + 2]) << 32) |
+                        chars[off + 4] << 16 |
+                        chars[off + 6];
+                final long batch2 = (((long) chars[off + 1]) << 48) |
+                        (((long) chars[off + 3]) << 32) |
+                        chars[off + 5] << 16 |
+                        chars[off + 7];
+                if (((batch1 | batch2) & 0xff80_ff80_ff80_ff80L) != 0) {
+                    return i << 3;
+                }
+                final long batch = (batch1 << 8) | batch2;
+                buffer.putLong(out, batch);
+                out += Long.BYTES;
+                off += Long.BYTES;
+            }
+            final int byteRounds = len & 7;
+            if (byteRounds > 0) {
+                for (int i = 0; i < byteRounds; i++) {
+                    final char c = chars[off + i];
+                    if (c > 127) {
+                        return (longRounds << 3) + i;
+                    }
+                    buffer.put(out + i, (byte) c);
+                }
+            }
+            return len;
+        }
+    }
+
+    public enum NonBatchFixedBufferOffsetAsciiEncoder implements AsciiEncoder {
+        Instance;
+
+        @Override
+        public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
+            int i = start;
+            while (i < end) {
+                final int bufferPos = buffer.position();
+                final int bufferRemaining = buffer.remaining();
+                final int sRemaining = end - i;
+                final int remaining = Math.min(sRemaining, bufferRemaining);
+                final int written = setAscii(buffer, bufferPos, chars, i, remaining);
+                i += written;
+                buffer.position(bufferPos + written);
+                if (!buffer.hasRemaining()) {
+                    flusher.flushBuffer(buffer);
+                }
+                // we have given up with the fast path? slow path NOW!
+                if (written < remaining) {
+                    return i;
+                }
+            }
+            return i;
+        }
+
+        private static int setAscii(ByteBuffer buffer, int out, char[] chars, int off, int len) {
+            for (int i = 0; i < len; i++) {
+                final char c = chars[off + i];
+                if (c > 127) {
+                    return i;
+                }
+                buffer.put(out + i, (byte) c);
+            }
+            return len;
+        }
+    }
+
+    public enum NonBatchMutableBufferOffsetAsciiEncoder implements AsciiEncoder {
+        Instance;
+
+        @Override
+        public int writeAndFlushAscii(BufferFlusher flusher, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
+            //fast path, basically we are hoping this is ascii only
+            int remaining = buffer.remaining();
+            boolean ok = true;
+            //so we have a pure ascii buffer, just write it out and skip all the encoder cost
+            int i = start;
+            int flushPos = i + remaining;
+            while (ok && i < end) {
+                int realEnd = Math.min(end, flushPos);
+                for (; i < realEnd; ++i) {
+                    char c = chars[i];
+                    if (c > 127) {
+                        ok = false;
+                        break;
+                    } else {
+                        buffer.put((byte) c);
+                    }
+                }
+                if (i == flushPos) {
+                    flusher.flushBuffer(buffer);
+                    flushPos = i + buffer.remaining();
+                }
+            }
+            if (ok) {
+                return end - start;
+            }
+            return -1;
+        }
+    }
+
+
+}
diff --git a/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncodingBenchmark.java b/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncodingBenchmark.java
new file mode 100644
index 0000000..22ba988
--- /dev/null
+++ b/benchmarks/src/main/java/io/undertow/benchmarks/AsciiEncodingBenchmark.java
@@ -0,0 +1,96 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2023 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package io.undertow.benchmarks;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.CompilerControl;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.SplittableRandom;
+import java.util.concurrent.TimeUnit;
+
+@State(Scope.Benchmark)
+@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
+@Warmup(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class AsciiEncodingBenchmark implements AsciiEncoders.BufferFlusher {
+
+    private static final int ASCII_GEN_SEED = 0;
+    @Param({"7", "19", "248", "12392", "493727"})
+    private int inLength;
+    @Param({"8196"})
+    private int outCapacity;
+
+    @Param({"batch", "noBatch", "vanilla"})
+    private String encoderType;
+    private ByteBuffer out;
+    private char[] input;
+
+    private AsciiEncoders.AsciiEncoder encoder;
+
+    @Setup
+    public void init() {
+        out = ByteBuffer.allocateDirect(outCapacity).order(ByteOrder.BIG_ENDIAN);
+        SplittableRandom random = new SplittableRandom(ASCII_GEN_SEED);
+        input = new char[inLength];
+        for (int i = 0; i < inLength; i++) {
+            input[i] = (char) random.nextInt(0, 128);
+        }
+        switch (encoderType) {
+            case "batch":
+                encoder = AsciiEncoders.BatchFixedBufferOffsetAsciiEncoder.Instance;
+                break;
+            case "noBatch":
+                encoder = AsciiEncoders.NonBatchFixedBufferOffsetAsciiEncoder.Instance;
+                break;
+            case "vanilla":
+                encoder = AsciiEncoders.NonBatchMutableBufferOffsetAsciiEncoder.Instance;
+                break;
+        }
+    }
+
+    @Override
+    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
+    public void flushBuffer(ByteBuffer buffer) throws IOException {
+        buffer.position(0);
+    }
+
+    @Benchmark
+    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
+    public int encode() throws IOException {
+        final ByteBuffer out = this.out;
+        final char[] input = this.input;
+        out.clear();
+        return encoder.writeAndFlushAscii(this, out, this.input, 0, input.length);
+    }
+}
diff --git a/core/pom.xml b/core/pom.xml
index d378cbd..6890ca7 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-core</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Core</name>
 
diff --git a/core/src/main/java/io/undertow/UndertowMessages.java b/core/src/main/java/io/undertow/UndertowMessages.java
index a3e521e..138da8d 100644
--- a/core/src/main/java/io/undertow/UndertowMessages.java
+++ b/core/src/main/java/io/undertow/UndertowMessages.java
@@ -643,4 +643,7 @@ public interface UndertowMessages {
 
     @Message(id = 206, value = "Path '%s' is not a directory")
     IOException pathElementIsRegularFile(Path path);
+
+    @Message(id = 207, value = "Invalid SNI hostname '%s'")
+    IllegalArgumentException invalidSniHostname(String hostNameValue, @Cause Throwable t);
 }
diff --git a/core/src/main/java/io/undertow/UndertowOptions.java b/core/src/main/java/io/undertow/UndertowOptions.java
index 52691c8..57f0513 100644
--- a/core/src/main/java/io/undertow/UndertowOptions.java
+++ b/core/src/main/java/io/undertow/UndertowOptions.java
@@ -117,11 +117,25 @@ public class UndertowOptions {
      * this is disabled by default.
      * <p>
      * Defaults to false
-     *
+     * <p>
      * See <a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450">CVE-2007-0450</a>
+     * @deprecated - this option was interpreted improperly.
+     * @See {@link #DECODE_SLASH}
      */
     public static final Option<Boolean> ALLOW_ENCODED_SLASH = Option.simple(UndertowOptions.class, "ALLOW_ENCODED_SLASH", Boolean.class);
 
+    /**
+     * If a request comes in with encoded / characters (i.e. %2F), will these be decoded.
+     * <p>
+     * This can cause security problems if a front end proxy does not perform the same decoding, and as a result
+     * this is disabled by default.
+     * <p>
+     * If this option is set explicitly, the {@link #ALLOW_ENCODED_SLASH} is ignored. Should default to <b>true</b>
+     * <p>
+     * See <a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450">CVE-2007-0450</a>
+     */
+    public static final Option<Boolean> DECODE_SLASH = Option.simple(UndertowOptions.class, "DECODE_SLASH", Boolean.class);
+
     /**
      * If this is true then the parser will decode the URL and query parameters using the selected character encoding (UTF-8 by default). If this is false they will
      * not be decoded. This will allow a later handler to decode them into whatever charset is desired.
@@ -344,7 +358,7 @@ public class UndertowOptions {
     public static final Option<Integer> SHUTDOWN_TIMEOUT = Option.simple(UndertowOptions.class, "SHUTDOWN_TIMEOUT", Integer.class);
 
     /**
-     * The endpoint identification algorithm.
+     * The endpoint identification algorithm, the empty string can be used to set none.
      *
      * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
      */
diff --git a/core/src/main/java/io/undertow/client/http/HttpClientProvider.java b/core/src/main/java/io/undertow/client/http/HttpClientProvider.java
index b9dffab..8699b2b 100644
--- a/core/src/main/java/io/undertow/client/http/HttpClientProvider.java
+++ b/core/src/main/java/io/undertow/client/http/HttpClientProvider.java
@@ -39,6 +39,8 @@ import org.xnio.ssl.XnioSsl;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.URI;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -50,6 +52,21 @@ import java.util.Set;
  */
 public class HttpClientProvider implements ClientProvider {
 
+    public static final String DISABLE_HTTPS_ENDPOINT_IDENTIFICATION_PROPERTY = "io.undertow.client.https.disableEndpointIdentification";
+    public static final boolean DISABLE_HTTPS_ENDPOINT_IDENTIFICATION;
+
+    static {
+        String disable = System.getSecurityManager() == null
+                ? System.getProperty(DISABLE_HTTPS_ENDPOINT_IDENTIFICATION_PROPERTY)
+                : AccessController.doPrivileged(new PrivilegedAction<String>() {
+                    @Override
+                    public String run() {
+                        return System.getProperty(DISABLE_HTTPS_ENDPOINT_IDENTIFICATION_PROPERTY);
+                    }
+                });
+        DISABLE_HTTPS_ENDPOINT_IDENTIFICATION = disable != null && (disable.isEmpty() || Boolean.parseBoolean(disable));
+    }
+
     @Override
     public Set<String> handlesSchemes() {
         return new HashSet<>(Arrays.asList(new String[]{"http", "https"}));
@@ -72,7 +89,11 @@ public class HttpClientProvider implements ClientProvider {
                 listener.failed(UndertowMessages.MESSAGES.sslWasNull());
                 return;
             }
-            OptionMap tlsOptions = OptionMap.builder().addAll(options).set(Options.SSL_STARTTLS, true).getMap();
+            OptionMap tlsOptions = OptionMap.builder()
+                    .set(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, DISABLE_HTTPS_ENDPOINT_IDENTIFICATION? "" : "HTTPS")
+                    .addAll(options)
+                    .set(Options.SSL_STARTTLS, true)
+                    .getMap();
             if (bindAddress == null) {
                 ssl.openSslConnection(worker, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, tlsOptions, uri), tlsOptions).addNotifier(createNotifier(listener), null);
             } else {
@@ -94,7 +115,11 @@ public class HttpClientProvider implements ClientProvider {
                 listener.failed(UndertowMessages.MESSAGES.sslWasNull());
                 return;
             }
-            OptionMap tlsOptions = OptionMap.builder().addAll(options).set(Options.SSL_STARTTLS, true).getMap();
+            OptionMap tlsOptions = OptionMap.builder()
+                    .set(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, DISABLE_HTTPS_ENDPOINT_IDENTIFICATION? "" : "HTTPS")
+                    .addAll(options)
+                    .set(Options.SSL_STARTTLS, true)
+                    .getMap();
             if (bindAddress == null) {
                 ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, tlsOptions, uri), tlsOptions).addNotifier(createNotifier(listener), null);
             } else {
diff --git a/core/src/main/java/io/undertow/client/http2/Http2ClientProvider.java b/core/src/main/java/io/undertow/client/http2/Http2ClientProvider.java
index dd9dac4..11dc7eb 100644
--- a/core/src/main/java/io/undertow/client/http2/Http2ClientProvider.java
+++ b/core/src/main/java/io/undertow/client/http2/Http2ClientProvider.java
@@ -26,6 +26,7 @@ import io.undertow.client.ClientCallback;
 import io.undertow.client.ClientConnection;
 import io.undertow.client.ClientProvider;
 import io.undertow.client.ClientStatistics;
+import io.undertow.client.http.HttpClientProvider;
 import io.undertow.conduits.ByteActivityCallback;
 import io.undertow.conduits.BytesReceivedStreamSourceConduit;
 import io.undertow.conduits.BytesSentStreamSinkConduit;
@@ -78,7 +79,7 @@ public class Http2ClientProvider implements ClientProvider {
 
     @Override
     public Set<String> handlesSchemes() {
-        return new HashSet<>(Arrays.asList(new String[]{"h2"}));
+        return new HashSet<>(Arrays.asList(new String[]{HTTP2}));
     }
 
     @Override
@@ -87,7 +88,11 @@ public class Http2ClientProvider implements ClientProvider {
             listener.failed(UndertowMessages.MESSAGES.sslWasNull());
             return;
         }
-        OptionMap tlsOptions = OptionMap.builder().addAll(options).set(Options.SSL_STARTTLS, true).getMap();
+        OptionMap tlsOptions = OptionMap.builder()
+                .set(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, HttpClientProvider.DISABLE_HTTPS_ENDPOINT_IDENTIFICATION? "" : "HTTPS")
+                .addAll(options)
+                .set(Options.SSL_STARTTLS, true)
+                .getMap();
         if(bindAddress == null) {
             ssl.openSslConnection(worker, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, tlsOptions), tlsOptions).addNotifier(createNotifier(listener), null);
         } else {
@@ -102,11 +107,15 @@ public class Http2ClientProvider implements ClientProvider {
             listener.failed(UndertowMessages.MESSAGES.sslWasNull());
             return;
         }
+        OptionMap tlsOptions = OptionMap.builder()
+                .set(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, HttpClientProvider.DISABLE_HTTPS_ENDPOINT_IDENTIFICATION? "" : "HTTPS")
+                .addAll(options)
+                .set(Options.SSL_STARTTLS, true)
+                .getMap();
         if(bindAddress == null) {
-            OptionMap tlsOptions = OptionMap.builder().addAll(options).set(Options.SSL_STARTTLS, true).getMap();
-            ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, tlsOptions), options).addNotifier(createNotifier(listener), null);
+            ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, tlsOptions), tlsOptions).addNotifier(createNotifier(listener), null);
         } else {
-            ssl.openSslConnection(ioThread, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null);
+            ssl.openSslConnection(ioThread, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, tlsOptions), tlsOptions).addNotifier(createNotifier(listener), null);
         }
 
     }
diff --git a/core/src/main/java/io/undertow/io/UndertowOutputStream.java b/core/src/main/java/io/undertow/io/UndertowOutputStream.java
index 46c4950..22b7b49 100644
--- a/core/src/main/java/io/undertow/io/UndertowOutputStream.java
+++ b/core/src/main/java/io/undertow/io/UndertowOutputStream.java
@@ -272,11 +272,18 @@ public class UndertowOutputStream extends OutputStream implements BufferWritable
      * {@inheritDoc}
      */
     public void flush() throws IOException {
-        if (anyAreSet(state, FLAG_CLOSED)) {
-            throw UndertowMessages.MESSAGES.streamIsClosed();
-        }
+        boolean closed = anyAreSet(state, FLAG_CLOSED);
         if (buffer != null && buffer.position() != 0) {
+            if (closed) {
+                throw UndertowMessages.MESSAGES.streamIsClosed();
+            }
             writeBufferBlocking(false);
+        } else if (closed) {
+            // No-op if flush is called with no buffered data on a closed channel.
+            // This stream closes itself when a Content-Length is provided, once sufficient
+            // bytes have been written -- a flush following the last write is entirely
+            // reasonable.
+            return;
         }
         if (channel == null) {
             channel = exchange.getResponseChannel();
diff --git a/core/src/main/java/io/undertow/protocols/ssl/SslConduit.java b/core/src/main/java/io/undertow/protocols/ssl/SslConduit.java
index 92a6b1f..91b6c89 100644
--- a/core/src/main/java/io/undertow/protocols/ssl/SslConduit.java
+++ b/core/src/main/java/io/undertow/protocols/ssl/SslConduit.java
@@ -999,7 +999,8 @@ public class SslConduit implements StreamSourceConduit, StreamSinkConduit {
 
     private SSLEngineResult wrapAndFlip(ByteBuffer[] userBuffers, int off, int len) throws IOException {
         SSLEngineResult result = null;
-        while (result == null || (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP && result.getStatus() != SSLEngineResult.Status.BUFFER_OVERFLOW)) {
+        while (result == null || (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP
+                && result.getStatus() != SSLEngineResult.Status.BUFFER_OVERFLOW && !engine.isInboundDone())) {
             if (userBuffers == null) {
                 result = engine.wrap(EMPTY_BUFFER, wrappedData.getBuffer());
             } else {
@@ -1275,14 +1276,16 @@ public class SslConduit implements StreamSourceConduit, StreamSinkConduit {
             }
             if(anyAreSet(state, FLAG_READS_RESUMED) && (unwrappedData != null || anyAreSet(state, FLAG_DATA_TO_UNWRAP))) {
                 if(anyAreSet(state, FLAG_READ_CLOSED)) {
-                    if(unwrappedData != null) {
-                        unwrappedData.close();
-                    }
-                    if(dataToUnwrap != null) {
-                        dataToUnwrap.close();
+                    synchronized (SslConduit.this) {
+                        if (unwrappedData != null) {
+                            unwrappedData.close();
+                        }
+                        if (dataToUnwrap != null) {
+                            dataToUnwrap.close();
+                        }
+                        unwrappedData = null;
+                        dataToUnwrap = null;
                     }
-                    unwrappedData = null;
-                    dataToUnwrap = null;
                 } else {
                     //there is data in the buffers so we do a wakeup
                     //as we may not get an actual read notification
diff --git a/core/src/main/java/io/undertow/protocols/ssl/UndertowXnioSsl.java b/core/src/main/java/io/undertow/protocols/ssl/UndertowXnioSsl.java
index 1d39d4a..c036452 100644
--- a/core/src/main/java/io/undertow/protocols/ssl/UndertowXnioSsl.java
+++ b/core/src/main/java/io/undertow/protocols/ssl/UndertowXnioSsl.java
@@ -68,6 +68,7 @@ import org.xnio.ssl.SslConnection;
 import org.xnio.ssl.XnioSsl;
 
 import static org.xnio.IoUtils.safeClose;
+import static io.undertow.UndertowMessages.MESSAGES;
 
 /**
  * @author Stuart Douglas
@@ -322,7 +323,7 @@ public class UndertowXnioSsl extends XnioSsl {
             sslParameters.setUseCipherSuitesOrder(true);
             engine.setSSLParameters(sslParameters);
         }
-        final String endpointIdentificationAlgorithm = optionMap.get(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, null);
+        final String endpointIdentificationAlgorithm = optionMap.get(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM);
         if (endpointIdentificationAlgorithm != null) {
             SSLParameters sslParameters = engine.getSSLParameters();
             sslParameters.setEndpointIdentificationAlgorithm(endpointIdentificationAlgorithm);
@@ -454,7 +455,13 @@ public class UndertowXnioSsl extends XnioSsl {
             hostnameValue = destination.getHostString();
         }
         if (hostnameValue != null) {
-            params.setServerNames(Collections.singletonList(new SNIHostName(hostnameValue)));
+            SNIHostName sniHostName = null;
+            try {
+                sniHostName = new SNIHostName(hostnameValue);
+            } catch (IllegalArgumentException e) {
+                throw MESSAGES.invalidSniHostname(hostnameValue, e);
+            }
+            params.setServerNames(Collections.singletonList(sniHostName));
         }
     }
 
@@ -477,7 +484,7 @@ public class UndertowXnioSsl extends XnioSsl {
                 SSLEngine sslEngine = JsseSslUtils.createSSLEngine(sslContext, optionMap, destination);
                 SSLParameters params = sslEngine.getSSLParameters();
                 setSNIHostName(destination, optionMap, params);
-                final String endpointIdentificationAlgorithm = optionMap.get(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, null);
+                final String endpointIdentificationAlgorithm = optionMap.get(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM);
                 if (endpointIdentificationAlgorithm != null) {
                     params.setEndpointIdentificationAlgorithm(endpointIdentificationAlgorithm);
                 }
diff --git a/core/src/main/java/io/undertow/server/Connectors.java b/core/src/main/java/io/undertow/server/Connectors.java
index c12ca57..d3931a8 100644
--- a/core/src/main/java/io/undertow/server/Connectors.java
+++ b/core/src/main/java/io/undertow/server/Connectors.java
@@ -444,7 +444,8 @@ public class Connectors {
     @Deprecated
     public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer) {
         try {
-            setExchangeRequestPath(exchange, encodedPath, charset, decode, allowEncodedSlash, decodeBuffer, exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS));
+            final boolean slashDecodingFlag = URLUtils.getSlashDecodingFlag(allowEncodedSlash, exchange.getConnection().getUndertowOptions().get(UndertowOptions.DECODE_SLASH));
+            setExchangeRequestPath(exchange, encodedPath, charset, decode, slashDecodingFlag, decodeBuffer, exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS));
         } catch (ParameterLimitException e) {
             throw new RuntimeException(e);
         }
@@ -461,10 +462,11 @@ public class Connectors {
      */
     public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, StringBuilder decodeBuffer) throws ParameterLimitException {
         final OptionMap options = exchange.getConnection().getUndertowOptions();
+        boolean slashDecodingFlag = URLUtils.getSlashDecodingFlag(options);
         setExchangeRequestPath(exchange, encodedPath,
                 options.get(UndertowOptions.URL_CHARSET, StandardCharsets.UTF_8.name()),
                 options.get(UndertowOptions.DECODE_URL, true),
-                options.get(UndertowOptions.ALLOW_ENCODED_SLASH, false),
+                slashDecodingFlag,
                 decodeBuffer,
                 options.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS));
     }
@@ -476,7 +478,7 @@ public class Connectors {
      * @param encodedPath The encoded path
      * @param charset     The charset
      */
-    public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer, int maxParameters) throws ParameterLimitException {
+    public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean decodeSlashFlag, StringBuilder decodeBuffer, int maxParameters) throws ParameterLimitException {
         boolean requiresDecode = false;
         final StringBuilder pathBuilder = new StringBuilder();
         int currentPathPartIndex = 0;
@@ -486,7 +488,7 @@ public class Connectors {
                 String part;
                 String encodedPart = encodedPath.substring(currentPathPartIndex, i);
                 if (requiresDecode) {
-                    part = URLUtils.decode(encodedPart, charset, allowEncodedSlash,false, decodeBuffer);
+                    part = URLUtils.decode(encodedPart, charset, decodeSlashFlag,false, decodeBuffer);
                 } else {
                     part = encodedPart;
                 }
@@ -503,7 +505,7 @@ public class Connectors {
                 String part;
                 String encodedPart = encodedPath.substring(currentPathPartIndex, i);
                 if (requiresDecode) {
-                    part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer);
+                    part = URLUtils.decode(encodedPart, charset, decodeSlashFlag, false, decodeBuffer);
                 } else {
                     part = encodedPart;
                 }
@@ -519,7 +521,7 @@ public class Connectors {
         String part;
         String encodedPart = encodedPath.substring(currentPathPartIndex);
         if (requiresDecode) {
-            part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer);
+            part = URLUtils.decode(encodedPart, charset, decodeSlashFlag, false, decodeBuffer);
         } else {
             part = encodedPart;
         }
diff --git a/core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java b/core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java
index 2dd2bfc..e33dbf6 100644
--- a/core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java
+++ b/core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java
@@ -76,10 +76,10 @@ public class URLDecodingHandler implements HttpHandler {
     }
 
     private static void decodePath(HttpServerExchange exchange, String charset, StringBuilder sb) {
-        final boolean decodeSlash = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_ENCODED_SLASH, false);
-        exchange.setRequestPath(URLUtils.decode(exchange.getRequestPath(), charset, decodeSlash, false, sb));
-        exchange.setRelativePath(URLUtils.decode(exchange.getRelativePath(), charset, decodeSlash, false, sb));
-        exchange.setResolvedPath(URLUtils.decode(exchange.getResolvedPath(), charset, decodeSlash, false, sb));
+        boolean slashDecodingFlag = URLUtils.getSlashDecodingFlag(exchange.getConnection().getUndertowOptions());
+        exchange.setRequestPath(URLUtils.decode(exchange.getRequestPath(), charset, slashDecodingFlag, false, sb));
+        exchange.setRelativePath(URLUtils.decode(exchange.getRelativePath(), charset, slashDecodingFlag, false, sb));
+        exchange.setResolvedPath(URLUtils.decode(exchange.getResolvedPath(), charset, slashDecodingFlag, false, sb));
     }
 
     private static void decodeQueryString(HttpServerExchange exchange, String charset, StringBuilder sb) {
diff --git a/core/src/main/java/io/undertow/server/handlers/builder/PredicatedHandlersParser.java b/core/src/main/java/io/undertow/server/handlers/builder/PredicatedHandlersParser.java
index 1450cb3..8c0b917 100644
--- a/core/src/main/java/io/undertow/server/handlers/builder/PredicatedHandlersParser.java
+++ b/core/src/main/java/io/undertow/server/handlers/builder/PredicatedHandlersParser.java
@@ -629,9 +629,16 @@ public class PredicatedHandlersParser {
             char c = string.charAt(pos);
             if (currentStringDelim != 0) {
                 if (c == currentStringDelim) {
-                    if (current.length() > 0 && current.charAt(current.length() - 1) == '\\') {
-                        current.setLength(current.length() - 1);
-                        current.append(c);
+                    if (current.length() > 0) {
+                        if (current.charAt(current.length() - 1) != '\\') {
+                            ret.add(new Token(current.toString(), pos));
+                            current.setLength(0);
+                            currentStringDelim = 0;
+                        } else {
+                            // previous '\\' escaped delimiter, replace escape with current char
+                            current.deleteCharAt(current.length() - 1);
+                            current.append(c);
+                        }
                     } else {
                         ret.add(new Token(current.toString(), pos));
                         current.setLength(0);
diff --git a/core/src/main/java/io/undertow/server/protocol/ajp/AjpOpenListener.java b/core/src/main/java/io/undertow/server/protocol/ajp/AjpOpenListener.java
index 6ea9c66..7ea563e 100644
--- a/core/src/main/java/io/undertow/server/protocol/ajp/AjpOpenListener.java
+++ b/core/src/main/java/io/undertow/server/protocol/ajp/AjpOpenListener.java
@@ -32,6 +32,8 @@ import io.undertow.server.HttpHandler;
 import io.undertow.server.OpenListener;
 import io.undertow.server.ServerConnection;
 import io.undertow.server.XnioByteBufferPool;
+import io.undertow.util.URLUtils;
+
 import org.xnio.IoUtils;
 import org.xnio.OptionMap;
 import org.xnio.Options;
@@ -98,7 +100,7 @@ public class AjpOpenListener implements OpenListener {
         PooledByteBuffer buf = pool.allocate();
         this.bufferSize = buf.getBuffer().remaining();
         buf.close();
-        parser = new AjpRequestParser(undertowOptions.get(URL_CHARSET, StandardCharsets.UTF_8.name()), undertowOptions.get(DECODE_URL, true), undertowOptions.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS), undertowOptions.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS), undertowOptions.get(UndertowOptions.ALLOW_ENCODED_SLASH, false), undertowOptions.get(UndertowOptions.ALLOW_UNESCAPED_CHARACTERS_IN_URL, false), undertowOptions.get(UndertowOptions.AJP_ALLOWED_REQUEST_ATTRIBUTES_PATTERN, DEFAULT_AJP_ALLOWED_REQUEST_ATTRIBUTES_PATTERN));
+        parser = new AjpRequestParser(undertowOptions.get(URL_CHARSET, StandardCharsets.UTF_8.name()), undertowOptions.get(DECODE_URL, true), undertowOptions.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS), undertowOptions.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS), URLUtils.getSlashDecodingFlag(undertowOptions), undertowOptions.get(UndertowOptions.ALLOW_UNESCAPED_CHARACTERS_IN_URL, false), undertowOptions.get(UndertowOptions.AJP_ALLOWED_REQUEST_ATTRIBUTES_PATTERN, DEFAULT_AJP_ALLOWED_REQUEST_ATTRIBUTES_PATTERN));
         connectorStatistics = new ConnectorStatisticsImpl();
         statisticsEnabled = undertowOptions.get(UndertowOptions.ENABLE_CONNECTOR_STATISTICS, false);
     }
@@ -178,7 +180,7 @@ public class AjpOpenListener implements OpenListener {
         }
         this.undertowOptions = undertowOptions;
         statisticsEnabled = undertowOptions.get(UndertowOptions.ENABLE_CONNECTOR_STATISTICS, false);
-        parser = new AjpRequestParser(undertowOptions.get(URL_CHARSET, StandardCharsets.UTF_8.name()), undertowOptions.get(DECODE_URL, true), undertowOptions.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS), undertowOptions.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS), undertowOptions.get(UndertowOptions.ALLOW_ENCODED_SLASH, false), undertowOptions.get(UndertowOptions.ALLOW_UNESCAPED_CHARACTERS_IN_URL, false));
+        parser = new AjpRequestParser(undertowOptions.get(URL_CHARSET, StandardCharsets.UTF_8.name()), undertowOptions.get(DECODE_URL, true), undertowOptions.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS), undertowOptions.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS), URLUtils.getSlashDecodingFlag(undertowOptions), undertowOptions.get(UndertowOptions.ALLOW_UNESCAPED_CHARACTERS_IN_URL, false));
     }
 
     @Override
diff --git a/core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java b/core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java
index 11e988c..f6e0cc3 100644
--- a/core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java
+++ b/core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java
@@ -74,7 +74,7 @@ public class AjpRequestParser {
 
     private final String encoding;
     private final boolean doDecode;
-    private final boolean allowEncodedSlash;
+    private final boolean slashDecodingFlag;
     private final int maxParameters;
     private final int maxHeaders;
     private StringBuilder decodeBuffer;
@@ -184,16 +184,16 @@ public class AjpRequestParser {
         ATTR_SET = new HashSet<String>(Arrays.asList(ATTRIBUTES));
     }
 
-    public AjpRequestParser(String encoding, boolean doDecode, int maxParameters, int maxHeaders, boolean allowEncodedSlash, boolean allowUnescapedCharactersInUrl) {
-        this(encoding, doDecode, maxParameters, maxHeaders, allowEncodedSlash, allowUnescapedCharactersInUrl, null);
+    public AjpRequestParser(String encoding, boolean doDecode, int maxParameters, int maxHeaders, boolean slashDecodingFlag, boolean allowUnescapedCharactersInUrl) {
+        this(encoding, doDecode, maxParameters, maxHeaders, slashDecodingFlag, allowUnescapedCharactersInUrl, null);
     }
 
-    public AjpRequestParser(String encoding, boolean doDecode, int maxParameters, int maxHeaders, boolean allowEncodedSlash, boolean allowUnescapedCharactersInUrl, String allowedRequestAttributesPattern) {
+    public AjpRequestParser(String encoding, boolean doDecode, int maxParameters, int maxHeaders, boolean slashDecodingFlag, boolean allowUnescapedCharactersInUrl, String allowedRequestAttributesPattern) {
         this.encoding = encoding;
         this.doDecode = doDecode;
         this.maxParameters = maxParameters;
         this.maxHeaders = maxHeaders;
-        this.allowEncodedSlash = allowEncodedSlash;
+        this.slashDecodingFlag = slashDecodingFlag;
         this.allowUnescapedCharactersInUrl = allowUnescapedCharactersInUrl;
         if (allowedRequestAttributesPattern != null && !allowedRequestAttributesPattern.isEmpty()) {
             this.allowedRequestAttributesPattern = Pattern.compile(allowedRequestAttributesPattern);
@@ -512,7 +512,7 @@ public class AjpRequestParser {
                 if(decodeBuffer == null) {
                     decodeBuffer = new StringBuilder();
                 }
-                return URLUtils.decode(url, this.encoding, allowEncodedSlash, false, decodeBuffer);
+                return URLUtils.decode(url, this.encoding, slashDecodingFlag, false, decodeBuffer);
             } catch (Exception e) {
                 throw UndertowMessages.MESSAGES.failedToDecodeURL(url, encoding, e);
             }
diff --git a/core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java b/core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java
index 736a658..57113cd 100644
--- a/core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java
+++ b/core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java
@@ -162,7 +162,7 @@ public abstract class HttpRequestParser {
 
     private final int maxParameters;
     private final int maxHeaders;
-    private final boolean allowEncodedSlash;
+    private final boolean slashDecodingFlag;
     private final boolean decode;
     private final String charset;
     private final int maxCachedHeaderSize;
@@ -208,7 +208,7 @@ public abstract class HttpRequestParser {
     public HttpRequestParser(OptionMap options) {
         maxParameters = options.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS);
         maxHeaders = options.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS);
-        allowEncodedSlash = options.get(UndertowOptions.ALLOW_ENCODED_SLASH, false);
+        slashDecodingFlag = URLUtils.getSlashDecodingFlag(options);
         decode = options.get(UndertowOptions.DECODE_URL, true);
         charset = options.get(UndertowOptions.URL_CHARSET, StandardCharsets.UTF_8.name());
         maxCachedHeaderSize = options.get(UndertowOptions.MAX_CACHED_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_CACHED_HEADER_SIZE);
@@ -408,9 +408,11 @@ public abstract class HttpRequestParser {
             } else if (next == ';') {
                 state.parseState = parseState;
                 state.urlDecodeRequired = urlDecodeRequired;
-                state.pos = canonicalPathStart;
                 // store at canonical path the partial path parsed up until here
                 state.canonicalPath.append(stringBuilder.substring(canonicalPathStart));
+                state.stringBuilder.append(";");
+                // set position to end of path (possibly start of parameter name)
+                state.pos = state.stringBuilder.length();
                 // handle the path parameters
                 handlePathParameters(buffer, state, exchange);
                 // if state is PATH, it means that handlePathParameters found a / after parsing path parameters
@@ -456,7 +458,7 @@ public abstract class HttpRequestParser {
             exchange.setRelativePath("/");
             exchange.setRequestURI(path, true);
         } else if (parseState < HOST_DONE && state.canonicalPath.length() == 0) {
-            String decodedPath = decode(path, urlDecodeRequired, state, allowEncodedSlash, false);
+            String decodedPath = decode(path, urlDecodeRequired, state, slashDecodingFlag, false);
             exchange.setRequestPath(decodedPath);
             exchange.setRelativePath(decodedPath);
             exchange.setRequestURI(path, false);
@@ -479,7 +481,7 @@ public abstract class HttpRequestParser {
 
     private void handleFullUrl(ParseState state, HttpServerExchange exchange, int canonicalPathStart, boolean urlDecodeRequired, String path, int parseState) {
         state.canonicalPath.append(path.substring(canonicalPathStart));
-        String thePath = decode(state.canonicalPath.toString(), urlDecodeRequired, state, allowEncodedSlash, false);
+        String thePath = decode(state.canonicalPath.toString(), urlDecodeRequired, state, slashDecodingFlag, false);
         exchange.setRequestPath(thePath);
         exchange.setRelativePath(thePath);
         exchange.setRequestURI(path, parseState == HOST_DONE);
@@ -569,9 +571,9 @@ public abstract class HttpRequestParser {
         state.mapCount = mapCount;
     }
 
-    private String decode(final String value, boolean urlDecodeRequired, ParseState state, final boolean allowEncodedSlash, final boolean formEncoded) {
+    private String decode(final String value, boolean urlDecodeRequired, ParseState state, final boolean slashDecodingFlag, final boolean formEncoded) {
         if (urlDecodeRequired) {
-            return URLUtils.decode(value, charset, allowEncodedSlash, formEncoded, state.decodeBuffer);
+            return URLUtils.decode(value, charset, slashDecodingFlag, formEncoded, state.decodeBuffer);
         } else {
             return value;
         }
@@ -586,8 +588,7 @@ public abstract class HttpRequestParser {
         boolean urlDecodeRequired = state.urlDecodeRequired;
         String param = state.nextQueryParam;
         final StringBuilder stringBuilder = state.stringBuilder;
-        stringBuilder.append(";");
-        int pos = stringBuilder.length();
+        int pos = state.pos;
 
         //so this is a bit funky, because it not only deals with parsing, but
         //also deals with URL decoding the query parameters as well, while also
diff --git a/core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java b/core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java
index ef64153..257d572 100644
--- a/core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java
+++ b/core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java
@@ -52,6 +52,8 @@ import io.undertow.util.Methods;
 import io.undertow.util.ParameterLimitException;
 import io.undertow.util.Protocols;
 import io.undertow.util.StatusCodes;
+import io.undertow.util.URLUtils;
+
 import org.xnio.ChannelListener;
 import org.xnio.IoUtils;
 import org.xnio.OptionMap;
@@ -78,7 +80,7 @@ public class Http2ReceiveListener implements ChannelListener<Http2Channel> {
     private final String encoding;
     private final boolean decode;
     private final StringBuilder decodeBuffer = new StringBuilder();
-    private final boolean allowEncodingSlash;
+    private final boolean slashDecodingFlag;
     private final int bufferSize;
     private final int maxParameters;
     private final boolean recordRequestStartTime;
@@ -92,7 +94,7 @@ public class Http2ReceiveListener implements ChannelListener<Http2Channel> {
         this.bufferSize = bufferSize;
         this.connectorStatistics = connectorStatistics;
         this.maxEntitySize = undertowOptions.get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE);
-        this.allowEncodingSlash = undertowOptions.get(UndertowOptions.ALLOW_ENCODED_SLASH, false);
+        this.slashDecodingFlag = URLUtils.getSlashDecodingFlag(undertowOptions);
         this.decode = undertowOptions.get(UndertowOptions.DECODE_URL, true);
         this.maxParameters = undertowOptions.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS);
         this.recordRequestStartTime = undertowOptions.get(UndertowOptions.RECORD_REQUEST_START_TIME, false);
@@ -190,7 +192,7 @@ public class Http2ReceiveListener implements ChannelListener<Http2Channel> {
         }
 
         try {
-            Connectors.setExchangeRequestPath(exchange, path, encoding, decode, allowEncodingSlash, decodeBuffer, maxParameters);
+            Connectors.setExchangeRequestPath(exchange, path, encoding, decode, slashDecodingFlag, decodeBuffer, maxParameters);
         } catch (ParameterLimitException e) {
             //this can happen if max parameters is exceeded
             UndertowLogger.REQUEST_IO_LOGGER.debug("Failed to set request path", e);
@@ -241,7 +243,7 @@ public class Http2ReceiveListener implements ChannelListener<Http2Channel> {
         Connectors.terminateRequest(exchange);
         String uri = exchange.getQueryString().isEmpty() ? initial.getRequestURI() : initial.getRequestURI() + '?' + exchange.getQueryString();
         try {
-            Connectors.setExchangeRequestPath(exchange, uri, encoding, decode, allowEncodingSlash, decodeBuffer, maxParameters);
+            Connectors.setExchangeRequestPath(exchange, uri, encoding, decode, slashDecodingFlag, decodeBuffer, maxParameters);
         } catch (ParameterLimitException e) {
             exchange.setStatusCode(StatusCodes.BAD_REQUEST);
             exchange.endExchange();
diff --git a/core/src/main/java/io/undertow/server/protocol/http2/Http2ServerConnection.java b/core/src/main/java/io/undertow/server/protocol/http2/Http2ServerConnection.java
index 5714c7f..56e1a52 100644
--- a/core/src/main/java/io/undertow/server/protocol/http2/Http2ServerConnection.java
+++ b/core/src/main/java/io/undertow/server/protocol/http2/Http2ServerConnection.java
@@ -72,6 +72,7 @@ import io.undertow.util.AttachmentList;
 import io.undertow.util.HeaderMap;
 import io.undertow.util.HttpString;
 import io.undertow.util.StatusCodes;
+import io.undertow.util.URLUtils;
 
 import static io.undertow.protocols.http2.Http2Channel.AUTHORITY;
 import static io.undertow.protocols.http2.Http2Channel.METHOD;
@@ -432,7 +433,7 @@ public class Http2ServerConnection extends ServerConnection {
             exchange.setProtocol(Protocols.HTTP_1_1);
             exchange.setRequestScheme(this.exchange.getRequestScheme());
             try {
-                Connectors.setExchangeRequestPath(exchange, path, getUndertowOptions().get(UndertowOptions.URL_CHARSET, StandardCharsets.UTF_8.name()), getUndertowOptions().get(UndertowOptions.DECODE_URL, true), getUndertowOptions().get(UndertowOptions.ALLOW_ENCODED_SLASH, false), new StringBuilder(), getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_HEADERS));
+                Connectors.setExchangeRequestPath(exchange, path, getUndertowOptions().get(UndertowOptions.URL_CHARSET, StandardCharsets.UTF_8.name()), getUndertowOptions().get(UndertowOptions.DECODE_URL, true), URLUtils.getSlashDecodingFlag(getUndertowOptions()), new StringBuilder(), getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_HEADERS));
             } catch (ParameterLimitException e) {
                 UndertowLogger.REQUEST_IO_LOGGER.debug("Too many parameters in HTTP/2 request", e);
                 exchange.setStatusCode(StatusCodes.BAD_REQUEST);
diff --git a/core/src/main/java/io/undertow/util/URLUtils.java b/core/src/main/java/io/undertow/util/URLUtils.java
index 50566dc..18d49b3 100644
--- a/core/src/main/java/io/undertow/util/URLUtils.java
+++ b/core/src/main/java/io/undertow/util/URLUtils.java
@@ -21,7 +21,10 @@ package io.undertow.util;
 import java.io.UnsupportedEncodingException;
 import java.util.regex.Pattern;
 
+import org.xnio.OptionMap;
+
 import io.undertow.UndertowMessages;
+import io.undertow.UndertowOptions;
 import io.undertow.server.HttpServerExchange;
 
 /**
@@ -349,4 +352,20 @@ public class URLUtils {
         }
         return false;
     }
+
+    public static boolean getSlashDecodingFlag(final OptionMap options) {
+        final boolean allowEncodedSlash = options.get(UndertowOptions.ALLOW_ENCODED_SLASH, false);
+        final Boolean decodeSlash = options.get(UndertowOptions.DECODE_SLASH);
+        return getSlashDecodingFlag(allowEncodedSlash, decodeSlash);
+    }
+
+    public static boolean getSlashDecodingFlag(final boolean allowEncodedSlash, final Boolean decodeSlash) {
+        final boolean slashDecodingFlag;
+        if (decodeSlash != null) {
+            slashDecodingFlag = decodeSlash;
+        } else {
+            slashDecodingFlag = allowEncodedSlash;
+        }
+        return slashDecodingFlag;
+    }
 }
diff --git a/core/src/test/java/io/undertow/client/http/Http2ClientTestCase.java b/core/src/test/java/io/undertow/client/http/Http2ClientTestCase.java
index 2f740b3..e26da3b 100644
--- a/core/src/test/java/io/undertow/client/http/Http2ClientTestCase.java
+++ b/core/src/test/java/io/undertow/client/http/Http2ClientTestCase.java
@@ -16,8 +16,11 @@
 package io.undertow.client.http;
 
 import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.channels.ClosedChannelException;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
@@ -80,6 +83,8 @@ public class Http2ClientTestCase {
 
     private static final AttachmentKey<String> RESPONSE_BODY = AttachmentKey.create(String.class);
 
+    private IOException exception;
+
     static {
         final OptionMap.Builder builder = OptionMap.builder()
                 .set(Options.WORKER_IO_THREADS, 8)
@@ -195,6 +200,32 @@ public class Http2ClientTestCase {
         }
     }
 
+    @Test
+    public void testH2ServerIdentity() throws Exception {
+        final UndertowClient client = createClient();
+        exception = null;
+
+        final List<ClientResponse> responses = new CopyOnWriteArrayList<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        InetAddress address = InetAddress.getByName(ADDRESS.getHost());
+        String hostname = address instanceof Inet6Address? "[" + address.getHostAddress() + "]" : address.getHostAddress();
+        URI uri = new URI("h2", ADDRESS.getUserInfo(), hostname, ADDRESS.getPort(), ADDRESS.getPath(), ADDRESS.getQuery(), ADDRESS.getFragment());
+        final ClientConnection connection = client.connect(uri, worker, new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()), DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get();
+        try {
+            connection.getIoThread().execute(() -> {
+                final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath(MESSAGE);
+                request.getRequestHeaders().put(Headers.HOST, DefaultServer.getHostAddress());
+                connection.sendRequest(request, createClientCallback(responses, latch));
+            });
+
+            latch.await(10, TimeUnit.SECONDS);
+
+            Assert.assertEquals(0, responses.size());
+            Assert.assertTrue(exception instanceof ClosedChannelException);
+        } finally {
+            IoUtils.safeClose(connection);
+        }
+    }
 
     @Test
     public void testHeadRequest() throws Exception {
@@ -317,7 +348,7 @@ public class Http2ClientTestCase {
                             @Override
                             protected void error(IOException e) {
                                 e.printStackTrace();
-
+                                exception = e;
                                 latch.countDown();
                             }
                         }.setup(result.getResponseChannel());
@@ -326,7 +357,7 @@ public class Http2ClientTestCase {
                     @Override
                     public void failed(IOException e) {
                         e.printStackTrace();
-
+                        exception = e;
                         latch.countDown();
                     }
                 });
@@ -338,6 +369,7 @@ public class Http2ClientTestCase {
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
+                    exception = e;
                     latch.countDown();
                 }
             }
@@ -345,6 +377,7 @@ public class Http2ClientTestCase {
             @Override
             public void failed(IOException e) {
                 e.printStackTrace();
+                exception = e;
                 latch.countDown();
             }
         };
diff --git a/core/src/test/java/io/undertow/client/http/HttpClientSNITestCase.java b/core/src/test/java/io/undertow/client/http/HttpClientSNITestCase.java
index ab13afd..41aec2e 100644
--- a/core/src/test/java/io/undertow/client/http/HttpClientSNITestCase.java
+++ b/core/src/test/java/io/undertow/client/http/HttpClientSNITestCase.java
@@ -151,7 +151,7 @@ public class HttpClientSNITestCase {
 
         // connect using the hostname, SNI expected
         final ClientConnection connection = client.connect(new URI("https://" + address.getHostName() + ":" + ADDRESS.getPort()), worker,
-                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
+                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.createClientSslContext()),
                 DefaultServer.getBufferPool(), OptionMap.EMPTY).get();
         try {
             connection.getIoThread().execute(() -> {
@@ -182,8 +182,8 @@ public class HttpClientSNITestCase {
 
         // connect using the IP, no SNI expected
         final ClientConnection connection = client.connect(new URI("https://" + hostname + ":" + ADDRESS.getPort()), worker,
-                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
-                DefaultServer.getBufferPool(), OptionMap.EMPTY).get();
+                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.createClientSslContext()),
+                DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, "")).get();
         try {
             connection.getIoThread().execute(() -> {
                 final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath(SNI);
@@ -213,7 +213,7 @@ public class HttpClientSNITestCase {
 
         // connect using IP but adding hostname via option, SNI expected to the forced one
         final ClientConnection connection = client.connect(new URI("https://" + hostname + ":" + ADDRESS.getPort()), worker,
-                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
+                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.createClientSslContext()),
                 DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.SSL_SNI_HOSTNAME, address.getHostName())).get();
         try {
             connection.getIoThread().execute(() -> {
@@ -243,8 +243,8 @@ public class HttpClientSNITestCase {
 
         // connect using hostname but add option to another hostname, SNI expected to the forced one
         final ClientConnection connection = client.connect(new URI("https://" + address.getHostName() + ":" + ADDRESS.getPort()), worker,
-                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
-                DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.SSL_SNI_HOSTNAME, "server")).get();
+                new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.createClientSslContext()),
+                DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.SSL_SNI_HOSTNAME, "server", UndertowOptions.ENDPOINT_IDENTIFICATION_ALGORITHM, "")).get();
         try {
             connection.getIoThread().execute(() -> {
                 final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath(SNI);
diff --git a/core/src/test/java/io/undertow/client/http/HttpClientTestCase.java b/core/src/test/java/io/undertow/client/http/HttpClientTestCase.java
index 05ab3bb..65897a5 100644
--- a/core/src/test/java/io/undertow/client/http/HttpClientTestCase.java
+++ b/core/src/test/java/io/undertow/client/http/HttpClientTestCase.java
@@ -18,17 +18,6 @@
 
 package io.undertow.client.http;
 
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import javax.net.ssl.SSLContext;
-
 import io.undertow.client.ClientCallback;
 import io.undertow.client.ClientConnection;
 import io.undertow.client.ClientExchange;
@@ -64,6 +53,18 @@ import org.xnio.channels.ReadTimeoutException;
 import org.xnio.channels.StreamSinkChannel;
 import org.xnio.ssl.XnioSsl;
 
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 import static io.undertow.testutils.StopServerWithExternalWorkerUtils.stopWorker;
 
 /**
@@ -330,6 +331,42 @@ public class HttpClientTestCase {
         }
     }
 
+    @Test
+    public void testSslServerIdentity() throws Exception {
+        final UndertowClient client = createClient();
+        exception = null;
+
+        final List<ClientResponse> responses = new CopyOnWriteArrayList<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+        DefaultServer.startSSLServer();
+        SSLContext context = DefaultServer.getClientSSLContext();
+        XnioSsl ssl = new UndertowXnioSsl(DefaultServer.getWorker().getXnio(), OptionMap.EMPTY, DefaultServer.SSL_BUFFER_POOL, context);
+
+        // change the URI to use the IP instead the "localhost" name set in the certificate
+        URI uri = new URI(DefaultServer.getDefaultServerSSLAddress());
+        InetAddress address = InetAddress.getByName(uri.getHost());
+        String hostname = address instanceof Inet6Address? "[" + address.getHostAddress() + "]" : address.getHostAddress();
+        uri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
+
+        // this should fail as IP alternative name is not set in the certificate
+        final ClientConnection connection = client.connect(uri, worker, ssl, DefaultServer.getBufferPool(), OptionMap.EMPTY).get();
+        try {
+            connection.getIoThread().execute(() -> {
+                final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath(MESSAGE);
+                request.getRequestHeaders().put(Headers.HOST, DefaultServer.getHostAddress());
+                connection.sendRequest(request, createClientCallback(responses, latch));
+            });
+
+            latch.await(10, TimeUnit.SECONDS);
+
+            Assert.assertEquals(0, responses.size());
+            // see UNDERTOW-2249: assert exception instanceof ClosedChannelException
+            Assert.assertNotNull(exception);
+        } finally {
+            connection.getIoThread().execute(() -> IoUtils.safeClose(connection));
+            DefaultServer.stopSSLServer();
+        }
+    }
 
     @Test
     public void testConnectionClose() throws Exception {
diff --git a/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java b/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java
index 8e3fab2..7ded4cd 100644
--- a/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java
+++ b/core/src/test/java/io/undertow/server/handlers/PredicatedHandlersTestCase.java
@@ -19,6 +19,7 @@
 package io.undertow.server.handlers;
 
 import io.undertow.Handlers;
+import io.undertow.server.HandlerWrapper;
 import io.undertow.server.HttpHandler;
 import io.undertow.server.HttpServerExchange;
 import io.undertow.server.handlers.builder.PredicatedHandlersParser;
@@ -145,4 +146,17 @@ public class PredicatedHandlersTestCase {
             client.getConnectionManager().shutdown();
         }
     }
+
+    @Test
+    public void testSetHeader() throws Exception {
+        HandlerWrapper predicate = PredicatedHandlersParser.parseHandler("set(attribute='%{o,test-header}', value='foo\\'bar')", PredicatedHandlersTestCase.class.getClassLoader());
+        HttpServerExchange e = new HttpServerExchange(null);
+        SetAttributeHandler handler = (SetAttributeHandler) predicate.wrap(new HttpHandler() {
+            @Override
+            public void handleRequest(HttpServerExchange exchange) throws Exception {
+            }
+        });
+        handler.handleRequest(e);
+        Assert.assertEquals("foo'bar",e.getResponseHeaders().get("test-header").getFirst());
+    }
 }
diff --git a/core/src/test/java/io/undertow/server/handlers/blocking/BlockingServerStreamClosureTestCase.java b/core/src/test/java/io/undertow/server/handlers/blocking/BlockingServerStreamClosureTestCase.java
new file mode 100644
index 0000000..edc23ef
--- /dev/null
+++ b/core/src/test/java/io/undertow/server/handlers/blocking/BlockingServerStreamClosureTestCase.java
@@ -0,0 +1,119 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2022 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package io.undertow.server.handlers.blocking;
+
+import io.undertow.server.handlers.BlockingHandler;
+import io.undertow.testutils.DefaultServer;
+import io.undertow.testutils.HttpClientUtils;
+import io.undertow.testutils.TestHttpClient;
+import io.undertow.util.Headers;
+import io.undertow.util.StatusCodes;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author Carter Kozak
+ */
+@RunWith(DefaultServer.class)
+public class BlockingServerStreamClosureTestCase {
+
+    @Test
+    public void testFlushAfterContentLengthReached() throws IOException {
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        DefaultServer.setRootHandler(new BlockingHandler(exchange -> {
+            exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "1");
+            try {
+                OutputStream out = exchange.getOutputStream();
+                out.write(65);
+                out.flush();
+                out.close();
+            } catch (Throwable t) {
+                thrown.set(t);
+                throw t;
+            }
+        }));
+        makeSuccessfulRequest("A");
+        Throwable maybeFailure = thrown.get();
+        if (maybeFailure != null) {
+            throw new AssertionError("Unexpected failure", maybeFailure);
+        }
+    }
+
+    @Test
+    public void testEmptyWriteAfterContentLength() throws IOException {
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        DefaultServer.setRootHandler(new BlockingHandler(exchange -> {
+            exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "1");
+            try {
+                OutputStream out = exchange.getOutputStream();
+                out.write(65);
+                out.write(new byte[10], 2, 0);
+                out.close();
+            } catch (Throwable t) {
+                thrown.set(t);
+                throw t;
+            }
+        }));
+        makeSuccessfulRequest("A");
+        Throwable maybeFailure = thrown.get();
+        if (maybeFailure != null) {
+            throw new AssertionError("Unexpected failure", maybeFailure);
+        }
+    }
+
+    @Test
+    public void testFlushAfterClose() throws IOException {
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        DefaultServer.setRootHandler(new BlockingHandler(exchange -> {
+            try {
+                OutputStream out = exchange.getOutputStream();
+                out.write(65);
+                out.close();
+                out.flush();
+            } catch (Throwable t) {
+                thrown.set(t);
+                throw t;
+            }
+        }));
+        makeSuccessfulRequest("A");
+        Throwable maybeFailure = thrown.get();
+        if (maybeFailure != null) {
+            throw new AssertionError("Unexpected failure", maybeFailure);
+        }
+    }
+
+    private static void makeSuccessfulRequest(String expectedContent) throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        try {
+            HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL());
+            HttpResponse result = client.execute(get);
+            Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
+            Assert.assertEquals(expectedContent, HttpClientUtils.readResponse(result));
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+}
diff --git a/core/src/test/java/io/undertow/server/protocol/http/ParserResumeTestCase.java b/core/src/test/java/io/undertow/server/protocol/http/ParserResumeTestCase.java
index c53ae02..0274fa2 100644
--- a/core/src/test/java/io/undertow/server/protocol/http/ParserResumeTestCase.java
+++ b/core/src/test/java/io/undertow/server/protocol/http/ParserResumeTestCase.java
@@ -44,6 +44,7 @@ public class ParserResumeTestCase {
     public static final HttpRequestParser PARSER = HttpRequestParser.instance(OptionMap.create(UndertowOptions.ALLOW_ENCODED_SLASH, true));
 
     final ParseState context = new ParseState(10);
+
     @Test
     public void testMethodSplit() {
         byte[] in = DATA.getBytes();
@@ -56,6 +57,31 @@ public class ParserResumeTestCase {
         }
     }
 
+    @Test
+    public void testMatrixParamSplit() throws BadRequestException {
+        String data = "GET http://host/path;hoge=fuga;foo=bar HTTP/1.1\n\n";
+        byte[] in = data.getBytes();
+
+        context.reset();
+        HttpServerExchange exchange = new HttpServerExchange(null);
+        ByteBuffer buffer = ByteBuffer.wrap(in);
+        buffer.limit(data.indexOf("fuga") + 2); // resume in the middle of a parameter value
+        PARSER.handle(buffer, context, exchange);
+
+        buffer.limit(data.indexOf("bar") + 2); // resume in the middle of a parameter value
+        PARSER.handle(buffer, context, exchange);
+
+        buffer.limit(buffer.capacity());
+        PARSER.handle(buffer, context, exchange);
+
+        Assert.assertEquals("/path", exchange.getRequestPath());
+        Assert.assertEquals(2, exchange.getPathParameters().size());
+        Assert.assertTrue(exchange.getPathParameters().containsKey("hoge"));
+        Assert.assertTrue(exchange.getPathParameters().containsKey("foo"));
+        Assert.assertEquals("fuga", exchange.getPathParameters().get("hoge").getFirst());
+        Assert.assertEquals("bar", exchange.getPathParameters().get("foo").getFirst());
+    }
+
     @Test
     public void testOneCharacterAtATime() throws BadRequestException {
         context.reset();
@@ -74,6 +100,17 @@ public class ParserResumeTestCase {
         runAssertions(result);
     }
 
+    private HttpServerExchange resume(final int split, byte[] in) throws BadRequestException {
+        context.reset();
+        HttpServerExchange result = new HttpServerExchange(null);
+        ByteBuffer buffer = ByteBuffer.wrap(in);
+        buffer.limit(split);
+        PARSER.handle(buffer, context, result);
+        buffer.limit(buffer.capacity());
+        PARSER.handle(buffer, context, result);
+        return result;
+    }
+
     private void testResume(final int split, byte[] in) throws BadRequestException {
         context.reset();
         HttpServerExchange result = new HttpServerExchange(null);
diff --git a/core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java b/core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java
index 2971605..bca70bd 100644
--- a/core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java
+++ b/core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java
@@ -71,6 +71,31 @@ public class SimpleParserTestCase {
         Assert.assertEquals("/somepath%2fotherPath", result.getRequestURI());
     }
 
+    @Test
+    public void testEncodedSlashDisallowed_DECODE_FLAG() throws BadRequestException {
+        byte[] in = "GET /somepath%2FotherPath HTTP/1.1\r\n\r\n".getBytes();
+
+        final ParseState context = new ParseState(10);
+        HttpServerExchange result = new HttpServerExchange(null);
+        HttpRequestParser.instance(OptionMap.create(UndertowOptions.DECODE_SLASH, false)).handle(ByteBuffer.wrap(in), context, result);
+        Assert.assertSame(Methods.GET, result.getRequestMethod());
+        Assert.assertEquals("/somepath%2FotherPath", result.getRequestURI());
+        Assert.assertEquals("/somepath%2FotherPath", result.getRequestPath());
+    }
+
+    @Test
+    public void testEncodedSlashAllowed_DECODE_FLAG() throws BadRequestException {
+        byte[] in = "GET /somepath%2fotherPath HTTP/1.1\r\n\r\n".getBytes();
+
+        final ParseState context = new ParseState(10);
+        HttpServerExchange result = new HttpServerExchange(null);
+        //this also tests override of UndertowOptions.ALLOW_ENCODED_SLASH
+        HttpRequestParser.instance(OptionMap.create(UndertowOptions.ALLOW_ENCODED_SLASH, false, UndertowOptions.DECODE_SLASH, true)).handle(ByteBuffer.wrap(in), context, result);
+        Assert.assertSame(Methods.GET, result.getRequestMethod());
+        Assert.assertEquals("/somepath/otherPath", result.getRequestPath());
+        Assert.assertEquals("/somepath%2fotherPath", result.getRequestURI());
+    }
+
     @Test
     public void testColonSlashInURL() throws BadRequestException {
         byte[] in = "GET /a/http://myurl.com/b/c HTTP/1.1\r\n\r\n".getBytes();
@@ -549,6 +574,24 @@ public class SimpleParserTestCase {
 
     }
 
+    @Test
+    public void testQueryParams_DECODE_FLAG() throws BadRequestException {
+        byte[] in = "GET http://www.somehost.net/somepath?a=b%3e%2F&b=c&d&e&f= HTTP/1.1\nHost: \t www.somehost.net\nOtherHeader:\tsome\n \t value\n\r\n".getBytes();
+
+        final ParseState context = new ParseState(10);
+        HttpServerExchange result = new HttpServerExchange(null);
+        HttpRequestParser.instance(OptionMap.create(UndertowOptions.DECODE_SLASH, false)).handle(ByteBuffer.wrap(in), context, result);
+        Assert.assertEquals("/somepath", result.getRelativePath());
+        Assert.assertEquals("http://www.somehost.net/somepath", result.getRequestURI());
+        Assert.assertEquals("a=b%3e%2F&b=c&d&e&f=", result.getQueryString());
+        Assert.assertEquals("b>/", result.getQueryParameters().get("a").getFirst());
+        Assert.assertEquals("c", result.getQueryParameters().get("b").getFirst());
+        Assert.assertEquals("", result.getQueryParameters().get("d").getFirst());
+        Assert.assertEquals("", result.getQueryParameters().get("e").getFirst());
+        Assert.assertEquals("", result.getQueryParameters().get("f").getFirst());
+
+    }
+
     @Test
     public void testSameHttpStringReturned() throws BadRequestException {
         byte[] in = "GET http://www.somehost.net/somepath HTTP/1.1\nHost: \t www.somehost.net\nAccept-Charset:\tsome\n \t  value\n\r\n".getBytes();
diff --git a/core/src/test/java/io/undertow/testutils/DefaultServer.java b/core/src/test/java/io/undertow/testutils/DefaultServer.java
index 5fe9844..7055a2d 100644
--- a/core/src/test/java/io/undertow/testutils/DefaultServer.java
+++ b/core/src/test/java/io/undertow/testutils/DefaultServer.java
@@ -954,6 +954,24 @@ public class DefaultServer extends BlockJUnit4ClassRunner {
         }
     }
 
+    public static OptionMap getProxyOptions() {
+        if (proxyOpenListener != null) {
+            return proxyOpenListener.getUndertowOptions();
+        } else {
+            return null;
+        }
+    }
+
+    public static void setProxyOptions(final OptionMap options) {
+        OptionMap.Builder builder = OptionMap.builder().addAll(options);
+        builder = builder.set(UndertowOptions.BUFFER_PIPELINED_DATA, true);
+
+        if (proxyOpenListener != null) {
+            proxyOpenListener.setUndertowOptions(builder.getMap());
+            proxyOpenListener.closeConnections();
+        }
+    }
+
     public static void setServerOptions(final OptionMap options) {
         serverOptionMapBuilder = OptionMap.builder().addAll(options);
     }
diff --git a/coverage-report/pom.xml b/coverage-report/pom.xml
index 63d6ea4..44330ac 100644
--- a/coverage-report/pom.xml
+++ b/coverage-report/pom.xml
@@ -3,7 +3,7 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
     <artifactId>undertow-coverage-report</artifactId>
     <name>Undertow Test Coverage Report</name>
diff --git a/debian/changelog b/debian/changelog
index 39039f5..301bd75 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,9 +1,13 @@
-undertow (2.2.22-1) UNRELEASED; urgency=medium
+undertow (2.2.24+ds-1) UNRELEASED; urgency=medium
 
+  [ Markus Koschany ]
   * New upstream version 2.2.22.
   * Declare compliance with Debian Policy 4.6.2.
 
- -- Markus Koschany <apo@debian.org>  Sun, 01 Jan 2023 23:26:51 +0100
+  [ Debian Janitor ]
+  * New upstream release.
+
+ -- Markus Koschany <apo@debian.org>  Wed, 07 Jun 2023 05:14:55 -0000
 
 undertow (2.2.21-1) unstable; urgency=medium
 
diff --git a/debian/patches/ALPN.patch b/debian/patches/ALPN.patch
index 5a8f392..c1ef268 100644
--- a/debian/patches/ALPN.patch
+++ b/debian/patches/ALPN.patch
@@ -10,11 +10,10 @@ Forwarded: not-needed
  1 file changed, 252 insertions(+)
  create mode 100644 core/src/main/java/org/eclipse/jetty/alpn/ALPN.java
 
-diff --git a/core/src/main/java/org/eclipse/jetty/alpn/ALPN.java b/core/src/main/java/org/eclipse/jetty/alpn/ALPN.java
-new file mode 100644
-index 0000000..6a70617
+Index: undertow.git/core/src/main/java/org/eclipse/jetty/alpn/ALPN.java
+===================================================================
 --- /dev/null
-+++ b/core/src/main/java/org/eclipse/jetty/alpn/ALPN.java
++++ undertow.git/core/src/main/java/org/eclipse/jetty/alpn/ALPN.java
 @@ -0,0 +1,252 @@
 +//
 +//  ========================================================================
diff --git a/debian/patches/java9.patch b/debian/patches/java9.patch
index 02bf3ba..1769b73 100644
--- a/debian/patches/java9.patch
+++ b/debian/patches/java9.patch
@@ -10,11 +10,11 @@ Forwarded: no
  pom.xml | 8 ++++++++
  1 file changed, 8 insertions(+)
 
-diff --git a/pom.xml b/pom.xml
-index c6b37d4..a6c45aa 100644
---- a/pom.xml
-+++ b/pom.xml
-@@ -657,6 +657,14 @@
+Index: undertow.git/pom.xml
+===================================================================
+--- undertow.git.orig/pom.xml
++++ undertow.git/pom.xml
+@@ -585,6 +585,14 @@
                                  </compilerArgs>
                              </configuration>
                          </plugin>
diff --git a/debian/patches/no-wildfly.patch b/debian/patches/no-wildfly.patch
index 0892736..00c7ac2 100644
--- a/debian/patches/no-wildfly.patch
+++ b/debian/patches/no-wildfly.patch
@@ -7,11 +7,11 @@ Forwarded: not-needed
  core/src/test/java/io/undertow/testutils/DefaultServer.java | 6 ------
  1 file changed, 6 deletions(-)
 
-diff --git a/core/src/test/java/io/undertow/testutils/DefaultServer.java b/core/src/test/java/io/undertow/testutils/DefaultServer.java
-index adb3b58..a970dad 100644
---- a/core/src/test/java/io/undertow/testutils/DefaultServer.java
-+++ b/core/src/test/java/io/undertow/testutils/DefaultServer.java
-@@ -85,7 +85,6 @@ import org.junit.runners.model.FrameworkMethod;
+Index: undertow.git/core/src/test/java/io/undertow/testutils/DefaultServer.java
+===================================================================
+--- undertow.git.orig/core/src/test/java/io/undertow/testutils/DefaultServer.java
++++ undertow.git/core/src/test/java/io/undertow/testutils/DefaultServer.java
+@@ -85,7 +85,6 @@ import org.junit.runners.model.Framework
  import org.junit.runners.model.InitializationError;
  import org.junit.runners.model.Statement;
  import org.junit.runners.model.TestClass;
@@ -19,7 +19,7 @@ index adb3b58..a970dad 100644
  import org.xnio.ChannelListener;
  import org.xnio.ChannelListeners;
  import org.xnio.IoUtils;
-@@ -162,11 +161,6 @@ public class DefaultServer extends BlockJUnit4ClassRunner {
+@@ -162,11 +161,6 @@ public class DefaultServer extends Block
  
      static {
          Throwable failure = null;
diff --git a/dist/pom.xml b/dist/pom.xml
index 6e8aaf6..9d8f4ce 100644
--- a/dist/pom.xml
+++ b/dist/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-dist</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow: Distribution</name>
 
diff --git a/examples/pom.xml b/examples/pom.xml
index e3304aa..d696214 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-examples</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Examples</name>
 
diff --git a/karaf/pom.xml b/karaf/pom.xml
index 60c1b2e..3dc22cd 100644
--- a/karaf/pom.xml
+++ b/karaf/pom.xml
@@ -23,12 +23,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>karaf</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Karaf feature</name>
 
diff --git a/parser-generator/pom.xml b/parser-generator/pom.xml
index 397e816..73056fb 100644
--- a/parser-generator/pom.xml
+++ b/parser-generator/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-parser-generator</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Parser Generator</name>
     <description>An annotation processor that is used to generate the HTTP parser</description>
diff --git a/pom.xml b/pom.xml
index 87ca920..55c8e4b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-parent</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow</name>
     <description>Undertow</description>
diff --git a/servlet/pom.xml b/servlet/pom.xml
index a971fe4..839be88 100644
--- a/servlet/pom.xml
+++ b/servlet/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-servlet</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow Servlet</name>
 
diff --git a/servlet/src/main/java/io/undertow/servlet/core/SessionListenerBridge.java b/servlet/src/main/java/io/undertow/servlet/core/SessionListenerBridge.java
index 42d658e..6c691c1 100644
--- a/servlet/src/main/java/io/undertow/servlet/core/SessionListenerBridge.java
+++ b/servlet/src/main/java/io/undertow/servlet/core/SessionListenerBridge.java
@@ -32,6 +32,7 @@ import io.undertow.servlet.api.Deployment;
 import io.undertow.servlet.api.ThreadSetupHandler;
 import io.undertow.servlet.handlers.ServletRequestContext;
 import io.undertow.servlet.spec.HttpSessionImpl;
+import io.undertow.servlet.spec.ServletContextImpl;
 
 /**
  * Class that bridges between Undertow native session listeners and servlet ones.
@@ -60,7 +61,10 @@ public class SessionListenerBridge implements SessionListener {
 
     @Override
     public void sessionCreated(final Session session, final HttpServerExchange exchange) {
-        final HttpSessionImpl httpSession = SecurityActions.forSession(session, servletContext, true);
+        ServletContext sc = servletContext;
+        if (servletContext instanceof ServletContextImpl)
+            sc = exchange.removeAttachment(((ServletContextImpl) servletContext).contextAttachmentKey);
+        final HttpSessionImpl httpSession = SecurityActions.forSession(session, sc, true);
         applicationListeners.sessionCreated(httpSession);
     }
 
diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/security/SecurityPathMatches.java b/servlet/src/main/java/io/undertow/servlet/handlers/security/SecurityPathMatches.java
index 81e6bf9..e7d996d 100644
--- a/servlet/src/main/java/io/undertow/servlet/handlers/security/SecurityPathMatches.java
+++ b/servlet/src/main/java/io/undertow/servlet/handlers/security/SecurityPathMatches.java
@@ -75,17 +75,23 @@ public class SecurityPathMatches {
      * @return <code>true</code> If no security path information has been defined
      */
     public boolean isEmpty() {
-        return defaultPathSecurityInformation.excludedMethodRoles.isEmpty() &&
-                defaultPathSecurityInformation.perMethodRequiredRoles.isEmpty() &&
-                defaultPathSecurityInformation.defaultRequiredRoles.isEmpty() &&
+        return isDefaultPathSecurityEmpty() &&
                 exactPathRoleInformation.isEmpty() &&
                 prefixPathRoleInformation.isEmpty() &&
                 extensionRoleInformation.isEmpty();
     }
 
+    public boolean isDefaultPathSecurityEmpty() {
+        return defaultPathSecurityInformation.excludedMethodRoles.isEmpty() &&
+                defaultPathSecurityInformation.perMethodRequiredRoles.isEmpty() &&
+                defaultPathSecurityInformation.defaultRequiredRoles.isEmpty();
+    }
+
     public SecurityPathMatch getSecurityInfo(final String path, final String method) {
         RuntimeMatch currentMatch = new RuntimeMatch();
-        handleMatch(method, defaultPathSecurityInformation, currentMatch);
+        if (!isDefaultPathSecurityEmpty()) {
+            handleMatch(method, defaultPathSecurityInformation, currentMatch);
+        }
         PathSecurityInformation match = exactPathRoleInformation.get(path);
         PathSecurityInformation extensionMatch = null;
         if (match != null) {
@@ -184,16 +190,24 @@ public class SecurityPathMatches {
                 transport(currentMatch, role.transportGuaranteeType);
                 currentMatch.constraints.add(new SingleConstraintMatch(role.emptyRoleSemantic, role.roles));
             }
-        } else if(denyUncoveredHttpMethods) {
-            if(exact.perMethodRequiredRoles.size() == 0) {
-                // 13.8.4. When HTTP methods are not enumerated within a security-constraint, the protections defined by the
-                // constraint apply to the complete set of HTTP (extension) methods.
-                currentMatch.uncovered = false;
-                currentMatch.constraints.add(new SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic.PERMIT, new HashSet<>()));
-            } else if(exact.perMethodRequiredRoles.size() > 0) {
-                //at this point method is null, but there is match, above if will be triggered for default path, we need to flip it?
-                currentMatch.uncovered = true;
-                //NOTE: ?
+        } else if (denyUncoveredHttpMethods) {
+            if (exact.perMethodRequiredRoles.size() == 0) {
+                if (exact.excludedMethodRoles.isEmpty()) {
+                    // 13.8.4. When HTTP methods are not enumerated within a security-constraint, the protections defined by the
+                    // constraint apply to the complete set of HTTP (extension) methods.
+                    currentMatch.uncovered = false;
+                } else {
+                    for (ExcludedMethodRoles excluded : exact.excludedMethodRoles) {
+                        if (!excluded.methods.contains(method)) {
+                            currentMatch.uncovered = false;
+                            break;
+                        }
+                    }
+                }
+            }
+            if (currentMatch.uncovered) {
+                //at this point method info is null, but there is match, above if will be triggered for default path, we need to flip it?
+                // keep currentMatch.uncovered value as true (this is the value that is initially set)
                 currentMatch.constraints.clear();
                 currentMatch.constraints.add(new SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic.DENY, new HashSet<>()));
             }
diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/security/ServletSecurityConstraintHandler.java b/servlet/src/main/java/io/undertow/servlet/handlers/security/ServletSecurityConstraintHandler.java
index 6758778..230ba1d 100644
--- a/servlet/src/main/java/io/undertow/servlet/handlers/security/ServletSecurityConstraintHandler.java
+++ b/servlet/src/main/java/io/undertow/servlet/handlers/security/ServletSecurityConstraintHandler.java
@@ -43,6 +43,7 @@ public class ServletSecurityConstraintHandler implements HttpHandler {
     @Override
     public void handleRequest(final HttpServerExchange exchange) throws Exception {
         final String path = exchange.getRelativePath();
+
         SecurityPathMatch securityMatch = securityPathMatches.getSecurityInfo(path, exchange.getRequestMethod().toString());
         final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
         List<SingleConstraintMatch> list = servletRequestContext.getRequiredConstrains();
diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletContextImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletContextImpl.java
index 1321be5..48580ea 100644
--- a/servlet/src/main/java/io/undertow/servlet/spec/ServletContextImpl.java
+++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletContextImpl.java
@@ -115,6 +115,7 @@ public class ServletContextImpl implements ServletContext {
     private final ConcurrentMap<String, Object> attributes;
     private final SessionCookieConfigImpl sessionCookieConfig;
     private final AttachmentKey<HttpSessionImpl> sessionAttachmentKey = AttachmentKey.create(HttpSessionImpl.class);
+    public static final AttachmentKey<ServletContextImpl> contextAttachmentKey = AttachmentKey.create(ServletContextImpl.class);
     private volatile Set<SessionTrackingMode> sessionTrackingModes = new HashSet<>(Arrays.asList(new SessionTrackingMode[]{SessionTrackingMode.COOKIE, SessionTrackingMode.URL}));
     private volatile Set<SessionTrackingMode> defaultSessionTrackingModes = new HashSet<>(Arrays.asList(new SessionTrackingMode[]{SessionTrackingMode.COOKIE, SessionTrackingMode.URL}));
     private volatile SessionConfig sessionConfig;
@@ -982,6 +983,7 @@ public class ServletContextImpl implements ServletContext {
 
                 try {
                     if (httpSession == null) {
+                        exchange.putAttachment(contextAttachmentKey, this);
                         final Session newSession = sessionManager.createSession(exchange, c);
                         httpSession = SecurityActions.forSession(newSession, this, true);
                         exchange.putAttachment(sessionAttachmentKey, httpSession);
diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletPrintWriter.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletPrintWriter.java
index 9672a27..b959557 100644
--- a/servlet/src/main/java/io/undertow/servlet/spec/ServletPrintWriter.java
+++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletPrintWriter.java
@@ -22,6 +22,7 @@ import javax.servlet.DispatcherType;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
@@ -193,47 +194,28 @@ public class ServletPrintWriter {
     }
 
     public void write(final int c) {
-        write(Character.toString((char)c));
+        write(Character.toString((char) c));
     }
 
     public void write(final char[] buf, final int off, final int len) {
-        if(charsetEncoder == null) {
+        if (charsetEncoder == null) {
             try {
                 ByteBuffer buffer = outputStream.underlyingBuffer();
-                if(buffer == null) {
+                if (buffer == null) {
                     //already closed
                     error = true;
                     return;
                 }
                 //fast path, basically we are hoping this is ascii only
                 int remaining = buffer.remaining();
-                boolean ok = true;
-                //so we have a pure ascii buffer, just write it out and skip all the encoder cost
-
                 int end = off + len;
-                int i = off;
-                int flushPos = i + remaining;
-                while (ok && i < end) {
-                    int realEnd = Math.min(end, flushPos);
-                    for (; i < realEnd; ++i) {
-                        char c = buf[i];
-                        if (c > 127) {
-                            ok = false;
-                            break;
-                        } else {
-                            buffer.put((byte) c);
-                        }
-                    }
-                    if (i == flushPos) {
-                        outputStream.flushInternal();
-                        flushPos = i + buffer.remaining();
-                    }
-                }
+                //so we have a pure ascii buffer, just write it out and skip all the encoder cost
+                final int sPos = writeAndFlushAscii(outputStream, buffer, buf, off, off + len);
                 outputStream.updateWritten(remaining - buffer.remaining());
-                if (ok) {
+                if (sPos == end) {
                     return;
                 }
-                final CharBuffer cb = CharBuffer.wrap(buf, i, len - (i - off));
+                final CharBuffer cb = CharBuffer.wrap(buf, sPos, len - (sPos - off));
                 write(cb);
                 return;
             } catch (IOException e) {
@@ -246,45 +228,84 @@ public class ServletPrintWriter {
         write(cb);
     }
 
+    private static int writeAndFlushAscii(ServletOutputStreamImpl outputStream, ByteBuffer buffer, char[] chars, int start, int end) throws IOException {
+        assert buffer.order() == ByteOrder.BIG_ENDIAN;
+        int i = start;
+        while (i < end) {
+            final int bufferPos = buffer.position();
+            final int bufferRemaining = buffer.remaining();
+            final int sRemaining = end - i;
+            final int remaining = Math.min(sRemaining, bufferRemaining);
+            final int written = setAsciiBE(buffer, bufferPos, chars, i, remaining);
+            i += written;
+            buffer.position(bufferPos + written);
+            if (!buffer.hasRemaining()) {
+                outputStream.flushInternal();
+            }
+            // we have given up with the fast path? slow path NOW!
+            if (written < remaining) {
+                return i;
+            }
+        }
+        return i;
+    }
+
+    private static int setAsciiBE(ByteBuffer buffer, int out, char[] chars, int off, int len) {
+        final int longRounds = len >>> 3;
+        for (int i = 0; i < longRounds; i++) {
+            final long batch1 = (((long) chars[off]) << 48) |
+                    (((long) chars[off + 2]) << 32) |
+                    chars[off + 4] << 16 |
+                    chars[off + 6];
+            final long batch2 = (((long) chars[off + 1]) << 48) |
+                    (((long) chars[off + 3]) << 32) |
+                    chars[off + 5] << 16 |
+                    chars[off + 7];
+            if (((batch1 | batch2) & 0xff80_ff80_ff80_ff80L) != 0) {
+                return i << 3;
+            }
+            final long batch = (batch1 << 8) | batch2;
+            buffer.putLong(out, batch);
+            out += Long.BYTES;
+            off += Long.BYTES;
+        }
+        final int byteRounds = len & 7;
+        if (byteRounds > 0) {
+            for (int i = 0; i < byteRounds; i++) {
+                final char c = chars[off + i];
+                if (c > 127) {
+                    return (longRounds << 3) + i;
+                }
+                buffer.put(out + i, (byte) c);
+            }
+        }
+        return len;
+    }
+
     public void write(final char[] buf) {
-        write(buf,0, buf.length);
+        write(buf, 0, buf.length);
     }
 
     public void write(final String s, final int off, final int len) {
-        if(charsetEncoder == null) {
+        if (charsetEncoder == null) {
             try {
                 ByteBuffer buffer = outputStream.underlyingBuffer();
-                if(buffer == null) {
+                if (buffer == null) {
                     //already closed
                     error = true;
                     return;
                 }
                 //fast path, basically we are hoping this is ascii only
                 int remaining = buffer.remaining();
-                boolean ok = true;
-                //so we have a pure ascii buffer, just write it out and skip all the encoder cost
-
                 int end = off + len;
-                int i = off;
-                int fpos = i + remaining;
-                for (; i < end; ++i) {
-                    if (i == fpos) {
-                        outputStream.flushInternal();
-                        fpos = i + buffer.remaining();
-                    }
-                    char c = s.charAt(i);
-                    if (c > 127) {
-                        ok = false;
-                        break;
-                    }
-                    buffer.put((byte) c);
-                }
+                //so we have a pure ascii buffer, just write it out and skip all the encoder cost
+                final int sPos = writeAndFlushAscii(outputStream, buffer, s, off, off + len);
                 outputStream.updateWritten(remaining - buffer.remaining());
-                if (ok) {
+                if (sPos == end) {
                     return;
                 }
                 //wrap(String, off, len) acts wrong in the presence of multi byte characters
-                final CharBuffer cb = CharBuffer.wrap(s.toCharArray(), i, len - (i - off));
+                final CharBuffer cb = CharBuffer.wrap(s.toCharArray(), sPos, len - (sPos - off));
                 write(cb);
                 return;
             } catch (IOException e) {
@@ -297,6 +318,60 @@ public class ServletPrintWriter {
         write(cb);
     }
 
+    private static int writeAndFlushAscii(ServletOutputStreamImpl outputStream, ByteBuffer buffer, String s, int start, int end) throws IOException {
+        assert buffer.order() == ByteOrder.BIG_ENDIAN;
+        int i = start;
+        while (i < end) {
+            final int bufferPos = buffer.position();
+            final int bufferRemaining = buffer.remaining();
+            final int sRemaining = end - i;
+            final int remaining = Math.min(sRemaining, bufferRemaining);
+            final int written = setAsciiBE(buffer, bufferPos, s, i, remaining);
+            i += written;
+            buffer.position(bufferPos + written);
+            if (!buffer.hasRemaining()) {
+                outputStream.flushInternal();
+            }
+            // we have given up with the fast path? slow path NOW!
+            if (written < remaining) {
+                return i;
+            }
+        }
+        return i;
+    }
+
+    private static int setAsciiBE(ByteBuffer buffer, int out, String s, int off, int len) {
+        final int longRounds = len >>> 3;
+        for (int i = 0; i < longRounds; i++) {
+            final long batch1 = ((long)s.charAt(off)) << 48 |
+                    ((long) s.charAt(off + 2)) << 32 |
+                    s.charAt(off + 4) << 16 |
+                    s.charAt(off + 6);
+            final long batch2 = ((long)s.charAt(off + 1)) << 48 |
+                    ((long) s.charAt(off + 3)) << 32 |
+                    s.charAt(off + 5) << 16 |
+                    s.charAt(off + 7);
+            if (((batch1 | batch2) & 0xff80_ff80_ff80_ff80L) != 0) {
+                return i << 3;
+            }
+            final long batch = (batch1 << 8) | batch2;
+            buffer.putLong(out, batch);
+            out += Long.BYTES;
+            off += Long.BYTES;
+        }
+        final int byteRounds = len & 7;
+        if (byteRounds > 0) {
+            for (int i = 0; i < byteRounds; i++) {
+                final char c = s.charAt(off + i);
+                if (c > 127) {
+                    return (longRounds << 3) + i;
+                }
+                buffer.put(out + i, (byte) c);
+            }
+        }
+        return len;
+    }
+
     public void write(final String s) {
         write(s, 0, s.length());
     }
diff --git a/servlet/src/test/java/io/undertow/servlet/test/security/constraint/AuthenticationMessageServlet.java b/servlet/src/test/java/io/undertow/servlet/test/security/constraint/AuthenticationMessageServlet.java
index f9fffa0..aa0e4c7 100644
--- a/servlet/src/test/java/io/undertow/servlet/test/security/constraint/AuthenticationMessageServlet.java
+++ b/servlet/src/test/java/io/undertow/servlet/test/security/constraint/AuthenticationMessageServlet.java
@@ -45,6 +45,26 @@ public class AuthenticationMessageServlet extends MessageServlet {
         doGet(req, resp);
     }
 
+    @Override
+    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        doGet(req, resp);
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        doGet(req, resp);
+    }
+
+    @Override
+    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        doGet(req, resp);
+    }
+
+    @Override
+    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        doGet(req, resp);
+    }
+
     private void checkExpectedMechanism(HttpServletRequest req) {
         String expectedMechanism = req.getHeader("ExpectedMechanism");
         if (expectedMechanism == null) {
@@ -56,7 +76,7 @@ public class AuthenticationMessageServlet extends MessageServlet {
             }
         } else if (expectedMechanism.equals("BASIC")) {
             if (req.getAuthType() != HttpServletRequest.BASIC_AUTH) {
-                throw new IllegalStateException("Expected mechanism type not matched.");
+                throw new IllegalStateException("Expected mechanism type not matched: " + req.getAuthType());
             }
         } else {
             throw new IllegalStateException("ExpectedMechanism not recognised.");
diff --git a/servlet/src/test/java/io/undertow/servlet/test/security/constraint/DenyUncoveredHttpMethodsTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/security/constraint/DenyUncoveredHttpMethodsTestCase.java
new file mode 100644
index 0000000..778b234
--- /dev/null
+++ b/servlet/src/test/java/io/undertow/servlet/test/security/constraint/DenyUncoveredHttpMethodsTestCase.java
@@ -0,0 +1,301 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2023 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package io.undertow.servlet.test.security.constraint;
+
+import io.undertow.server.handlers.PathHandler;
+import io.undertow.servlet.api.DeploymentInfo;
+import io.undertow.servlet.api.DeploymentManager;
+import io.undertow.servlet.api.LoginConfig;
+import io.undertow.servlet.api.SecurityConstraint;
+import io.undertow.servlet.api.SecurityInfo;
+import io.undertow.servlet.api.ServletContainer;
+import io.undertow.servlet.api.ServletInfo;
+import io.undertow.servlet.api.WebResourceCollection;
+import io.undertow.servlet.test.util.MessageServlet;
+import io.undertow.servlet.test.util.TestClassIntrospector;
+import io.undertow.testutils.DefaultServer;
+import io.undertow.testutils.HttpClientUtils;
+import io.undertow.testutils.TestHttpClient;
+import io.undertow.util.FlexBase64;
+import javax.servlet.ServletException;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpTrace;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+
+import static io.undertow.util.Headers.AUTHORIZATION;
+import static io.undertow.util.Headers.BASIC;
+import static io.undertow.util.Headers.WWW_AUTHENTICATE;
+import static io.undertow.util.StatusCodes.FORBIDDEN;
+import static io.undertow.util.StatusCodes.OK;
+import static io.undertow.util.StatusCodes.UNAUTHORIZED;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test that verifies correct handling of &lt;deny-uncovered-http-methods/&gt;.
+ *
+ * @author Flavia Rainone
+ */
+@RunWith(DefaultServer.class)
+public class DenyUncoveredHttpMethodsTestCase {
+
+    public static final String HELLO_WORLD = "Hello World";
+
+    @BeforeClass
+    public static void setup() throws ServletException {
+
+        final PathHandler root = new PathHandler();
+        final ServletContainer container = ServletContainer.Factory.newInstance();
+
+        ServletInfo s = new ServletInfo("servlet", AuthenticationMessageServlet.class)
+                .addInitParam(MessageServlet.MESSAGE, HELLO_WORLD)
+                .addMapping("/vanilla")
+                .addMapping("/fully-covered")
+                .addMapping("/fully-covered-permit-no-role")
+                .addMapping("/fully-covered-deny-no-role")
+                .addMapping("/omitted-methods")
+                .addMapping("/omitted-methods-deny-empty-role");
+
+        ServletIdentityManager identityManager = new ServletIdentityManager();
+        identityManager.addUser("user", "password", "role");
+        identityManager.addUser("unauthorized-user", "password", "unauthorized-role");
+
+        DeploymentInfo builder = new DeploymentInfo()
+                .setClassLoader(DenyUncoveredHttpMethodsTestCase.class.getClassLoader())
+                .setContextPath("/servletContext")
+                .setClassIntrospecter(TestClassIntrospector.INSTANCE)
+                .setDeploymentName("servletContext.war")
+                .setIdentityManager(identityManager)
+                .setLoginConfig(new LoginConfig("BASIC", "Test Realm"))
+                .addServlet(s);
+
+        // deny uncovered http methods
+        builder.setDenyUncoveredHttpMethods(true);
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/vanilla").addHttpMethod("GET").addHttpMethod("POST"))
+                .addRoleAllowed("role").setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT));
+
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/fully-covered"))
+                .addRoleAllowed("role"));
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/fully-covered-permit-no-role"))
+                .addRoleAllowed("role").setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT));
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/fully-covered-deny-no-role"))
+                .addRoleAllowed("role").setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.DENY));
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/omitted-methods").addHttpMethodOmission("GET"))
+                .setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT));
+
+        builder.addSecurityConstraint(new SecurityConstraint().addWebResourceCollection(new WebResourceCollection()
+                        .addUrlPattern("/omitted-methods-deny-empty-role").addHttpMethodOmission("GET"))
+                .setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.DENY));
+
+        DeploymentManager manager = container.addDeployment(builder);
+        manager.deploy();
+        root.addPrefixPath(builder.getContextPath(), manager.start());
+        DefaultServer.setRootHandler(root);
+    }
+
+    @Test
+    public void testVanillaSecurityConstraint() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        final String url = DefaultServer.getDefaultServerURL() + "/servletContext/vanilla";
+        try {
+            testAccessible(()->new HttpGet(url), client, false);
+            testAccessible(()->new HttpPost(url), client, false);
+            testForbidden(()->new HttpPut(url), client);
+            testForbidden(()->new HttpDelete(url), client);
+            testForbidden(()->new HttpOptions(url), client);
+            testForbidden(()->new HttpTrace(url), client);
+
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    @Test
+    public void testFullyCoveredConstraint() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        final String url = DefaultServer.getDefaultServerURL() + "/servletContext/fully-covered";
+        try {
+            testAccessible(()->new HttpGet(url), client, false);
+            testAccessible(()->new HttpPost(url), client, false);
+            testAccessible(()->new HttpPut(url), client, false);
+            testAccessible(()->new HttpDelete(url), client, false);
+            testAccessible(()->new HttpOptions(url), client, false);
+            testAccessible(()->new HttpTrace(url), client, false);
+
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    @Test
+    public void testFullyCoveredPermitNoRoleConstraint() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        final String url = DefaultServer.getDefaultServerURL() + "/servletContext/fully-covered-permit-no-role";
+        try {
+            testAccessible(()->new HttpGet(url), client, false);
+            testAccessible(()->new HttpPost(url), client, false);
+            testAccessible(()->new HttpPut(url), client, false);
+            testAccessible(()->new HttpDelete(url), client, false);
+            testAccessible(()->new HttpOptions(url), client, false);
+            testAccessible(()->new HttpTrace(url), client, false);
+
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    @Test
+    public void testFullyCoveredDenyNoRoleConstraint() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        final String url = DefaultServer.getDefaultServerURL() + "/servletContext/fully-covered-deny-no-role";
+        try {
+            testAccessible(()->new HttpGet(url), client, false);
+            testAccessible(()->new HttpPost(url), client, false);
+            testAccessible(()->new HttpPut(url), client, false);
+            testAccessible(()->new HttpDelete(url), client, false);
+            testAccessible(()->new HttpOptions(url), client, false);
+            testAccessible(()->new HttpTrace(url), client, false);
+
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    @Test
+    public void testOmittedMethods() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        String url = DefaultServer.getDefaultServerURL() + "/servletContext/omitted-methods";
+        try {
+            testForbidden(()->new HttpGet(url), client);
+            testAccessible(()->new HttpPost(url), client, true);
+            testAccessible(()->new HttpPut(url), client, true);
+            testAccessible(()->new HttpDelete(url), client, true);
+            testAccessible(()->new HttpOptions(url), client, true);
+            testAccessible(()->new HttpTrace(url), client, true);
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    @Test
+    public void testOmittedMethodsDenyEmptyRole() throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        String url = DefaultServer.getDefaultServerURL() + "/servletContext/omitted-methods-deny-empty-role";
+        try {
+            testForbidden(()->new HttpGet(url), client);
+            testForbidden(()->new HttpPost(url), client);
+            testForbidden(()->new HttpPut(url), client);
+            testForbidden(()->new HttpDelete(url), client);
+            testForbidden(()->new HttpOptions(url), client);
+            testForbidden(()->new HttpTrace(url), client);
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+
+    public void testAccessible(Supplier<org.apache.http.client.methods.HttpRequestBase> newRequest, TestHttpClient client, boolean noRolePermitted) throws IOException {
+        HttpRequestBase request = newRequest.get();
+        request.addHeader("ExpectedMechanism", "None");
+        request.addHeader("ExpectedUser", "None");
+        HttpResponse result = client.execute(request);
+        if (noRolePermitted) {
+            assertEquals(OK, result.getStatusLine().getStatusCode());
+        } else {
+            assertEquals(UNAUTHORIZED, result.getStatusLine().getStatusCode());
+            Header[] values = result.getHeaders(WWW_AUTHENTICATE.toString());
+            assertEquals(1, values.length);
+            assertEquals(BASIC + " realm=\"Test Realm\"", values[0].getValue());
+        }
+        HttpClientUtils.readResponse(result);
+
+        request = newRequest.get();
+        request.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("user:password".getBytes(), false));
+        request.addHeader("ExpectedMechanism", "BASIC");
+        request.addHeader("ExpectedUser", "user");
+        result = client.execute(request);
+        assertEquals(OK, result.getStatusLine().getStatusCode());
+        String response = HttpClientUtils.readResponse(result);
+        assertEquals(HELLO_WORLD, response);
+
+        request = newRequest.get();
+        request.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("unauthorized-user:password".getBytes(), false));
+        request.addHeader("ExpectedMechanism", "BASIC");
+        request.addHeader("ExpectedUser", "unauthorized-user");
+        result = client.execute(request);
+        if (noRolePermitted) {
+            assertEquals(OK, result.getStatusLine().getStatusCode());
+            response = HttpClientUtils.readResponse(result);
+            assertEquals(HELLO_WORLD, response);
+        } else {
+            assertEquals(FORBIDDEN, result.getStatusLine().getStatusCode());
+            HttpClientUtils.readResponse(result);
+        }
+    }
+
+
+    public void testForbidden(Supplier<HttpRequestBase> newRequest, TestHttpClient client) throws IOException {
+        HttpRequestBase request = newRequest.get();
+        HttpResponse result = client.execute(request);
+        assertEquals(FORBIDDEN, result.getStatusLine().getStatusCode());
+        Header[] values = result.getHeaders(WWW_AUTHENTICATE.toString());
+        assertEquals(0, values.length);
+        HttpClientUtils.readResponse(result);
+
+        request = newRequest.get();
+        request.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("user:password".getBytes(), false));
+        request.addHeader("ExpectedMechanism", "BASIC");
+        request.addHeader("ExpectedUser", "user");
+        result = client.execute(request);
+        assertEquals(FORBIDDEN, result.getStatusLine().getStatusCode());
+        values = result.getHeaders(WWW_AUTHENTICATE.toString());
+        assertEquals(0, values.length);
+        HttpClientUtils.readResponse(result);
+
+        request = newRequest.get();
+        request.addHeader(AUTHORIZATION.toString(), BASIC + " " + FlexBase64.encodeString("unauthorized-user:password".getBytes(), false));
+        request.addHeader("ExpectedMechanism", "BASIC");
+        request.addHeader("ExpectedUser", "user");
+        result = client.execute(request);
+        assertEquals(FORBIDDEN, result.getStatusLine().getStatusCode());
+        values = result.getHeaders(WWW_AUTHENTICATE.toString());
+        assertEquals(0, values.length);
+        HttpClientUtils.readResponse(result);
+    }
+}
diff --git a/servlet/src/test/java/io/undertow/servlet/test/streams/ServletOutputStreamClosureTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/streams/ServletOutputStreamClosureTestCase.java
new file mode 100644
index 0000000..dc86659
--- /dev/null
+++ b/servlet/src/test/java/io/undertow/servlet/test/streams/ServletOutputStreamClosureTestCase.java
@@ -0,0 +1,110 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2022 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package io.undertow.servlet.test.streams;
+
+import io.undertow.servlet.api.ServletInfo;
+import io.undertow.servlet.test.util.DeploymentUtils;
+import io.undertow.servlet.util.ImmediateInstanceFactory;
+import io.undertow.testutils.DefaultServer;
+import io.undertow.testutils.HttpClientUtils;
+import io.undertow.testutils.TestHttpClient;
+import io.undertow.util.Headers;
+import io.undertow.util.StatusCodes;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author Carter Kozak
+ */
+@RunWith(DefaultServer.class)
+public class ServletOutputStreamClosureTestCase {
+
+    @Test
+    public void testFlushAfterContentLengthReached() throws IOException {
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        DeploymentUtils.setupServlet(
+                new ServletInfo("servlet", HttpServlet.class, new ImmediateInstanceFactory<HttpServlet>(new HttpServlet() {
+                    @Override
+                    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                        resp.setHeader(Headers.CONTENT_LENGTH_STRING, "1");
+                        try {
+                            ServletOutputStream out = resp.getOutputStream();
+                            out.write(65);
+                            out.flush();
+                            out.close();
+                        } catch (Throwable t) {
+                            thrown.set(t);
+                            throw t;
+                        }
+                    }
+                })).addMapping("/*"));
+        makeSuccessfulRequest("A");
+        Throwable maybeFailure = thrown.get();
+        if (maybeFailure != null) {
+            throw new AssertionError("Unexpected failure", maybeFailure);
+        }
+    }
+
+    @Test
+    public void testFlushAfterClose() throws IOException {
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        DeploymentUtils.setupServlet(
+                new ServletInfo("servlet", HttpServlet.class, new ImmediateInstanceFactory<HttpServlet>(new HttpServlet() {
+                    @Override
+                    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+                        try {
+                            ServletOutputStream out = resp.getOutputStream();
+                            out.write(65);
+                            out.close();
+                            out.flush();
+                        } catch (Throwable t) {
+                            thrown.set(t);
+                            throw t;
+                        }
+                    }
+                })).addMapping("/*"));
+        makeSuccessfulRequest("A");
+        Throwable maybeFailure = thrown.get();
+        if (maybeFailure != null) {
+            throw new AssertionError("Unexpected failure", maybeFailure);
+        }
+    }
+
+    private static void makeSuccessfulRequest(String expectedContent) throws IOException {
+        TestHttpClient client = new TestHttpClient();
+        try {
+            HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext");
+            HttpResponse result = client.execute(get);
+            Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode());
+            Assert.assertEquals(expectedContent, HttpClientUtils.readResponse(result));
+        } finally {
+            client.getConnectionManager().shutdown();
+        }
+    }
+}
diff --git a/websockets-jsr/pom.xml b/websockets-jsr/pom.xml
index 0f77f0d..c9fb753 100644
--- a/websockets-jsr/pom.xml
+++ b/websockets-jsr/pom.xml
@@ -25,12 +25,12 @@
     <parent>
         <groupId>io.undertow</groupId>
         <artifactId>undertow-parent</artifactId>
-        <version>2.2.22.Final</version>
+        <version>2.2.24.Final</version>
     </parent>
 
     <groupId>io.undertow</groupId>
     <artifactId>undertow-websockets-jsr</artifactId>
-    <version>2.2.22.Final</version>
+    <version>2.2.24.Final</version>
 
     <name>Undertow WebSockets JSR356 implementations</name>
 

More details

Full run details

Historical runs