Merge "Fix RequireSslFilter redirect when canonicalWebUrl is HTTPS"
diff --git a/java/com/google/gerrit/httpd/RequireSslFilter.java b/java/com/google/gerrit/httpd/RequireSslFilter.java
index ca3c3d8..a456cf9 100644
--- a/java/com/google/gerrit/httpd/RequireSslFilter.java
+++ b/java/com/google/gerrit/httpd/RequireSslFilter.java
@@ -80,7 +80,14 @@
if (isLocalHost(req)) {
url = getLocalHostUrl(req);
} else {
- url = urlProvider.get() + req.getServletPath();
+ // Build the redirect URL safely, avoiding duplicated slashes
+ String baseUrl = urlProvider.get();
+ String servletPath = req.getServletPath();
+
+ if (baseUrl.endsWith("/") && servletPath.startsWith("/")) {
+ servletPath = servletPath.substring(1); // remove one leading slash
+ }
+ url = baseUrl + servletPath;
}
rsp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
rsp.setHeader("Location", url);
diff --git a/javatests/com/google/gerrit/httpd/RequireSslFilterTest.java b/javatests/com/google/gerrit/httpd/RequireSslFilterTest.java
new file mode 100644
index 0000000..65060db
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/RequireSslFilterTest.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// 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 com.google.gerrit.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RequireSslFilterTest {
+ @Mock FilterChain filterChain;
+
+ @Test
+ public void shouldRedirectWithoutDoubleSlashWhenCanonicalUrlEndsWithSlash()
+ throws IOException, ServletException {
+ FakeHttpServletRequest request =
+ new FakeHttpServletRequest("gerrit.example.com", 80, "", "/a/changes/12345");
+ FakeHttpServletResponse response = new FakeHttpServletResponse();
+
+ RequireSslFilter filter = new RequireSslFilter(() -> "https://gerrit.example.com/");
+
+ filter.doFilter(request, response, filterChain);
+
+ assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_MOVED_PERMANENTLY);
+ assertThat(response.getHeader("Location"))
+ .isEqualTo("https://gerrit.example.com/a/changes/12345");
+ verify(filterChain, never()).doFilter(request, response);
+ }
+}