Repository: helidon-io/helidon (java)

Issue:
Helidon gives internal server error for an invalid host header

## Environment Details
* Helidon Version: 4.0.8
* Helidon SE or Helidon MP : Helidon MP
* JDK version: jdk 21
* OS: 
* Docker version (if applicable):

----------

curl -v http://localhost:8080/api/v1/oci/secret/full -H 'Host: w.localhost:808a'


Stack trace - 

```
Internal server error io.helidon.http.RequestException: For input string: "808a"
    at io.helidon.http.RequestException$Builder.build(RequestException.java:139) ~[helidon-http-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.ErrorHandlers.unhandledError(ErrorHandlers.java:203) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.ErrorHandlers.lambda$handleError$1(ErrorHandlers.java:182) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at java.util.Optional.ifPresentOrElse(Optional.java:198) ~[?:?]
    at io.helidon.webserver.http.ErrorHandlers.handleError(ErrorHandlers.java:181) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.ErrorHandlers.runWithErrorHandling(ErrorHandlers.java:118) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.Filters.filter(Filters.java:83) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.HttpRoutingImpl.route(HttpRoutingImpl.java:73) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http1.Http1Connection.route(Http1Connection.java:357) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http1.Http1Connection.handle(Http1Connection.java:194) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.ConnectionHandler.run(ConnectionHandler.java:165) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.common.task.InterruptableTask.call(InterruptableTask.java:47) ~[helidon-common-task-4.0.8.jar:4.0.8]
    at io.helidon.webserver.ThreadPerTaskExecutor$ThreadBoundFuture.run(ThreadPerTaskExecutor.java:239) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at java.lang.VirtualThread.run(VirtualThread.java:329) ~[?:?]\nCaused by: java.lang.NumberFormatException: For input string: "808a"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) ~[?:?]
    at java.lang.Integer.parseInt(Integer.java:662) ~[?:?]
    at java.lang.Integer.parseInt(Integer.java:778) ~[?:?]
    at io.helidon.common.uri.UriBuilderSupport$UriInfoCustomMethods.authority(UriBuilderSupport.java:69) ~[helidon-common-uri-4.0.8.jar:4.0.8]
    at io.helidon.common.uri.UriInfo$BuilderBase.authority(UriInfo.java:115) ~[helidon-common-uri-4.0.8.jar:4.0.8]
    at io.helidon.http.RequestedUriDiscoveryContext$Builder$RequestedUriDiscoveryContextImpl.uriInfo(RequestedUriDiscoveryContext.java:344) ~[helidon-http-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http1.Http1ServerRequest.createUriInfo(Http1ServerRequest.java:226) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.common.LazyValueImpl.get(LazyValueImpl.java:118) ~[helidon-common-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http1.Http1ServerRequest.requestedUri(Http1ServerRequest.java:215) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.security.SecurityContextFilter.filter(SecurityContextFilter.java:63) ~[helidon-webserver-security-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.Filters$FilterChainImpl.proceed(Filters.java:119) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.common.context.Contexts.runInContext(Contexts.java:117) ~[helidon-common-context-4.0.8.jar:4.0.8]
    at io.helidon.webserver.context.ContextRoutingFeature.filter(ContextRoutingFeature.java:50) ~[helidon-webserver-context-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.Filters$FilterChainImpl.proceed(Filters.java:119) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.Filters.executeFilters(Filters.java:87) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.Filters.lambda$filter$0(Filters.java:83) ~[helidon-webserver-4.0.8.jar:4.0.8]
    at io.helidon.webserver.http.ErrorHandlers.runWithErrorHandling(ErrorHandlers.java:75) ~[helidon-webserver-4.0.8.jar:4.0.8]
```


Seems like it is not possible to intercept this call in the filters to validate the request. None of the ExceptionMappers are also getting called.


Pull request:
4.0.12: Host header validation for server request

Backport of #9336 to Helidon 4.0.12

Resolves #9270 

### Description
`Host` header is now fully validated according to the specification in server request. If invalid, a 400 is returned (bad request).

