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

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit a483b5cdfdf544587583ba77855a9b91bcdbc420
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Aug 4 14:26:34 2026 -1000

    Publish a relative OpenAPI server URL instead of following the Host
    
    Closes F3 by default rather than leaving it to configuration. The earlier
    position -- that a relative servers[].url risked breaking MCP clients that 
need
    an absolute one -- does not survive contact with the actual consumers.
    
    Axis2's own MCP catalog never emits servers[] or any absolute base URL;
    generateMcpCatalogJson publishes relative paths ("POST /services/...") and a
    relative tokenEndpoint, so /openapi-mcp.json was never affected by this at 
all.
    The claim in the report that MCP tool clients use servers[].url does not 
hold
    for this implementation.
    
    Nor do the JSON and OpenAPI clients this surface was designed against. Those
    surveyed carry no MCP code, do not fetch the specification at runtime -- one
    bundles a snapshot of it -- and resolve their base URL from configuration.
    None of them reads servers[].url.
    
    So servers[].url is now the context path. OpenAPI 3 resolves that against
    wherever the document was fetched from, which is both safer -- the URL a
    consumer sends credentials to is no longer chosen by whoever set the Host 
-- and
    more accurate behind a proxy. openapi.serverBaseUrl still pins an absolute 
URL
    where one is wanted.
    
    The HTTP/2 tests asserted the spec advertised an https:// server; a 
relative URL
    inherits the document's scheme, so that premise is obsolete and they now 
assert
    relativity instead.
    
    Co-Authored-By: Claude Fable 5 <[email protected]>
---
 .../apache/axis2/openapi/OpenApiSpecGenerator.java | 62 ++++++----------------
 .../axis2/openapi/Http2OpenApiBasicTest.java       | 20 ++++---
 .../axis2/openapi/Http2OpenApiIntegrationTest.java | 20 ++++---
 .../axis2/openapi/OpenApiSpecGeneratorTest.java    | 46 +++++++++-------
 4 files changed, 69 insertions(+), 79 deletions(-)

diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
index 03b5d2a785..ad732e8b3c 100644
--- 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
@@ -242,53 +242,25 @@ public class OpenApiSpecGenerator {
             return servers;
         }
 
