You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2026/04/02 21:00:43 UTC

(solr) branch main updated: SOLR-18168: Remove PathExclusionFilter; serve static content via ResourceServlet (#4243)

This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 00e0870c4d5 SOLR-18168: Remove PathExclusionFilter; serve static content via ResourceServlet (#4243)
00e0870c4d5 is described below

commit 00e0870c4d50a6020f1a9039722b376741e569b8
Author: David Smiley <ds...@apache.org>
AuthorDate: Thu Apr 2 17:00:35 2026 -0400

    SOLR-18168: Remove PathExclusionFilter; serve static content via ResourceServlet (#4243)
    
    Instead of using a bespoke PathExclusionFilter to short-circuit Solr's filter chain
    for Admin UI static resources (/css/*, /js/*, /img/*, /partials/*, etc.), web.xml now
    maps those paths directly to Jetty's built-in ResourceServlet. Solr's filters are
    remapped from url-pattern="/*" to servlet-name="SolrServlet", so they naturally apply
    only to Solr requests and bypass static resources without bespoke code.
    
    BATS integration tests were added to replace ones that had used JettySolrRunner previously.
    
    Co-authored-by: copilot-swe-agent[bot] <19...@users.noreply.github.com>
    Co-authored-by: dsmiley <37...@users.noreply.github.com>
---
 AGENTS.md                                          |  24 +++--
 changelog/unreleased/SOLR-18041.yml                |   8 +-
 .../apache/solr/servlet/PathExclusionFilter.java   |  80 ----------------
 .../solr/servlet/LoadAdminUiServletTest.java       | 106 ---------------------
 solr/packaging/build.gradle                        |   5 +-
 solr/packaging/test/test_adminconsole_urls.bats    |  28 +++++-
 solr/solrj/build.gradle                            |   1 -
 .../client/solrj/embedded/JettyWebappTest.java     | 102 --------------------
 .../org/apache/solr/embedded/JettySolrRunner.java  |  12 ---
 solr/webapp/web/WEB-INF/web.xml                    |  86 ++++++++++-------
 10 files changed, 105 insertions(+), 347 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 4395a2a4ee5..d53f75fe777 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,7 +29,7 @@ While README.md and CONTRIBUTING.md are mainly written for humans, this file is
 
 ## Build and Development Workflow
 
-- When done or preparing to commit changes to java source files, be sure to run `gradlew tidy` to format the code
+- When done or preparing to commit changes to java source files, be sure to run `gradlew tidy` to format the code.  Don't bother beforehand.
 - Always run `gradlew check -x test` before declaring a feature done
 
 ## Code Quality and Best Practices
@@ -38,15 +38,25 @@ While README.md and CONTRIBUTING.md are mainly written for humans, this file is
 - Be careful to not add non-essential logging! If you add slf4j log calls, make sure to wrap debug/trace level calls in `logger.isXxxEnabled()` clause
 - Validate user input. For file paths, always call `myCoreContainer.assertPathAllowed(myPath)` before using
 
-## Testing
+## Running Tests
+
+- See `dev-docs/gradle-help/tests.txt` for hints on running tests
+- To run a specific test: `gradlew :solr:core:test --tests "org.apache.solr.search.TestCaffeineCache"`
+- To run a specific BATS test: `gradlew iTest --tests test_adminconsole_urls.bats`
+- The randomization seed is important.  To repeat a failing tests, pass the same seed given in the failure by adding to Gradle: `-Ptests.seed=HEXADECIMALHERE`.
+- Test output goes to `solr/<module>/build/test-results/test/outputs/OUTPUT-<fully.qualified.TestName>.txt` (stdout/stderr log) and `solr/<module>/build/test-results/test/TEST-<fully.qualified.TestName>.xml` (JUnit XML with pass/fail/error details)
+- To scan test output for a specific issue across already-run tests: `grep -rl "pattern" solr/*/build/test-results/test/outputs/`
+
+## Writing Tests
 
 - When adding a test to an existing suite/file, keep the same style / design choices
 - When adding a *new* Java test suite/file:
-  - Subclass SolrTestCase, or if SolrCloud is needed then SolrCloudTestCase
-  - If SolrTestCase and need to embed Solr, use either EmbeddedSolrServerTestRule (doesn't use HTTP) or SolrJettyTestRule if HTTP/Jetty is relevant to what is being tested.
-  - Avoid SolrTestCaseJ4 for new tests
-
-- See `dev-docs/gradle-help/tests.txt` for hints on running tests
+    - Subclass SolrTestCase, or if SolrCloud is needed then SolrCloudTestCase
+    - If SolrTestCase and need to embed Solr, use either EmbeddedSolrServerTestRule (doesn't use HTTP) or SolrJettyTestRule if HTTP/Jetty is relevant to what is being tested.
+    - Avoid SolrTestCaseJ4 for new tests
+- For BATS shell integration tests in `solr/packaging/test/`:
+    - Always use `run <command>` followed by `assert_output --partial "..."` or `refute_output --partial "..."` instead of capturing output into local variables and using `[[ ]]` comparisons
+    - Avoid patterns like `local var=$(cmd | grep ...); [[ "$var" == *"..."* ]]` — use `run cmd` + `assert_output`/`refute_output` instead
 
 ## Documentation
 
diff --git a/changelog/unreleased/SOLR-18041.yml b/changelog/unreleased/SOLR-18041.yml
index d36eaad3ee5..269ce9ad666 100644
--- a/changelog/unreleased/SOLR-18041.yml
+++ b/changelog/unreleased/SOLR-18041.yml
@@ -1,8 +1,10 @@
-# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
-title: SOLR 18041 - Path exclusions for the admin UI are now defined in a separate servlet filter
-type: other # added, changed, fixed, deprecated, removed, dependency_update, security, other
+title: Path exclusions for the admin UI no longer need bespoke Solr code.
+type: other
 authors:
   - name: Gus Heck
+  - name: David Smiley
 links:
   - name: SOLR-18041
     url: https://issues.apache.org/jira/browse/SOLR-18041
+  - name: SOLR-18168
+    url: https://issues.apache.org/jira/browse/SOLR-18168
diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java b/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java
deleted file mode 100644
index 87e1e195a21..00000000000
--- a/solr/core/src/java/org/apache/solr/servlet/PathExclusionFilter.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.solr.servlet;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.FilterConfig;
-import jakarta.servlet.RequestDispatcher;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpFilter;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Filter to identify paths that should be processed by Jetty's DefaultServlet. Typically, these
- * paths contain static resources that need to be returned verbatim, with appropriate content type,
- * which Jetty will determine via
- *
- * <p>{@code org.eclipse.jetty.http.MimeTypes#getMimeByExtension(java.lang.String)}
- */
-public class PathExclusionFilter extends HttpFilter {
-
-  private List<Pattern> excludePatterns;
-
-  boolean shouldBeExcluded(HttpServletRequest request) {
-    String requestPath = ServletUtils.getPathAfterContext(request);
-    if (excludePatterns != null) {
-      return excludePatterns.stream().map(p -> p.matcher(requestPath)).anyMatch(Matcher::lookingAt);
-    }
-    return false;
-  }
-
-  @Override
-  public void init(FilterConfig config) throws ServletException {
-    String patternConfig = config.getInitParameter("excludePatterns");
-    if (patternConfig != null) {
-      String[] excludeArray = patternConfig.split(",");
-      this.excludePatterns = Arrays.stream(excludeArray).map(Pattern::compile).toList();
-    }
-    super.init(config);
-  }
-
-  @Override
-  protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
-      throws IOException, ServletException {
-    if (shouldBeExcluded(req)) {
-      // N.B. "default" is the name for org.eclipse.jetty.ee10.servlet.DefaultServlet
-      // configured in solr/server/etc/webdefault.xml if it doesn't exist something is
-      // very wrong.
-      RequestDispatcher defaultServlet = req.getServletContext().getNamedDispatcher("default");
-      if (defaultServlet == null) {
-        res.sendError(
-            500,
-            "Server Misconfiguration: cannot find default servlet (normally defined as org.eclipse.jetty.ee10.servlet.DefaultServlet in webdefault.xml)");
-      } else {
-        defaultServlet.forward(req, res);
-      }
-    } else {
-      chain.doFilter(req, res);
-    }
-  }
-}
diff --git a/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java b/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java
deleted file mode 100644
index dccd954e1be..00000000000
--- a/solr/core/src/test/org/apache/solr/servlet/LoadAdminUiServletTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.solr.servlet;
-
-import static org.apache.solr.servlet.LoadAdminUiServlet.SYSPROP_CSP_CONNECT_SRC_URLS;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import jakarta.servlet.ServletConfig;
-import jakarta.servlet.ServletContext;
-import jakarta.servlet.ServletOutputStream;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.Set;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.core.CoreContainer;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-public class LoadAdminUiServletTest extends SolrTestCaseJ4 {
-
-  @InjectMocks private LoadAdminUiServlet servlet;
-  @Mock private HttpServletRequest mockRequest;
-  @Mock private HttpServletResponse mockResponse;
-  @Mock private CoreContainer coreContainer;
-  @Mock private ServletConfig servletConfig;
-  @Mock private ServletContext mockServletContext;
-  @Mock private ServletOutputStream mockOutputStream;
-  @Mock private CoreContainerProvider coreContainerProvider;
-
-  private static final Set<String> CSP_URLS =
-      Set.of(
-          "http://example1.com/token",
-          "https://example2.com/path/uri1",
-          "http://example3.com/oauth2/uri2");
-
-  @Override
-  @Before
-  public void setUp() throws Exception {
-    super.setUp();
-    MockitoAnnotations.openMocks(this);
-    when(mockRequest.getRequestURI()).thenReturn("/path/URI");
-    when(mockRequest.getContextPath()).thenReturn("/path");
-    when(mockRequest.getAttribute("org.apache.solr.CoreContainer")).thenReturn(coreContainer);
-    when(servletConfig.getServletContext()).thenReturn(mockServletContext);
-    when(mockResponse.getOutputStream()).thenReturn(mockOutputStream);
-    InputStream mockInputStream =
-        new ByteArrayInputStream("mock content".getBytes(StandardCharsets.UTF_8));
-    when(mockServletContext.getResourceAsStream(anyString())).thenReturn(mockInputStream);
-    when(mockServletContext.getAttribute(CoreContainerProvider.class.getName()))
-        .thenReturn(coreContainerProvider);
-    when(coreContainerProvider.getCoreContainer()).thenReturn(coreContainer);
-  }
-
-  @BeforeClass
-  public static void ensureWorkingMockito() {
-    assumeWorkingMockito();
-  }
-
-  @Test
-  public void testDefaultCSPHeaderSet() throws IOException {
-    System.setProperty(SYSPROP_CSP_CONNECT_SRC_URLS, String.join(",", CSP_URLS));
-    ArgumentCaptor<String> headerNameCaptor = ArgumentCaptor.forClass(String.class);
-    ArgumentCaptor<String> headerValueCaptor = ArgumentCaptor.forClass(String.class);
-    servlet.doGet(mockRequest, mockResponse);
-
-    verify(mockResponse).setHeader(headerNameCaptor.capture(), headerValueCaptor.capture());
-    assertEquals("Content-Security-Policy", headerNameCaptor.getValue());
-    String cspValue = headerValueCaptor.getValue();
-    for (String endpoint : CSP_URLS) {
-      assertTrue("Expected CSP value to contain " + endpoint, cspValue.contains(endpoint));
-    }
-  }
-
-  @Override
-  @After
-  public void tearDown() throws Exception {
-    super.tearDown();
-  }
-}
diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle
index 31d4904ba91..7320f2ebe2b 100644
--- a/solr/packaging/build.gradle
+++ b/solr/packaging/build.gradle
@@ -282,6 +282,7 @@ task integrationTests(type: BatsTask) {
   }
 
   inputs.dir(distDir)
+  inputs.dir('test')  // Track .bats test files and helper as inputs
   outputs.dir(integrationTestOutput)
 
   doFirst {
@@ -311,7 +312,9 @@ task integrationTests(type: BatsTask) {
 }
 
 class BatsTask extends Exec {
-  @InputDirectory
+  // Note: testDir is marked @Internal because it's a String configuration property, not a trackable input.
+  // The task using BatsTask should explicitly declare inputs.dir(testDir) in its configuration.
+  @Internal
   String testDir = 'test'
 
   @Input
diff --git a/solr/packaging/test/test_adminconsole_urls.bats b/solr/packaging/test/test_adminconsole_urls.bats
index ad0d2112c83..bc468a75470 100644
--- a/solr/packaging/test/test_adminconsole_urls.bats
+++ b/solr/packaging/test/test_adminconsole_urls.bats
@@ -28,11 +28,37 @@ teardown() {
   solr stop --all >/dev/null 2>&1
 }
 
+
 @test "assert able to launch solr admin console" {
   run solr start
 
   run curl -s -o /dev/null -w "%{http_code}" http://localhost:${SOLR_PORT}/solr/
+  assert_output "200"
+
+  run curl -s -I http://localhost:${SOLR_PORT}/solr/
+  assert_output --partial "text/html"
 
-  # Check if the HTTP response code is 200 (OK)
+  run curl -s http://localhost:${SOLR_PORT}/solr/
+  assert_output --partial "<html"
+
+  run curl -s -o /dev/null -w "%{http_code}" http://localhost:${SOLR_PORT}/solr/css/angular/chosen.css
   assert_output "200"
+
+  run curl -s -I http://localhost:${SOLR_PORT}/solr/css/angular/chosen.css
+  assert_output --partial "text/css"
+
+  run curl -s http://localhost:${SOLR_PORT}/solr/css/angular/chosen.css
+  assert_output --partial "{"
+}
+
+@test "assert CSP header contains custom connect src URLs" {
+  # Set custom CSP connect-src URLs via system property
+  local csp_urls="http://example1.com/token,https://example2.com/path/uri1,http://example3.com/oauth2/uri2"
+
+  run solr start -Dsolr.ui.headers.csp.connect-src.urls="${csp_urls}"
+
+  run curl -s -I http://localhost:${SOLR_PORT}/solr/
+  assert_output --partial "http://example1.com/token"
+  assert_output --partial "https://example2.com/path/uri1"
+  assert_output --partial "http://example3.com/oauth2/uri2"
 }
diff --git a/solr/solrj/build.gradle b/solr/solrj/build.gradle
index 098ad3e8c33..fbf02869a2b 100644
--- a/solr/solrj/build.gradle
+++ b/solr/solrj/build.gradle
@@ -75,7 +75,6 @@ dependencies {
   testRuntimeOnly(libs.eclipse.jetty.alpnjavaserver, {
     exclude group: "org.eclipse.jetty.alpn", module: "alpn-api"
   })
-  testImplementation libs.eclipse.jetty.session
 
   testImplementation(libs.mockito.core, {
     exclude group: "net.bytebuddy", module: "byte-buddy-agent"
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/JettyWebappTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/JettyWebappTest.java
deleted file mode 100644
index 25d21e01001..00000000000
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/embedded/JettyWebappTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 org.apache.solr.client.solrj.embedded;
-
-import java.io.InputStream;
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Locale;
-import java.util.Random;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.solrj.apache.HttpClientUtil;
-import org.apache.solr.util.ExternalPaths;
-import org.eclipse.jetty.ee10.webapp.WebAppContext;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.HttpConnectionFactory;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.session.DefaultSessionIdManager;
-
-/**
- * @since solr 1.3
- */
-public class JettyWebappTest extends SolrTestCaseJ4 {
-  int port = 0;
-
-  Server server;
-
-  @Override
-  public void setUp() throws Exception {
-    super.setUp();
-    System.setProperty("solr.solr.home", createTempDir().toString());
-    System.setProperty("tests.shardhandler.randomSeed", Long.toString(random().nextLong()));
-    System.setProperty("solr.tests.doContainerStreamCloseAssert", "false");
-
-    Path dataDir = createTempDir();
-    Files.createDirectories(dataDir);
-
-    System.setProperty("solr.data.dir", dataDir.toRealPath().toString());
-    String path = ExternalPaths.WEBAPP_HOME.toString();
-
-    server = new Server(port);
-    // insecure: only use for tests!!!!
-    server.addBean(new DefaultSessionIdManager(server, new Random(random().nextLong())));
-    server.setHandler(new WebAppContext(path, "/solr"));
-
-    ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory());
-    connector.setIdleTimeout(1000 * 60 * 60);
-    connector.setPort(0);
-    server.setConnectors(new Connector[] {connector});
-    server.setStopAtShutdown(true);
-
-    server.start();
-    port = connector.getLocalPort();
-  }
-
-  @Override
-  public void tearDown() throws Exception {
-    try {
-      server.stop();
-    } catch (Exception ex) {
-    }
-    super.tearDown();
-  }
-
-  public void testAdminUI() throws Exception {
-    // Not an extensive test, but it does connect to Solr and verify the Admin ui shows up.
-    String adminPath = "http://127.0.0.1:" + port + "/solr/";
-    try (InputStream is = URI.create(adminPath).toURL().openStream()) {
-      assertNotNull(is.readAllBytes()); // real error will be an exception
-    }
-
-    try (CloseableHttpClient client = HttpClients.createDefault()) {
-      HttpRequestBase m = new HttpGet(adminPath);
-      HttpResponse response = client.execute(m, HttpClientUtil.createNewHttpClientRequestContext());
-      assertEquals(200, response.getStatusLine().getStatusCode());
-      Header header = response.getFirstHeader("X-Frame-Options");
-      assertEquals("DENY", header.getValue().toUpperCase(Locale.ROOT));
-      m.releaseConnection();
-    }
-  }
-}
diff --git a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
index 5ecabf8abf9..1c6405675ee 100644
--- a/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
+++ b/solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java
@@ -65,7 +65,6 @@ import org.apache.solr.core.CoreContainer;
 import org.apache.solr.metrics.SolrMetricManager;
 import org.apache.solr.servlet.AuthenticationFilter;
 import org.apache.solr.servlet.CoreContainerProvider;
-import org.apache.solr.servlet.PathExclusionFilter;
 import org.apache.solr.servlet.RateLimitFilter;
 import org.apache.solr.servlet.RequiredSolrRequestFilter;
 import org.apache.solr.servlet.SolrServlet;
@@ -118,7 +117,6 @@ public class JettySolrRunner implements SolrBackend {
   private Server server;
 
   volatile FilterHolder debugFilter;
-  volatile FilterHolder pathExcludeFilter;
   volatile FilterHolder requiredFilter;
   volatile FilterHolder rateLimitFilter;
   volatile FilterHolder authFilter;
@@ -135,9 +133,6 @@ public class JettySolrRunner implements SolrBackend {
 
   private List<FilterHolder> extraFilters;
 
-  private static final String excludePatterns =
-      "/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+";
-
   private int proxyPort = -1;
 
   private final boolean enableProxy;
@@ -415,12 +410,6 @@ public class JettySolrRunner implements SolrBackend {
       // TODO: This needs to be driven by a parsing of web.xml eventually
       //  though we still want to avoid classpath scanning.
 
-      // this path excludes filter isn't actually necessary for any tests, but it's being
-      // added for parity with the live application.
-      pathExcludeFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED);
-      pathExcludeFilter.setHeldClass(PathExclusionFilter.class);
-      pathExcludeFilter.setInitParameter("excludePatterns", excludePatterns);
-
       // required request setup
       requiredFilter = root.getServletHandler().newFilterHolder(Source.EMBEDDED);
       requiredFilter.setHeldClass(RequiredSolrRequestFilter.class);
@@ -438,7 +427,6 @@ public class JettySolrRunner implements SolrBackend {
       authFilter.setHeldClass(AuthenticationFilter.class);
 
       // Map filters in same path as in web.xml
-      root.addFilter(pathExcludeFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
       root.addFilter(requiredFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
       root.addFilter(rateLimitFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
       root.addFilter(tracingFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml
index 201a88156c3..04f949e2f6b 100644
--- a/solr/webapp/web/WEB-INF/web.xml
+++ b/solr/webapp/web/WEB-INF/web.xml
@@ -16,89 +16,107 @@
  limitations under the License.
 -->
 <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
-         version="6.0"
-         metadata-complete="true">
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
+  version="6.0"
+  metadata-complete="true">
 
   <listener>
-   <listener-class>org.apache.solr.servlet.CoreContainerProvider</listener-class>
+    <listener-class>org.apache.solr.servlet.CoreContainerProvider</listener-class>
   </listener>
-  <!-- Any path (name) registered in solrconfig.xml will be sent to that filter -->
-  <filter>
-    <filter-name>PathExclusionsFilter</filter-name>
-    <filter-class>org.apache.solr.servlet.PathExclusionFilter</filter-class>
-    <!--
-    Exclude patterns is a list of directories that would be short-circuited by this
-    Filter. It includes all Admin UI related static content.
-    NOTE: It is NOT a pattern but only matches the start of the HTTP ServletPath.
-    -->
-    <init-param>
-      <param-name>excludePatterns</param-name>
-      <param-value>/partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+,/ui/.*</param-value>
-    </init-param>
-  </filter>
-
-  <filter-mapping>
-    <filter-name>PathExclusionsFilter</filter-name>
-    <url-pattern>/*</url-pattern>
-  </filter-mapping>
 
   <filter>
     <filter-name>SolrFilter</filter-name>
     <filter-class>org.apache.solr.servlet.RequiredSolrRequestFilter</filter-class>
   </filter>
-
   <filter-mapping>
     <filter-name>SolrFilter</filter-name>
-    <url-pattern>/*</url-pattern>
+    <servlet-name>SolrServlet</servlet-name>
   </filter-mapping>
 
   <filter>
     <filter-name>RateLimitFilter</filter-name>
     <filter-class>org.apache.solr.servlet.RateLimitFilter</filter-class>
   </filter>
-
   <filter-mapping>
     <filter-name>RateLimitFilter</filter-name>
-    <url-pattern>/*</url-pattern>
+    <servlet-name>SolrServlet</servlet-name>
   </filter-mapping>
 
   <filter>
     <filter-name>TracingFilter</filter-name>
     <filter-class>org.apache.solr.servlet.TracingFilter</filter-class>
   </filter>
-
   <filter-mapping>
     <filter-name>TracingFilter</filter-name>
-    <url-pattern>/*</url-pattern>
+    <servlet-name>SolrServlet</servlet-name>
   </filter-mapping>
 
   <filter>
     <filter-name>AuthenticationFilter</filter-name>
     <filter-class>org.apache.solr.servlet.AuthenticationFilter</filter-class>
   </filter>
-
   <filter-mapping>
     <filter-name>AuthenticationFilter</filter-name>
-    <url-pattern>/*</url-pattern>
+    <servlet-name>SolrServlet</servlet-name>
   </filter-mapping>
 
   <servlet>
     <servlet-name>SolrServlet</servlet-name>
     <servlet-class>org.apache.solr.servlet.SolrServlet</servlet-class>
   </servlet>
-
   <servlet-mapping>
     <servlet-name>SolrServlet</servlet-name>
     <url-pattern>/*</url-pattern>
   </servlet-mapping>
 
+  <!-- Define servlet for static Admin UI resources -->
+  <servlet>
+    <servlet-name>static</servlet-name>
+    <servlet-class>org.eclipse.jetty.ee10.servlet.ResourceServlet</servlet-class>
+    <init-param>
+      <param-name>pathInfoOnly</param-name>
+      <param-value>false</param-value>
+    </init-param>
+    <init-param>
+      <param-name>dirAllowed</param-name>
+      <param-value>false</param-value>
+    </init-param>
+  </servlet>
+  <!-- Map static Admin UI resources to the static servlet -->
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/partials/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/libs/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/css/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/js/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/img/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/templates/*</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>static</servlet-name>
+    <url-pattern>/ui/*</url-pattern>
+  </servlet-mapping>
+
   <servlet>
     <servlet-name>LoadAdminUI</servlet-name>
     <servlet-class>org.apache.solr.servlet.LoadAdminUiServlet</servlet-class>
   </servlet>
-
   <servlet-mapping>
     <servlet-name>LoadAdminUI</servlet-name>
     <url-pattern>/index.html</url-pattern>