Patch:
diff --git a/http/http/src/main/java/io/helidon/http/HostValidator.java b/http/http/src/main/java/io/helidon/http/HostValidator.java
new file mode 100644
index 0000000000..834a27b0b5
--- /dev/null
+++ b/http/http/src/main/java/io/helidon/http/HostValidator.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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.helidon.http;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Validate the host string (maybe from the {@code Host} header).
+ * <p>
+ * Validation is based on
+ * <a href="https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2">RFC-3986</a>.
+ */
+public final class HostValidator {
+    private static final Pattern IP_V4_PATTERN =
+            Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
+    private static final boolean[] HEXDIGIT = new boolean[256];
+    private static final boolean[] UNRESERVED = new boolean[256];
+    private static final boolean[] SUB_DELIMS = new boolean[256];
+
+    static {
+        // digits
+        for (int i = '0'; i <= '9'; i++) {
+            UNRESERVED[i] = true;
+        }
+        // alpha
+        for (int i = 'a'; i <= 'z'; i++) {
+            UNRESERVED[i] = true;
+        }
+        for (int i = 'A'; i <= 'Z'; i++) {
+            UNRESERVED[i] = true;
+        }
+        UNRESERVED['-'] = true;
+        UNRESERVED['.'] = true;
+        UNRESERVED['_'] = true;
+        UNRESERVED['~'] = true;
+
+        // hexdigits
+        // digits
+        for (int i = '0'; i <= '9'; i++) {
+            HEXDIGIT[i] = true;
+        }
+        // alpha
+        for (int i = 'a'; i <= 'f'; i++) {
+            HEXDIGIT[i] = true;
+        }
+        for (int i = 'A'; i <= 'F'; i++) {
+            HEXDIGIT[i] = true;
+        }
+
+        // sub-delim set
+        SUB_DELIMS['!'] = true;
+        SUB_DELIMS['$'] = true;
+        SUB_DELIMS['&'] = true;
+        SUB_DELIMS['\''] = true;
+        SUB_DELIMS['('] = true;
+        SUB_DELIMS[')'] = true;
+        SUB_DELIMS['*'] = true;
+        SUB_DELIMS['+'] = true;
+        SUB_DELIMS[','] = true;
+        SUB_DELIMS[';'] = true;
+        SUB_DELIMS['='] = true;
+    }
+
+    private HostValidator() {
+    }
+
+    /**
+     * Validate a host string.
+     *
+     * @param host host to validate
+     * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded
+     */
+    public static void validate(String host) {
+        Objects.requireNonNull(host);
+        if (host.indexOf('[') == 0 && host.indexOf(']') == host.length() - 1) {
+            validateIpLiteral(host);
+        } else {
+            validateNonIpLiteral(host);
+        }
+    }
+
+    /**
+     * An IP literal starts with {@code [} and ends with {@code ]}.
+     *
+     * @param ipLiteral host literal string, may be an IPv6 address, or IP version future
+     * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded
+     */
+    public static void validateIpLiteral(String ipLiteral) {
+        Objects.requireNonNull(ipLiteral);
+        checkNotBlank("IP Literal", ipLiteral, ipLiteral);
+
+        // IP-literal = "[" ( IPv6address / IPvFuture  ) "]"
+        if (ipLiteral.charAt(0) != '[' || ipLiteral.charAt(ipLiteral.length() - 1) != ']') {
+            throw new IllegalArgumentException("Invalid IP literal, missing square bracket(s): " + HtmlEncoder.encode(ipLiteral));
+        }
+
+        String host = ipLiteral.substring(1, ipLiteral.length() - 1);
+        checkNotBlank("Host", ipLiteral, host);
+        if (host.charAt(0) == 'v') {
+            // IP future - starts with version `v1` etc.
+            validateIpFuture(ipLiteral, host);
+            return;
+        }
+        // IPv6
+        /*
+        IPv6address   = 6( h16 ":" ) ls32
+                 /                       "::" 5( h16 ":" ) ls32
+                 / [               h16 ] "::" 4( h16 ":" ) ls32
+                 / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
+                 / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
+                 / [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
+                 / [ *4( h16 ":" ) h16 ] "::"              ls32
+                 / [ *5( h16 ":" ) h16 ] "::"              h16
+                 / [ *6( h16 ":" ) h16 ] "::"
+
+              ls32          = ( h16 ":" h16 ) / IPv4address
+              h16           = 1*4HEXDIG
+         */
+        if (host.equals("::")) {
+            // all empty
+            return;
+        }
+        if (host.equals("::1")) {
+            // localhost
+            return;
+        }
+        boolean skipped = false;
+        int segments = 0; // max segments is 8 (full IPv6 address)
+        String inProgress = host;
+        while (!inProgress.isEmpty()) {
+            if (inProgress.length() == 1) {
+                segments++;
+                validateH16(ipLiteral, inProgress);
+                break;
+            }
+            if (inProgress.charAt(0) == ':' && inProgress.charAt(1) == ':') {
+                // :: means skip everything that was before (or everything that is after)
+                if (skipped) {
+                    throw new IllegalArgumentException("Host IPv6 contains more than one skipped segment: "
+                                                               + HtmlEncoder.encode(ipLiteral));
+                }
+                skipped = true;
+                segments++;
+                inProgress = inProgress.substring(2);
+                continue;
+            }
+            if (inProgress.charAt(0) == ':') {
+                throw new IllegalArgumentException("Host IPv6 contains excessive colon: " + HtmlEncoder.encode(ipLiteral));
+            }
+            // this must be h16 (or an IPv4 address)
+            int nextColon = inProgress.indexOf(':');
+            if (nextColon == -1) {
+                // the rest of the string
+                if (inProgress.indexOf('.') == -1) {
+                    segments++;
+                    validateH16(ipLiteral, inProgress);
+                } else {
+                    Matcher matcher = IP_V4_PATTERN.matcher(inProgress);
+                    if (matcher.matches()) {
+                        validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(1));
+                        validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(2));
+                        validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(3));
+                        validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(4));
+                    } else {
+                        throw new IllegalArgumentException("Host IPv6 dual address contains invalid IPv4 address: "
+                                                                   + HtmlEncoder.encode(ipLiteral));
+                    }
+                }
+                break;
+            }
+            validateH16(ipLiteral, inProgress.substring(0, nextColon));
+            segments++;
+            if (inProgress.length() >= nextColon + 2) {
+                if (inProgress.charAt(nextColon + 1) == ':') {
+                    // double colon, keep it there
+                    inProgress = inProgress.substring(nextColon);
+                    continue;
+                }
+            }
+            inProgress = inProgress.substring(nextColon + 1);
+            if (inProgress.isBlank()) {
+                // this must fail on empty segment
+                validateH16(ipLiteral, inProgress);
+            }
+        }
+
+        if (segments > 8) {
+            throw new IllegalArgumentException("Host IPv6 address contains too many segments: " + HtmlEncoder.encode(ipLiteral));
+        }
+    }
+
+    /**
+     * Validate IPv4 address or a registered name.
+     *
+     * @param host string with either an IPv4 address, or a registered name
+     * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded
+     */
+    public static void validateNonIpLiteral(String host) {
+        Objects.requireNonNull(host);
+        checkNotBlank("Host", host, host);
+
+        // Ipv4 address: 127.0.0.1
+        Matcher matcher = IP_V4_PATTERN.matcher(host);
+        if (matcher.matches()) {
+            /*
+              IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
+              dec-octet   = DIGIT                 ; 0-9
+                  / %x31-39 DIGIT         ; 10-99
+                  / "1" 2DIGIT            ; 100-199
+                  / "2" %x30-34 DIGIT     ; 200-249
+                  / "25" %x30-35          ; 250-255
+            */
+
+            // we have found an IPv4 address, or a valid registered name (555.555.555.555 is a valid name...)
+            return;
+        }
+
+        // everything else is a registered name
+
+        // registered name
+        /*
+        reg-name    = *( unreserved / pct-encoded / sub-delims )
+        pct-encoded = "%" HEXDIG HEXDIG
+        unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
+        sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
+                  / "*" / "+" / "," / ";" / "="
+        */
+        char[] charArray = host.toCharArray();
+        for (int i = 0; i < charArray.length; i++) {
+            char c = charArray[i];
+            if (c > 255) {
+                throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(host));
+            }
+            if (UNRESERVED[c]) {
+                continue;
+            }
+            if (SUB_DELIMS[c]) {
+                continue;
+            }
+            if (c == '%') {
+                // percent encoding
+                if (i + 2 >= charArray.length) {
+                    throw new IllegalArgumentException("Host contains invalid % encoding: " + HtmlEncoder.encode(host));
+                }
+                char p1 = charArray[++i];
+                char p2 = charArray[++i];
+                // %p1p2
+                if (p1 > 255 || p2 > 255) {
+                    throw new IllegalArgumentException("Host contains invalid character in % encoding: "
+                                                               + HtmlEncoder.encode(host));
+                }
+                if (HEXDIGIT[p1] && HEXDIGIT[p2]) {
+                    continue;
+                }
+                throw new IllegalArgumentException("Host contains non-hexadecimal character in % encoding: "
+                                                           + HtmlEncoder.encode(host));
+            }
+            throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(host));
+        }
+    }
+
+    private static void validateH16(String host, String inProgress) {
+        if (inProgress.isBlank()) {
+            throw new IllegalArgumentException("IPv6 segment is empty: " + HtmlEncoder.encode(host));
+        }
+        if (inProgress.length() > 4) {
+            throw new IllegalArgumentException("IPv6 segment has more than 4 characters: " + HtmlEncoder.encode(host));
+        }
+        validateHexDigits("IPv6 segment", host, inProgress);
+    }
+
+    private static void validateHexDigits(String description, String host, String segment) {
+        for (char c : segment.toCharArray()) {
+            if (c > 255) {
+                throw new IllegalArgumentException(description + " non hexadecimal character: " + HtmlEncoder.encode(host));
+            }
+            if (!HEXDIGIT[c]) {
+                throw new IllegalArgumentException(description + " non hexadecimal character: " + HtmlEncoder.encode(host));
+            }
+        }
+    }
+
+    private static void validateIpOctet(String message, String host, String octet) {
+        int octetInt = Integer.parseInt(octet);
+        // cannot be negative, as the regexp will not match
+        if (octetInt > 255) {
+            throw new IllegalArgumentException(message + " " + HtmlEncoder.encode(host));
+        }
+    }
+
+    private static void validateIpFuture(String ipLiteral, String host) {
+        /*
+              IPvFuture     = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+              unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
+              sub-delims    = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+        */
+        int dot = host.indexOf('.');
+        if (dot == -1) {
+            throw new IllegalArgumentException("IP Future must contain 'v<version>.': " + HtmlEncoder.encode(ipLiteral));
+        }
+        // always starts with v
+        String version = host.substring(1, dot);
+        checkNotBlank("Version", ipLiteral, version);
+        validateHexDigits("Future version", ipLiteral, version);
+
+        String address = host.substring(dot + 1);
+        checkNotBlank("IP Future", ipLiteral, address);
+
+        for (char c : address.toCharArray()) {
+            if (c > 255) {
+                throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(ipLiteral));
+            }
+            if (UNRESERVED[c]) {
+                continue;
+            }
+            if (SUB_DELIMS[c]) {
+                continue;
+            }
+            if (c == ':') {
+                continue;
+            }
+            throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(ipLiteral));
+        }
+    }
+
+    private static void checkNotBlank(String message, String ipLiteral, String toValidate) {
+        if (toValidate.isBlank()) {
+            throw new IllegalArgumentException(message + " cannot be blank: " + HtmlEncoder.encode(ipLiteral));
+        }
+    }
+}
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
index ac51d1989f..fc037a2532 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ConfigBlueprint.java
@@ -72,6 +72,25 @@ interface Http1ConfigBlueprint extends ProtocolConfig {
     @Option.DefaultBoolean(true)
     boolean validateRequestHeaders();
 
+    /**
+     * Request host header validation.
+     * When host header is invalid, we return {@link io.helidon.http.Status#BAD_REQUEST_400}.
+     * <p>
+     * The validation is done according to RFC-3986 (see {@link io.helidon.http.HostValidator}). This is a requirement of
+     * the HTTP specification.
+     * <p>
+     * This option allows you to disable the "full-blown" validation ("simple" validation is still in - the port must be
+     * parseable to integer).
+     *
+     * @return whether to do a full validation of {@code Host} header according to the specification
+     * @deprecated this switch exists for temporary backward compatible behavior, and will be removed in a future Helidon
+     *              version
+     */
+    @Option.Configured
+    @Option.DefaultBoolean(true)
+    @Deprecated(forRemoval = true, since = "4.1.3")
+    boolean validateRequestHostHeader();
+
     /**
      * Whether to validate headers.
      * If set to false, any value is accepted, otherwise validates headers + known headers
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
index 6bc1bc0875..a676248940 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
@@ -20,6 +20,7 @@ import java.io.UncheckedIOException;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.ZonedDateTime;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Semaphore;
@@ -38,6 +39,8 @@ import io.helidon.http.DirectHandler;
 import io.helidon.http.DirectHandler.EventType;
 import io.helidon.http.HeaderNames;
 import io.helidon.http.HeaderValues;
+import io.helidon.http.HostValidator;
+import io.helidon.http.HtmlEncoder;
 import io.helidon.http.HttpPrologue;
 import io.helidon.http.InternalServerException;
 import io.helidon.http.RequestException;
@@ -65,15 +68,13 @@ import static java.lang.System.Logger.Level.WARNING;
  * HTTP/1.1 server connection.
  */
 public class Http1Connection implements ServerConnection, InterruptableTask<Void> {
+    static final byte[] CONTINUE_100 = "HTTP/1.1 100 Continue\r\n\r\n".getBytes(StandardCharsets.UTF_8);
     private static final System.Logger LOGGER = System.getLogger(Http1Connection.class.getName());
     private static final Supplier<RequestException> INVALID_SIZE_EXCEPTION_SUPPLIER =
             () -> RequestException.builder()
                     .type(EventType.BAD_REQUEST)
                     .message("Chunk size is invalid")
                     .build();
-
-    static final byte[] CONTINUE_100 = "HTTP/1.1 100 Continue\r\n\r\n".getBytes(StandardCharsets.UTF_8);
-
     private final ConnectionContext ctx;
     private final Http1Config http1Config;
     private final DataWriter writer;
@@ -103,7 +104,7 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
      * Create a new connection.
      *
      * @param ctx                connection context
-     * @param http1Config             connection provider configuration
+     * @param http1Config        connection provider configuration
      * @param upgradeProviderMap map of upgrade providers (protocol id to provider)
      */
     Http1Connection(ConnectionContext ctx,
@@ -134,6 +135,7 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
         return true;
     }
 
+    @SuppressWarnings("removal")
     @Override
     public void handle(Semaphore requestSemaphore) throws InterruptedException {
         this.myThread = Thread.currentThread();
@@ -153,6 +155,9 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
                 currentEntitySizeRead = 0;
 
                 WritableHeaders<?> headers = http1headers.readHeaders(prologue);
+                if (http1Config.validateRequestHeaders()) {
+                    validateHostHeader(prologue, headers, http1Config.validateRequestHostHeader());
+                }
                 ctx.remotePeer().tlsCertificates()
                         .flatMap(TlsUtils::parseCn)
                         .ifPresent(name -> headers.set(X_HELIDON_CN, name));
@@ -257,6 +262,133 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
         }
     }
 
+    void reset() {
+        currentEntitySize = 0;
+        currentEntitySizeRead = 0;
+    }
+
+    static void validateHostHeader(HttpPrologue prologue, WritableHeaders<?> headers, boolean fullValidation) {
+        if (fullValidation) {
+            try {
+                doValidateHostHeader(prologue, headers);
+            } catch (IllegalArgumentException e) {
+                throw RequestException.builder()
+                        .type(EventType.BAD_REQUEST)
+                        .status(Status.BAD_REQUEST_400)
+                        .request(DirectTransportRequest.create(prologue, headers))
+                        .setKeepAlive(false)
+                        .message("Invalid Host header: " + e.getMessage())
+                        .cause(e)
+                        .build();
+            }
+        } else {
+            simpleHostHeaderValidation(prologue, headers);
+        }
+    }
+
+    private static void simpleHostHeaderValidation(HttpPrologue prologue, WritableHeaders<?> headers) {
+        if (headers.contains(HeaderNames.HOST)) {
+            String host = headers.get(HeaderNames.HOST).get();
+            // this is what is used to set up URI information, and this MUST work
+            int index = host.lastIndexOf(':');
+            if (index < 1) {
+                return;
+            }
+            // this may still be an IPv6 address
+            if (host.charAt(host.length() - 1) == ']') {
+                // IP literal without port
+                return;
+            }
+
+            try {
+                // port must be parseable to int
+                Integer.parseInt(host.substring(index + 1));
+            } catch (NumberFormatException e) {
+                throw RequestException.builder()
+                        .type(EventType.BAD_REQUEST)
+                        .status(Status.BAD_REQUEST_400)
+                        .request(DirectTransportRequest.create(prologue, headers))
+                        .setKeepAlive(false)
+                        .message("Invalid port of the host header: " + HtmlEncoder.encode(host.substring(index + 1)))
+                        .build();
+            }
+
+        }
+
+    }
+
+    private static void doValidateHostHeader(HttpPrologue prologue, WritableHeaders<?> headers) {
+        List<String> hostHeaders = headers.all(HeaderNames.HOST, List::of);
+        if (hostHeaders.isEmpty()) {
+            throw RequestException.builder()
+                    .type(EventType.BAD_REQUEST)
+                    .status(Status.BAD_REQUEST_400)
+                    .request(DirectTransportRequest.create(prologue, headers))
+                    .setKeepAlive(false)
+                    .message("Host header must be present in the request")
+                    .build();
+        }
+        if (hostHeaders.size() > 1) {
+            throw RequestException.builder()
+                    .type(EventType.BAD_REQUEST)
+                    .status(Status.BAD_REQUEST_400)
+                    .request(DirectTransportRequest.create(prologue, headers))
+                    .setKeepAlive(false)
+                    .message("Only a single Host header is allowed in request")
+                    .build();
+        }
+        String host = hostHeaders.getFirst();
+        if (host.isEmpty()) {
+            throw RequestException.builder()
+                    .type(EventType.BAD_REQUEST)
+                    .status(Status.BAD_REQUEST_400)
+                    .request(DirectTransportRequest.create(prologue, headers))
+                    .setKeepAlive(false)
+                    .message("Host header must not be empty")
+                    .build();
+        }
+        // now host and port must be valid
+        int startLiteral = host.indexOf('[');
+        int endLiteral = host.lastIndexOf(']');
+        if (startLiteral == 0 && endLiteral == host.length() - 1) {
+            // this is most likely an IPv6 address without a port
+            HostValidator.validateIpLiteral(host);
+            return;
+        }
+        if (startLiteral == 0 && endLiteral == -1) {
+            HostValidator.validateIpLiteral(host);
+            return;
+        }
+        int colon = host.lastIndexOf(':');
+        if (colon == -1) {
+            // only host
+            HostValidator.validateNonIpLiteral(host);
+            return;
+        }
+
+        String portString = host.substring(colon + 1);
+        try {
+            Integer.parseInt(portString);
+        } catch (NumberFormatException e) {
+            throw RequestException.builder()
+                    .type(EventType.BAD_REQUEST)
+                    .status(Status.BAD_REQUEST_400)
+                    .request(DirectTransportRequest.create(prologue, headers))
+                    .setKeepAlive(false)
+                    .message("Invalid port of the host header: " + HtmlEncoder.encode(portString))
+                    .build();
+        }
+        String hostString = host.substring(0, colon);
+        // can be
+        // IP-literal [..::]
+        if (startLiteral == 0 && endLiteral == hostString.length() - 1) {
+            HostValidator.validateIpLiteral(hostString);
+            return;
+        }
+
+        HostValidator.validateNonIpLiteral(hostString);
+    }
+
     private BufferData readEntityFromPipeline(HttpPrologue prologue, WritableHeaders<?> headers) {
         if (currentEntitySize == -1) {
             // chunked
@@ -486,9 +618,4 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
             LOGGER.log(WARNING, "Internal server error", e);
         }
     }
-
-    void reset() {
-        currentEntitySize = 0;
-        currentEntitySizeRead = 0;
-    }
 }


