diff --git a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpServer.java b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpServer.java index 326d8b22995..67ac9e98e10 100644 --- a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpServer.java +++ b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpServer.java @@ -75,7 +75,7 @@ import java.util.concurrent.Executor; * *

The following table shows some request URIs and which, if any context they would * match with: - * + *
description
* * * @@ -281,51 +281,17 @@ public abstract class HttpServer { * @apiNote * The path should generally, but is not required to, end with {@code /}. * If the path does not end with {@code /}, e.g., such as with {@code /foo}, - * then this would match requests with a path of {@code /foobar} or - * {@code /foo/bar}. + * then some implementations may use string prefix matching where + * this context path matches request paths {@code /foo}, + * {@code /foo/bar}, or {@code /foobar}. Others may use path prefix + * matching where {@code /foo} matches only request paths + * {@code /foo} and {@code /foo/bar}, but not {@code /foobar}. * * @implNote - * The JDK built-in implementation performs strict path prefix - * matching such that matching file names must have an exact match, not - * partial. Consider following examples: - * - *
description
Request URI
- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Context pathRequest path
/foo/foo//foo/bar/foobar
/YYYY
/fooYYYN
/foo/NYYN
+ * By default, the JDK built-in implementation uses path prefix matching. + * String prefix matching can be enabled using the + * {@link jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher} + * system property. * * @param path the root URI path to associate the context with * @param handler the handler to invoke for incoming requests @@ -333,6 +299,8 @@ public abstract class HttpServer { * already exists for this path * @throws NullPointerException if either path, or handler are {@code null} * @return an instance of {@code HttpContext} + * + * @see jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher */ public abstract HttpContext createContext(String path, HttpHandler handler); @@ -355,57 +323,25 @@ public abstract class HttpServer { * @apiNote * The path should generally, but is not required to, end with {@code /}. * If the path does not end with {@code /}, e.g., such as with {@code /foo}, - * then this would match requests with a path of {@code /foobar} or - * {@code /foo/bar}. + * then some implementations may use string prefix matching where + * this context path matches request paths {@code /foo}, + * {@code /foo/bar}, or {@code /foobar}. Others may use path prefix + * matching where {@code /foo} matches request paths + * {@code /foo} and {@code /foo/bar}, but not {@code /foobar}. * * @implNote - * The JDK built-in implementation performs strict path prefix - * matching such that matching file names must have an exact match, not - * partial. Consider following examples: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Context pathRequest path
/foo/foo//foo/bar/foobar
/YYYY
/fooYYYN
/foo/NYYN
+ * By default, the JDK built-in implementation uses path prefix matching. + * String prefix matching can be enabled using the + * {@link jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher} + * system property. * * @param path the root URI path to associate the context with * @throws IllegalArgumentException if path is invalid, or if a context * already exists for this path * @throws NullPointerException if path is {@code null} * @return an instance of {@code HttpContext} + * + * @see jdk.httpserver/##sun.net.httpserver.pathMatcher sun.net.httpserver.pathMatcher */ public abstract HttpContext createContext(String path); diff --git a/src/jdk.httpserver/share/classes/module-info.java b/src/jdk.httpserver/share/classes/module-info.java index ac147582b14..8f04cc4c7ce 100644 --- a/src/jdk.httpserver/share/classes/module-info.java +++ b/src/jdk.httpserver/share/classes/module-info.java @@ -101,7 +101,32 @@ import com.sun.net.httpserver.*; *

  • {@systemProperty sun.net.httpserver.nodelay} (default: false)
    * Boolean value, which if true, sets the {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY} * socket option on all incoming connections. - *

  • + * + *
  • + *

    {@systemProperty sun.net.httpserver.pathMatcher} (default: + * {@code pathPrefix})
    + * + * The path matching scheme used to route requests to context handlers. + * Following list of values are allowed by this property.

    + * + *
    + *
    + *
    {@code pathPrefix} (default)
    + *
    The request path must begin with the context path and all matching path + * segments must be identical. For instance, the context path {@code /foo} + * would match request paths {@code /foo}, {@code /foo/}, and {@code /foo/bar}, + * but not {@code /foobar}.
    + *
    {@code stringPrefix}
    + *
    Request path string must begin with the context path string. For + * instance, the context path {@code /foo} would match request paths + * {@code /foo}, {@code /foo/}, {@code /foo/bar}, and {@code /foobar}. + *
    + *
    + *
    + * + *

    In case of a blank or invalid value, the default will be used.

    + *
  • + * * * @apiNote The API and SPI in this module are designed and implemented to support a minimal * HTTP server and simple HTTP semantics primarily. diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ContextList.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ContextList.java index a21ad80ca08..935416c81b7 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ContextList.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ContextList.java @@ -30,6 +30,8 @@ import java.util.function.BiPredicate; class ContextList { + private static final System.Logger LOGGER = System.getLogger(ContextList.class.getName()); + private final LinkedList list = new LinkedList<>(); public synchronized void add(HttpContextImpl ctx) { @@ -59,7 +61,8 @@ class ContextList { * @param path the request path */ HttpContextImpl findContext(String protocol, String path) { - return findContext(protocol, path, ContextPathMatcher.PREFIX); + var matcher = ContextPathMatcher.ofConfiguredPrefixPathMatcher(); + return findContext(protocol, path, matcher); } private synchronized HttpContextImpl findContext(String protocol, String path, ContextPathMatcher matcher) { @@ -90,11 +93,60 @@ class ContextList { EXACT(String::equals), /** - * Tests path prefix matches such that file names must have an exact - * match. Consider the following examples: + * Tests string prefix matches where the request path string + * starts with the context path string. + * + *

    Examples

    + * * * - * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Context pathRequest path
    /foo/foo//foo/bar/foobar
    /YYYY
    /fooYYYY
    /foo/NYYN
    + */ + STRING_PREFIX((contextPath, requestPath) -> requestPath.startsWith(contextPath)), + + /** + * Tests path prefix matches where path segments must have an + * exact match. + * + *

    Examples

    + * + * + * + * * * * @@ -130,7 +182,7 @@ class ContextList { * *
    Context pathRequest path
    */ - PREFIX((contextPath, requestPath) -> { + PATH_PREFIX((contextPath, requestPath) -> { // Fast-path for `/` if ("/".equals(contextPath)) { @@ -169,6 +221,23 @@ class ContextList { return predicate.test(contextPath, requestPath); } + private static ContextPathMatcher ofConfiguredPrefixPathMatcher() { + var propertyName = "sun.net.httpserver.pathMatcher"; + var propertyValueDefault = "pathPrefix"; + var propertyValue = System.getProperty(propertyName, propertyValueDefault); + return switch (propertyValue) { + case "pathPrefix" -> ContextPathMatcher.PATH_PREFIX; + case "stringPrefix" -> ContextPathMatcher.STRING_PREFIX; + default -> { + LOGGER.log( + System.Logger.Level.WARNING, + "System property \"{}\" contains an invalid value: \"{}\". Falling back to the default: \"{}\"", + propertyName, propertyValue, propertyValueDefault); + yield ContextPathMatcher.PATH_PREFIX; + } + }; + } + } public synchronized void remove(String protocol, String path) diff --git a/test/jdk/com/sun/net/httpserver/ContextPathMappingTest.java b/test/jdk/com/sun/net/httpserver/ContextPathMatcherPathPrefixTest.java similarity index 80% rename from test/jdk/com/sun/net/httpserver/ContextPathMappingTest.java rename to test/jdk/com/sun/net/httpserver/ContextPathMatcherPathPrefixTest.java index 365b6b258b7..20b4f1cd8a3 100644 --- a/test/jdk/com/sun/net/httpserver/ContextPathMappingTest.java +++ b/test/jdk/com/sun/net/httpserver/ContextPathMatcherPathPrefixTest.java @@ -41,14 +41,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; /* - * @test + * @test id=default * @bug 8272758 - * @summary Verifies that context paths are correctly mapped to request paths + * @summary Verifies path prefix matching using defaults * @build EchoHandler * @run junit ${test.main.class} */ -class ContextPathMappingTest { +/* + * @test id=withProperty + * @bug 8272758 + * @summary Verifies path prefix matching by providing a system property + * @build EchoHandler + * @run junit/othervm + * -Dsun.net.httpserver.pathMatcher=pathPrefix + * ${test.main.class} + */ + +/* + * @test id=withInvalidProperty + * @bug 8272758 + * @summary Verifies path prefix matching by providing a system property + * containing an invalid value, and observing it fall back to the + * default + * @build EchoHandler + * @run junit/othervm + * -Dsun.net.httpserver.pathMatcher=noSuchMatcher + * ${test.main.class} + */ + +public class ContextPathMatcherPathPrefixTest { private static final HttpClient CLIENT = HttpClient.newBuilder().proxy(NO_PROXY).build(); @@ -81,7 +103,7 @@ class ContextPathMappingTest { } } - private static final class Infra implements AutoCloseable { + public static final class Infra implements AutoCloseable { private static final InetSocketAddress LO_SA_0 = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); @@ -92,14 +114,14 @@ class ContextPathMappingTest { private final String contextPath; - private Infra(String contextPath) throws IOException { + public Infra(String contextPath) throws IOException { this.server = HttpServer.create(LO_SA_0, 10); server.createContext(contextPath, HANDLER); server.start(); this.contextPath = contextPath; } - private void expect(int statusCode, String... requestPaths) throws Exception { + public void expect(int statusCode, String... requestPaths) throws Exception { for (String requestPath : requestPaths) { var requestURI = URI.create("http://%s:%s%s".formatted( server.getAddress().getHostString(), diff --git a/test/jdk/com/sun/net/httpserver/ContextPathMatcherStringPrefixTest.java b/test/jdk/com/sun/net/httpserver/ContextPathMatcherStringPrefixTest.java new file mode 100644 index 00000000000..685b8f02be0 --- /dev/null +++ b/test/jdk/com/sun/net/httpserver/ContextPathMatcherStringPrefixTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpClient; + +import static java.net.http.HttpClient.Builder.NO_PROXY; + +/* + * @test + * @bug 8272758 + * @summary Verifies string prefix matching configured using a system property + * @build ContextPathMatcherPathPrefixTest + * EchoHandler + * @run junit/othervm + * -Dsun.net.httpserver.pathMatcher=stringPrefix + * ${test.main.class} + */ + +class ContextPathMatcherStringPrefixTest { + + private static final HttpClient CLIENT = + HttpClient.newBuilder().proxy(NO_PROXY).build(); + + @AfterAll + static void stopClient() { + CLIENT.shutdownNow(); + } + + @Test + void testContextPathAtRoot() throws Exception { + try (var infra = new ContextPathMatcherPathPrefixTest.Infra("/")) { + infra.expect(200, "/foo", "/foo/", "/foo/bar", "/foobar"); + } + } + + @Test + void testContextPathAtSubDir() throws Exception { + try (var infra = new ContextPathMatcherPathPrefixTest.Infra("/foo")) { + infra.expect(200, "/foo", "/foo/", "/foo/bar", "/foobar"); + } + } + + @Test + void testContextPathAtSubDirWithTrailingSlash() throws Exception { + try (var infra = new ContextPathMatcherPathPrefixTest.Infra("/foo/")) { + infra.expect(200, "/foo/", "/foo/bar"); + infra.expect(404, "/foo", "/foobar"); + } + } + +}