Add property to switch between prefix matching schemes

This commit is contained in:
Volkan Yazıcı 2026-01-21 13:17:27 +01:00
parent f44d4b35fa
commit e80aabcb7a
No known key found for this signature in database
GPG Key ID: D37D4387C9BD368E
5 changed files with 225 additions and 99 deletions

View File

@ -75,7 +75,7 @@ import java.util.concurrent.Executor;
*
* <p>The following table shows some request URIs and which, if any context they would
* match with:
* <table class="striped"><caption style="display:none">description</caption>
* <table class="striped" style="text-align:left"><caption style="display:none">description</caption>
* <thead>
* <tr>
* <th scope="col"><i>Request URI</i></th>
@ -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 <em>string prefix matching</em> where
* this context path matches request paths {@code /foo},
* {@code /foo/bar}, or {@code /foobar}. Others may use <em>path prefix
* matching</em> where {@code /foo} matches only request paths
* {@code /foo} and {@code /foo/bar}, but not {@code /foobar}.
*
* @implNote
* The JDK built-in implementation performs <em>strict</em> path prefix
* matching such that matching file names must have an exact match, not
* partial. Consider following examples:
*
* <table>
* <thead>
* <tr>
* <th rowspan="2">Context path</th>
* <th colspan="4">Request path</th>
* </tr>
* <tr>
* <th>/foo</th>
* <th>/foo/</th>
* <th>/foo/bar</th>
* <th>/foobar</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>/</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* </tr>
* <tr>
* <td>/foo</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>N</td>
* </tr>
* <tr>
* <td>/foo/</td>
* <td>N</td>
* <td>Y</td>
* <td>Y</td>
* <td>N</td>
* </tr>
* </tbody>
* </table>
* 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 <em>string prefix matching</em> where
* this context path matches request paths {@code /foo},
* {@code /foo/bar}, or {@code /foobar}. Others may use <em>path prefix
* matching</em> where {@code /foo} matches request paths
* {@code /foo} and {@code /foo/bar}, but not {@code /foobar}.
*
* @implNote
* The JDK built-in implementation performs <em>strict</em> path prefix
* matching such that matching file names must have an exact match, not
* partial. Consider following examples:
*
* <table>
* <thead>
* <tr>
* <th rowspan="2">Context path</th>
* <th colspan="4">Request path</th>
* </tr>
* <tr>
* <th>/foo</th>
* <th>/foo/</th>
* <th>/foo/bar</th>
* <th>/foobar</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>/</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* </tr>
* <tr>
* <td>/foo</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>N</td>
* </tr>
* <tr>
* <td>/foo/</td>
* <td>N</td>
* <td>Y</td>
* <td>Y</td>
* <td>N</td>
* </tr>
* </tbody>
* </table>
* 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);

View File

@ -101,7 +101,32 @@ import com.sun.net.httpserver.*;
* <li><p><b>{@systemProperty sun.net.httpserver.nodelay}</b> (default: false)<br>
* Boolean value, which if true, sets the {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY}
* socket option on all incoming connections.
* </li></ul>
* </li>
* <li>
* <p><b>{@systemProperty sun.net.httpserver.pathMatcher}</b> (default:
* {@code pathPrefix})<br/>
*
* The path matching scheme used to route requests to context handlers.
* Following list of values are allowed by this property.</p>
*
* <blockquote>
* <dl>
* <dt>{@code pathPrefix} (default)</dt>
* <dd>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}.</dd>
* <dt>{@code stringPrefix}</dt>
* <dd>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}.
* </dd>
* </dl>
* </blockquote>
*
* <p>In case of a blank or invalid value, the default will be used.</p>
* </li>
* </ul>
*
* @apiNote The API and SPI in this module are designed and implemented to support a minimal
* HTTP server and simple HTTP semantics primarily.

View File

@ -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<HttpContextImpl> 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 <em>string prefix matches</em> where the request path string
* starts with the context path string.
*
* <h3>Examples</h3>
*
* <table>
* <thead>
* <tr>
* <tr>
* <th rowspan="2">Context path</th>
* <th colspan="4">Request path</th>
* </tr>
* <tr>
* <th>/foo</th>
* <th>/foo/</th>
* <th>/foo/bar</th>
* <th>/foobar</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>/</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* </tr>
* <tr>
* <td>/foo</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* <td>Y</td>
* </tr>
* <tr>
* <td>/foo/</td>
* <td>N</td>
* <td>Y</td>
* <td>Y</td>
* <td>N</td>
* </tr>
* </tbody>
* </table>
*/
STRING_PREFIX((contextPath, requestPath) -> requestPath.startsWith(contextPath)),
/**
* Tests <em>path prefix matches</em> where path segments must have an
* exact match.
*
* <h3>Examples</h3>
*
* <table>
* <thead>
* <tr>
* <th rowspan="2">Context path</th>
* <th colspan="4">Request path</th>
* </tr>
@ -130,7 +182,7 @@ class ContextList {
* </tbody>
* </table>
*/
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)

View File

@ -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(),

View File

@ -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");
}
}
}