Test patch:
diff --git a/http/http/src/test/java/io/helidon/http/HostValidatorTest.java b/http/http/src/test/java/io/helidon/http/HostValidatorTest.java
new file mode 100644
index 0000000000..f0144f9e78
--- /dev/null
+++ b/http/http/src/test/java/io/helidon/http/HostValidatorTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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.helidon.http;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.http.HostValidator.validate;
+import static io.helidon.http.HostValidator.validateIpLiteral;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class HostValidatorTest {
+    @Test
+    void testGoodHostname() {
+        // sanity
+        validate("localhost");
+        // host names
+        validate("www.example.com");
+        // percent encoded
+        validate("%65%78%61%6D%70%6C%65");
+        validate("%65%78%61%6D%70%6C%65.com");
+        // with underscores
+        validate("www.exa_mple.com");
+        // with sub-delims
+        validate("www.exa$mple.com");
+    }
+
+    @Test
+    void testGoodIp4() {
+        // IPv4
+        validate("192.167.1.1");
+    }
+
+    @Test
+    void testGoodIpLiteral6() {
+        // IPv6
+        validate("[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]");
+        validate("[::1]");
+        validate("[2001:db8:3333:4444:5555:6666:7777:8888]");
+        validate("[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]");
+        validate("[::]");
+        validate("[2001:db8::]");
+        validate("[::1234:5678]");
+        validate("[::1234:5678:1]");
+        validate("[2001:db8::1234:5678]");
+        validate("[2001:db8:1::ab9:C0A8:102]");
+    }
+
+    @Test
+    void testGoodIpLiteral6Dual() {
+        // IPv6
+        validate("[2001:db8:3333:4444:5555:6666:1.2.3.4]");
+        validate("[::11.22.33.44]");
+        validate("[2001:db8::123.123.123.123]");
+        validate("[::1234:5678:91.123.4.56]");
+        validate("[::1234:5678:1.2.3.4]");
+        validate("[2001:db8::1234:5678:5.6.7.8]");
+    }
+
+    @Test
+    void testGoodIpLiteralFuture() {
+        // IPvFuture
+        validate("[v9.abc:def]");
+        validate("[v9.abc:def*]");
+    }
+
+    @Test
+    void testBadHosts() {
+        // just empty
+        invokeExpectFailure("Host cannot be blank: ", "");
+        // invalid brackets
+        invokeExpectFailure("Host contains invalid character: [start.but.not.end",
+                            "[start.but.not.end");
+        invokeExpectFailure("Host contains invalid character: end.but.not.start]",
+                            "end.but.not.start]");
+        invokeExpectFailure("Host contains invalid character: int.the[.middle]",
+                            "int.the[.middle]");
+        // invalid escape
+        invokeExpectFailure("Host contains non-hexadecimal character in % encoding: www.%ZAxample.com",
+                            "www.%ZAxample.com");
+        invokeExpectFailure("Host contains non-hexadecimal character in % encoding: www.%AZxample.com",
+                            "www.%AZxample.com");
+        // invalid character (non-ASCII
+        invokeExpectFailure("Host contains invalid character: www.čexample.com",
+                            "www.čexample.com");
+        // wrong trailing escape (must be two chars);
+        invokeExpectFailure("Host contains invalid % encoding: www.example.com%4",
+                            "www.example.com%4");
+        invokeExpectFailure("Host contains invalid character in % encoding: www.example.com%č4",
+                            "www.example.com%č4");
+        invokeExpectFailure("Host contains invalid character in % encoding: www.example.com%4č",
+                            "www.example.com%4č");
+    }
+
+    @Test
+    void testBadLiteral6() {
+        // IPv6
+        // empty segment
+        invokeExpectFailure("Host IPv6 contains more than one skipped segment: [2001:db8::85a3::7334]",
+                            "[2001:db8::85a3::7334]");
+        // wrong segment (G is not a hexadecimal number)
+        invokeExpectFailure("IPv6 segment non hexadecimal character: "
+                                    + "[GGGG:FFFF:0000:0000:0000:0000:0000:0000]",
+                            "[GGGG:FFFF:0000:0000:0000:0000:0000:0000]");
+        // non-ASCII character
+        invokeExpectFailure("IPv6 segment non hexadecimal character: "
+                                    + "[č:FFFF:0000:0000:0000:0000:0000:0000]",
+                            "[č:FFFF:0000:0000:0000:0000:0000:0000]");
+        // wrong segment (too many characters)
+        invokeExpectFailure("IPv6 segment has more than 4 characters: [aaaaa:FFFF:0000:0000:0000:0000:0000:0000]",
+                            "[aaaaa:FFFF:0000:0000:0000:0000:0000:0000]");
+        // empty segment
+        invokeExpectFailure("IPv6 segment is empty: [aaaa:FFFF:0000:0000:0000:0000:0000:]",
+                            "[aaaa:FFFF:0000:0000:0000:0000:0000:]");
+        // wrong number of segments
+        invokeExpectFailure("Host IPv6 address contains too many segments: "
+                                    + "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]",
+                            "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]");
+        // missing everything
+        invokeExpectFailure("Host cannot be blank: []",
+                            "[]");
+        // wrong start (leading colon)
+        invokeExpectFailure("Host IPv6 contains excessive colon: [:1:0::]",
+                            "[:1:0::]");
+        // wrong end, colon instead of value
+        invokeExpectFailure("IPv6 segment non hexadecimal character: [1:0:::]",
+                            "[1:0:::]");
+
+        invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): [::",
+                                   "[::");
+        invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): ::]",
+                                   "::]");
+    }
+
+    @Test
+    void testBadLiteralDual() {
+        invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44.74]",
+                                   "[::14.266.44.74]");
+        invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44]",
+                                   "[::14.266.44]");
+        invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.123.-44.147]",
+                                   "[::14.123.-44.147]");
+    }
+
+    @Test
+    void testBadLiteralFuture() {
+        // IPv future
+        // version must be present
+        invokeExpectFailure("Version cannot be blank: [v.abc:def]",
+                            "[v.abc:def]");
+        // missing address
+        invokeExpectFailure("IP Future must contain 'v<version>.': [v2]",
+                            "[v2]");
+        invokeExpectFailure("IP Future cannot be blank: [v2.]",
+                            "[v2.]");
+        // invalid character in the host (valid future)
+        invokeExpectFailure("Host contains invalid character: [v2./0:::]",
+                            "[v2./0:::]");
+        invokeExpectFailure("Host contains invalid character: [v2.0:č]",
+                            "[v2.0:č]");
+    }
+
+    private static void invokeExpectFailure(String message, String host) {
+        var t = assertThrows(IllegalArgumentException.class, () -> validate(host), "Testing host: " + host);
+        assertThat(t.getMessage(), is(message));
+    }
+
+    private static void invokeLiteralExpectFailure(String message, String host) {
+        var t = assertThrows(IllegalArgumentException.class, () -> validateIpLiteral(host), "Testing host: " + host);
+        assertThat(t.getMessage(), is(message));
+    }
+}
\ No newline at end of file
diff --git a/microprofile/tests/server/pom.xml b/microprofile/tests/server/pom.xml
index b085c0f9e1..0be7a8e9a7 100644
--- a/microprofile/tests/server/pom.xml
+++ b/microprofile/tests/server/pom.xml
@@ -66,5 +66,10 @@
             <artifactId>helidon-logging-jul</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.helidon.webclient</groupId>