-        if (request != null) {
-            // Build server URL from request
-            String scheme = request.getScheme();
-            String serverName = request.getServerName();
-            int serverPort = request.getServerPort();
-            String contextPath = request.getContextPath();
-            if (contextPath == null) {
-                contextPath = "";
-            }
-
-            if (!RequestUrlPolicy.isSafeHost(serverName)) {
-                // The Host is client-supplied. Publishing a malformed one 
would
-                // point every client that reads this specification at it, so 
fall
-                // back to a relative server URL, which OpenAPI 3 resolves 
against
-                // the location the document was retrieved from.
-                log.warn("Rejecting malformed Host header for the OpenAPI 
server URL; "
-                        + "publishing a relative server URL instead");
-                Server server = new Server();
-                server.setUrl(contextPath.isEmpty() ? "/" : contextPath);
-                server.setDescription("Current server");
-                servers.add(server);
-                return servers;
-            }
-
-            StringBuilder serverUrl = new StringBuilder();
-            serverUrl.append(scheme).append("://").append(serverName);
-
-            // Add port if not default
-            if ((scheme.equals("http") && serverPort != 80) ||
-                (scheme.equals("https") && serverPort != 443)) {
-                serverUrl.append(":").append(serverPort);
-            }
-
-            serverUrl.append(contextPath);
-
-            Server server = new Server();
-            server.setUrl(serverUrl.toString());
-            server.setDescription("Current server");
-            servers.add(server);
-        } else {
-            // Default server
-            Server server = new Server();
-            server.setUrl("http://localhost:8080";);
-            server.setDescription("Default server");
-            servers.add(server);
+        // Otherwise publish a relative server URL. OpenAPI 3 resolves it 
against
+        // the location the document was retrieved from, which is already the
+        // right origin for any client that just fetched this specification.
+        //
+        // Deriving it from the request Host instead would mean the URL every
+        // consumer sends its next request to — carrying whatever credentials 
it
+        // holds — is chosen by whoever set that header. A relative URL cannot 
be
+        // pointed anywhere, and it is also simply more accurate behind a 
proxy,
+        // where the Host the container sees is often not the public origin.
+        String contextPath = request == null ? null : request.getContextPath();
+        if (contextPath == null || contextPath.isEmpty()) {
+            contextPath = "/";
         }
 
+        Server server = new Server();
+        server.setUrl(contextPath);
+        server.setDescription("Current server");
+        servers.add(server);
+
         return servers;
     }
 
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java
index 0b56aa776d..3bcb818d25 100644
--- 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java
@@ -96,14 +96,15 @@ public class Http2OpenApiBasicTest extends TestCase {
         assertNotNull("Should have servers", servers);
         assertFalse("Should have at least one server", servers.isEmpty());
 
-        boolean hasHttpsServer = servers.stream()
-                .anyMatch(server -> server.getUrl().startsWith("https://";));
-        assertTrue("Should have HTTPS server for HTTP/2 compatibility", 
hasHttpsServer);
+        // The server URL is relative, so a client resolves it against the
+        // origin it fetched the document from. Over HTTPS that yields HTTPS
+        // requests without the spec having to name a scheme at all.
+        boolean allRelative = servers.stream()
+                .allMatch(server -> server.getUrl().startsWith("/"));
+        assertTrue("Server URL should be scheme-relative for HTTP/2 
compatibility", allRelative);
 
         System.out.println("✅ HTTPS OpenAPI spec generation: PASSED");
         System.out.println("   - Generated spec with " + 
openApi.getPaths().size() + " paths");
-        System.out.println("   - HTTPS servers configured: " +
-                          servers.stream().filter(s -> 
s.getUrl().startsWith("https://";)).count());
     }
 
     /**
@@ -242,9 +243,12 @@ public class Http2OpenApiBasicTest extends TestCase {
         // Verify HTTPS servers are configured for HTTP/2 compatibility
         List<Server> servers = openApi.getServers();
         assertNotNull("Should have servers", servers);
-        boolean hasHttpsServer = servers.stream()
-                .anyMatch(server -> server.getUrl().startsWith("https://";));
-        assertTrue("Should have HTTPS server for HTTP/2 compatibility", 
hasHttpsServer);
+        // The server URL is relative, so a client resolves it against the
+        // origin it fetched the document from. Over HTTPS that yields HTTPS
+        // requests without the spec having to name a scheme at all.
+        boolean allRelative = servers.stream()
+                .allMatch(server -> server.getUrl().startsWith("/"));
+        assertTrue("Server URL should be scheme-relative for HTTP/2 
compatibility", allRelative);
 
         System.out.println("✅ HTTP/2 compatibility validation: PASSED");
         System.out.println("   - HTTPS requirement satisfied");
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java
index 7af06be945..b3cffd9470 100644
--- 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java
@@ -108,10 +108,13 @@ public class Http2OpenApiIntegrationTest extends TestCase 
{
         assertNotNull("Should have servers configured", servers);
         assertFalse("Should have at least one server", servers.isEmpty());
 
-        // Verify HTTPS protocol (required for HTTP/2)
-        boolean hasHttpsServer = servers.stream()
-                .anyMatch(server -> server.getUrl().startsWith("https://";));
-        assertTrue("Should have HTTPS server for HTTP/2 compatibility", 
hasHttpsServer);
+        // Verify the scheme is inherited rather than pinned (HTTP/2 needs 
HTTPS)
+        // The server URL is relative, so a client resolves it against the
+        // origin it fetched the document from. Over HTTPS that yields HTTPS
+        // requests without the spec having to name a scheme at all.
+        boolean allRelative = servers.stream()
+                .allMatch(server -> server.getUrl().startsWith("/"));
+        assertTrue("Server URL should be scheme-relative for HTTP/2 
compatibility", allRelative);
 
         // Verify services are properly documented
         assertTrue("Should have documented endpoints", 
openApi.getPaths().size() > 0);
@@ -357,9 +360,12 @@ public class Http2OpenApiIntegrationTest extends TestCase {
 
         // Validate HTTPS configuration
         List<Server> servers = secureApi.getServers();
-        boolean hasSecureServer = servers.stream()
-                .anyMatch(server -> server.getUrl().startsWith("https://";));
-        assertTrue("Should require HTTPS for HTTP/2", hasSecureServer);
+        // The server URL is relative, so a client resolves it against the
+        // origin it fetched the document from. Over HTTPS that yields HTTPS
+        // requests without the spec having to name a scheme at all.
+        boolean allRelative = servers.stream()
+                .allMatch(server -> server.getUrl().startsWith("/"));
+        assertTrue("Server URL should be scheme-relative for HTTP/2", 
allRelative);
 
         // Validate security schemes
         if (secureApi.getComponents() != null && 
secureApi.getComponents().getSecuritySchemes() != null) {
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java
index 57901b9e22..c8ef81a532 100644
--- 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java
@@ -103,8 +103,8 @@ public class OpenApiSpecGeneratorTest extends TestCase {
     }
 
     /**
-     * Test server list generation from HTTP request.
-     * Verifies correct server URL construction.
+     * The published server URL is relative to where the document was fetched
+     * from, so it does not follow the client-supplied Host.
      */
     public void testServerListGeneration() throws Exception {
         // Arrange
@@ -122,14 +122,11 @@ public class OpenApiSpecGeneratorTest extends TestCase {
         assertFalse("At least one server should be configured", 
servers.isEmpty());
 
         Server server = servers.get(0);
-        assertEquals("https://api.example.com:8443/axis2";, server.getUrl());
+        assertEquals("/axis2", server.getUrl());
         assertEquals("Current server", server.getDescription());
     }
 
-    /**
-     * Test server list generation with default HTTP port.
-     * Should not include port 80 in URL.
-     */
+    /** A deployment at the root context publishes "/". */
     public void testServerListGenerationDefaultHttpPort() throws Exception {
         // Arrange
         mockRequest.setScheme("http");
@@ -142,13 +139,10 @@ public class OpenApiSpecGeneratorTest extends TestCase {
 
         // Assert
         Server server = openApi.getServers().get(0);
-        assertEquals("http://localhost";, server.getUrl());
+        assertEquals("/", server.getUrl());
     }
 
-    /**
-     * Test server list generation with default HTTPS port.
-     * Should not include port 443 in URL.
-     */
+    /** The scheme and port no longer take part in the published URL. */
     public void testServerListGenerationDefaultHttpsPort() throws Exception {
         // Arrange
         mockRequest.setScheme("https");
@@ -161,13 +155,10 @@ public class OpenApiSpecGeneratorTest extends TestCase {
 
         // Assert
         Server server = openApi.getServers().get(0);
-        assertEquals("https://secure.example.com/api";, server.getUrl());
+        assertEquals("/api", server.getUrl());
     }
 
-    /**
-     * Test OpenAPI generation with null request.
-     * Should use default server configuration.
-     */
+    /** With no request there is no context path either, so the root is used. 
*/
     public void testOpenApiGenerationWithNullRequest() throws Exception {
         // Act
         OpenAPI openApi = generator.generateOpenApiSpec(null);
@@ -179,8 +170,25 @@ public class OpenApiSpecGeneratorTest extends TestCase {
         assertFalse("Should have at least one server", servers.isEmpty());
 
         Server server = servers.get(0);
-        assertEquals("http://localhost:8080";, server.getUrl());
-        assertEquals("Default server", server.getDescription());
+        assertEquals("/", server.getUrl());
+        assertEquals("Current server", server.getDescription());
+    }
+
+    /**
+     * An operator who needs an absolute URL published - behind a proxy, say -
+     * pins it explicitly rather than having it inferred from the request.
+     */
+    public void testConfiguredServerBaseUrlIsPublished() throws Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        configuration.setServerBaseUrl("https://api.example.com/axis2";);
+        OpenApiSpecGenerator pinned =
+                new OpenApiSpecGenerator(configurationContext, configuration);
+
+        mockRequest.setServerName("attacker.evil.example");
+        Server server = 
pinned.generateOpenApiSpec(mockRequest).getServers().get(0);
+
+        assertEquals("https://api.example.com/axis2";, server.getUrl());
+        assertEquals("Configured server", server.getDescription());
     }
 
     /**

Reply via email to