+            <artifactId>helidon-webclient-http1</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java b/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java
new file mode 100644
index 0000000000..1efb71706b
--- /dev/null
+++ b/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2021, 2024 Oracle and/or its affiliates.
+ *
+ * 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.helidon.microprofile.tests.server;
+
+import io.helidon.http.Header;
+import io.helidon.http.HeaderValues;
+import io.helidon.http.Status;
+import io.helidon.microprofile.testing.junit5.AddBean;
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+import io.helidon.webclient.api.WebClient;
+import io.helidon.webserver.http.ServerRequest;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@HelidonTest
+@AddBean(BadHostHeaderTest.TestResource.class)
+public class BadHostHeaderTest {
+    private static final Header BAD_HOST_HEADER = HeaderValues.create("Host", "localhost:808a");
+
+    @Test
+    void testGetGoodHeader(WebTarget target) {
+        String getResponse = target.path("/get").request().get(String.class);
+        assertThat(getResponse, is("localhost"));
+    }
+
+    @Test
+    void testGetBadHeader(WebTarget target) {
+        WebClient webClient = WebClient.builder()
+                .baseUri(target.getUri())
+                .build();
+        var response = webClient.get("/get")
+                .header(BAD_HOST_HEADER)
+                .request(String.class);
+        assertThat(response.status(), is(Status.BAD_REQUEST_400));
+        assertThat(response.entity(), is("Invalid port of the host header: 808a"));
+    }
+
+    @Path("/")
+    public static class TestResource {
+        @Context ServerRequest request;
+
+        @GET
+        @Path("get")
+        @Produces(MediaType.TEXT_PLAIN)
+        public String getIt() {
+            return request.requestedUri().host();
+        }
+    }
+}
diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java
new file mode 100644
index 0000000000..99fccc4df2
--- /dev/null
+++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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.helidon.webserver.tests;
+
+import io.helidon.http.Header;
+import io.helidon.http.HeaderValues;
+import io.helidon.http.Method;
+import io.helidon.http.Status;
+import io.helidon.webclient.api.ClientResponseTyped;
+import io.helidon.webclient.http1.Http1Client;
+import io.helidon.webserver.http.HttpRouting;
+import io.helidon.webserver.http1.Http1Route;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpRoute;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ServerTest
+class BadHostTest {
+    private static final Header BAD_HOST_HEADER = HeaderValues.create("Host", "localhost:808a");
+
+    private final Http1Client client;
+
+    BadHostTest(Http1Client client) {
+        this.client = client;
+    }
+
+    @SetUpRoute
+    static void routing(HttpRouting.Builder builder) {
+        builder.route(Http1Route.route(Method.GET,
+                                       "/",
+                                       (req, res) -> res.send(req.requestedUri().host())));
+    }
+
+    @Test
+    void testOk() {
+        String response = client.method(Method.GET)
+                .requestEntity(String.class);
+
+        assertThat(response, is("localhost"));
+    }
+
+    @Test
+    void testInvalidRequest() {
+        ClientResponseTyped<String> response = client.method(Method.GET)
+                .header(BAD_HOST_HEADER)
+                .request(String.class);
+
+        assertThat(response.status(), is(Status.BAD_REQUEST_400));
+        assertThat(response.entity(), is("Invalid port of the host header: 808a"));
+    }
+}
diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java
index 59237c6183..a18675ba4d 100644
--- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java
+++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java
@@ -43,6 +43,7 @@ import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+
 @ServerTest
 class BadRequestTest {
     public static final String CUSTOM_REASON_PHRASE = "Custom-bad-request";
@@ -71,13 +72,10 @@ class BadRequestTest {
                                        .build());
     }
 
-    // no need to try with resources when reading as string
-    @SuppressWarnings("resource")
     @Test
     void testOk() {
         String response = client.method(Method.GET)
-                .request()
-                .as(String.class);
+                .requestEntity(String.class);
 
         assertThat(response, is("Hi"));
     }
diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/http1/ValidateHostHeaderTest.java b/webserver/webserver/src/test/java/io/helidon/webserver/http1/ValidateHostHeaderTest.java
new file mode 100644
index 0000000000..d817e044ad
--- /dev/null
+++ b/webserver/webserver/src/test/java/io/helidon/webserver/http1/ValidateHostHeaderTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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.helidon.webserver.http1;
+
+import java.util.Arrays;
+
+import io.helidon.http.HeaderNames;
+import io.helidon.http.HttpPrologue;
+import io.helidon.http.Method;
+import io.helidon.http.RequestException;
+import io.helidon.http.WritableHeaders;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.webserver.http1.Http1Connection.validateHostHeader;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ValidateHostHeaderTest {
+    private static final HttpPrologue TEST_PROLOGUE =
+            HttpPrologue.create("http", "http", "1.1", Method.GET, "/", false);
+
+    @Test
+    void testNone() {
+        invokeExpectFailure("Host header must be present in the request");
+    }
+
+    @Test
+    void testMany() {
+        invokeExpectFailure("Only a single Host header is allowed in request", "first", "second");
+    }
+
+    @Test
+    void testGoodHostname() {
+        // sanity
+        invoke("localhost");
+        invoke("localhost:8080");
+        // host names
+        invoke("www.example.com:445");
+        // percent encoded
+        invoke("%65%78%61%6D%70%6C%65:8080");
+        invoke("%65%78%61%6D%70%6C%65.com:8080");
+        // with underscores
+        invoke("www.exa_mple.com:8080");
+    }
+
+    @Test
+    void testGoodIp4() {
+        // IPv4
+        invoke("192.167.1.1");
+        invoke("192.167.1.1:8080");
+    }
+
+    @Test
+    void testGoodIpLiteral6() {
+        // IPv6
+        invoke("[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]");
+        invoke("[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]:8080");
+        invoke("[::1]");
+        invoke("[::1]:8080");
+    }
+
+    @Test
+    void testGoodIpLiteral6Dual() {
+        // IPv6
+        invoke("[2001:db8:3333:4444:5555:6666:1.2.3.4]:8080");
+        invoke("[::11.22.33.44]");
+    }
+
+    @Test
+    void testGoodIpLiteralFuture() {
+        // IPvFuture
+        invoke("[v9.abc:def]");
+        invoke("[v9.abc:def]:8080");
+    }
+
+    @Test
+    void testBadPort() {
+        // unparsable port
+        invokeExpectFailure("Invalid port of the host header: 80a", "192.167.1.1:80a");
+        invokeExpectFailure("Invalid port of the host header: 80_80", "localhost:80_80");
+    }
+
+    @Test
+    void testBadPortSimpleValidation() {
+        // these must fail even when validation is disabled
+        WritableHeaders<?> headers = WritableHeaders.create();
+        headers.set(HeaderNames.HOST, "192.167.1.1:80a");
+        var t = assertThrows(RequestException.class, () -> validateHostHeader(TEST_PROLOGUE, headers, false));
+        assertThat(t.getMessage(), is("Invalid port of the host header: 80a"));
+
+        headers.set(HeaderNames.HOST, "192.167.1.1:80_80");
+        t = assertThrows(RequestException.class, () -> validateHostHeader(TEST_PROLOGUE, headers, false));
+        assertThat(t.getMessage(), is("Invalid port of the host header: 80_80"));
+    }
+
+
+    @Test
+    void testBadHosts() {
+        // just empty
+        invokeExpectFailure("Host header must not be empty", "");
+        invokeExpectFailure("Invalid Host header: Host contains invalid character: int.the[.middle]",
+                            "int.the[.middle]:8080");
+    }
+
+    @Test
+    void testBadLiteral6() {
+        // IPv6
+        // empty segment
+        invokeExpectFailure("Invalid Host header: Host IPv6 contains more than one skipped segment: [2001:db8::85a3::7334]",
+                            "[2001:db8::85a3::7334]");
+    }
+
+    @Test
+    void testBadLiteralFuture() {
+        // IPv future
+        // version must be present
+        invokeExpectFailure("Invalid Host header: Version cannot be blank: [v.abc:def]",
+                            "[v.abc:def]");
+        // missing address
+    }
+
+    private static void invokeExpectFailure(String message, String... hosts) {
+        var t = assertThrows(RequestException.class, () -> invoke(hosts), "Testing hosts: " + Arrays.toString(hosts));
+        assertThat(t.getMessage(), is(message));
+    }
+
+    private static void invoke(String... values) {
+        WritableHeaders<?> headers = WritableHeaders.create();
+        if (values.length > 0) {
+            headers.set(HeaderNames.HOST, values);
+        }
+        validateHostHeader(TEST_PROLOGUE, headers, true);
+    }
+}