From 186d5f5276f13110d57e99c6346e261e11f90497 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 19:12:26 +0000 Subject: [PATCH 1/2] fix(common-openapi): resolve full servers[] precedence in OpenapiOperationView Wire path-item-level servers[] through OpenapiPathView into OpenapiOperationView so operation.servers resolves operation > path-item > root precedence per the OpenAPI spec, instead of skipping the path-item level entirely. Also mirror OpenapiView's empty-configs handling so operation/path-level servers aren't silently dropped when no server configs are supplied. Make server URL variable resolution explicit: a templated server URL whose configured override doesn't match the declared variable pattern now throws OpenapiException instead of silently falling back to the variable's default value. Non-templated server URLs are unaffected since there is no variable to mismatch against. Closes #2020 --- .../openapi/view/OpenapiOperationView.java | 13 +- .../common/openapi/view/OpenapiPathView.java | 2 +- .../openapi/view/OpenapiServerView.java | 23 ++- .../common/openapi/view/OpenapiViewTest.java | 149 ++++++++++++++++++ 4 files changed, 182 insertions(+), 5 deletions(-) diff --git a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiOperationView.java b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiOperationView.java index c37d3328aa..dfda07ced8 100644 --- a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiOperationView.java +++ b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiOperationView.java @@ -20,9 +20,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import io.aklivity.zilla.runtime.common.openapi.config.OpenapiServerConfig; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiOperation; +import io.aklivity.zilla.runtime.common.openapi.model.OpenapiServer; import io.aklivity.zilla.runtime.common.openapi.model.resolver.OpenapiResolver; public final class OpenapiOperationView @@ -50,6 +52,7 @@ public final class OpenapiOperationView OpenapiResolver resolver, String method, String path, + List pathServers, OpenapiOperation model) { this.specification = specification; @@ -83,9 +86,13 @@ public final class OpenapiOperationView .toList() : specification.security; - this.servers = model.servers != null - ? model.servers.stream() - .flatMap(s -> configs.stream().map(c -> new OpenapiServerView(resolver, s, c))) + List effectiveServers = model.servers != null ? model.servers : pathServers; + + this.servers = effectiveServers != null + ? effectiveServers.stream() + .flatMap(s -> configs.isEmpty() + ? Stream.of(new OpenapiServerView(resolver, s, null)) + : configs.stream().map(c -> new OpenapiServerView(resolver, s, c))) .toList() : specification.servers; diff --git a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiPathView.java b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiPathView.java index c570c6658c..757d08a1ec 100644 --- a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiPathView.java +++ b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiPathView.java @@ -80,7 +80,7 @@ public final class OpenapiPathView .filter(e -> e.getValue().apply(model) != null) .collect(Collectors.toMap(Map.Entry::getKey, e -> new OpenapiOperationView(specification, supplyCompositeId.getAsLong(), - configs, resolver, e.getKey(), path, e.getValue().apply(model)))); + configs, resolver, e.getKey(), path, model.servers, e.getValue().apply(model)))); this.extensions = model.extensions; } diff --git a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java index c3569080f8..4e32495b89 100644 --- a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java +++ b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java @@ -28,6 +28,7 @@ import org.agrona.LangUtil; +import io.aklivity.zilla.runtime.common.openapi.config.OpenapiException; import io.aklivity.zilla.runtime.common.openapi.config.OpenapiServerConfig; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiServer; import io.aklivity.zilla.runtime.common.openapi.model.resolver.OpenapiResolver; @@ -91,13 +92,31 @@ public static final class VariableMatcher { private static final Pattern VARIABLE = Pattern.compile("\\{([^}]*.?)\\}"); + private final String template; + private final boolean templated; private final Matcher matcher; private final String defaultValue; public String resolve( String value) { - return value != null && matcher.reset(value).matches() ? value : defaultValue; + String resolved; + + if (!templated || value == null) + { + resolved = defaultValue; + } + else if (matcher.reset(value).matches()) + { + resolved = value; + } + else + { + throw new OpenapiException(String.format( + "openapi server url \"%s\" does not match variable pattern of template \"%s\"", value, template)); + } + + return resolved; } private VariableMatcher( @@ -108,6 +127,8 @@ private VariableMatcher( .replaceAll(mr -> resolver.apply(mr.group(1)).values.stream() .collect(joining("|", "(", ")"))); + this.template = value; + this.templated = VARIABLE.matcher(value).find(); this.matcher = Pattern.compile(regex).matcher(""); this.defaultValue = VARIABLE.matcher(value) .replaceAll(mr -> resolver.apply(mr.group(1)).defaultValue); diff --git a/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java b/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java index ed83334b24..e72d8d257b 100644 --- a/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java +++ b/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java @@ -27,6 +27,7 @@ import org.junit.Test; +import io.aklivity.zilla.runtime.common.openapi.config.OpenapiException; import io.aklivity.zilla.runtime.common.openapi.config.OpenapiServerConfig; import io.aklivity.zilla.runtime.common.openapi.model.Openapi; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiComponents; @@ -39,6 +40,7 @@ import io.aklivity.zilla.runtime.common.openapi.model.OpenapiSchema; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiSecurityScheme; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiServer; +import io.aklivity.zilla.runtime.common.openapi.model.OpenapiServerVariable; public class OpenapiViewTest { @@ -305,6 +307,153 @@ public void shouldNotResolveScopesWithoutFlows() throws Exception assertEquals(List.of(), schemeView.scopes); } + @Test + public void shouldResolveOperationLevelServersOverPathAndRoot() throws Exception + { + OpenapiServer rootServer = new OpenapiServer(); + rootServer.url = "http://root.example.com"; + + OpenapiServer pathServer = new OpenapiServer(); + pathServer.url = "http://path.example.com"; + + OpenapiServer operationServer = new OpenapiServer(); + operationServer.url = "http://operation.example.com"; + + OpenapiOperation operation = new OpenapiOperation(); + operation.operationId = "ReadItems"; + operation.servers = List.of(operationServer); + + OpenapiPath path = new OpenapiPath(); + path.get = operation; + path.servers = List.of(pathServer); + + Openapi model = new Openapi(); + model.servers = List.of(rootServer); + model.paths = Map.of("/items", path); + + OpenapiServerConfig config = OpenapiServerConfig.builder().build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); + OpenapiOperationView operationView = view.paths.get("/items").methods.get("GET"); + + assertEquals(1, operationView.servers.size()); + assertEquals(URI.create("http://operation.example.com:80"), operationView.servers.get(0).url); + } + + @Test + public void shouldResolvePathLevelServersOverRootWhenOperationHasNone() throws Exception + { + OpenapiServer rootServer = new OpenapiServer(); + rootServer.url = "http://root.example.com"; + + OpenapiServer pathServer = new OpenapiServer(); + pathServer.url = "http://path.example.com"; + + OpenapiOperation operation = new OpenapiOperation(); + operation.operationId = "ReadItems"; + + OpenapiPath path = new OpenapiPath(); + path.get = operation; + path.servers = List.of(pathServer); + + Openapi model = new Openapi(); + model.servers = List.of(rootServer); + model.paths = Map.of("/items", path); + + OpenapiServerConfig config = OpenapiServerConfig.builder().build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); + OpenapiOperationView operationView = view.paths.get("/items").methods.get("GET"); + + assertEquals(1, operationView.servers.size()); + assertEquals(URI.create("http://path.example.com:80"), operationView.servers.get(0).url); + } + + @Test + public void shouldResolveRootLevelServersWhenNoOperationOrPathServers() throws Exception + { + OpenapiServer rootServer = new OpenapiServer(); + rootServer.url = "http://root.example.com"; + + OpenapiOperation operation = new OpenapiOperation(); + operation.operationId = "ReadItems"; + + OpenapiPath path = new OpenapiPath(); + path.get = operation; + + Openapi model = new Openapi(); + model.servers = List.of(rootServer); + model.paths = Map.of("/items", path); + + OpenapiServerConfig config = OpenapiServerConfig.builder().build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); + OpenapiOperationView operationView = view.paths.get("/items").methods.get("GET"); + + assertEquals(1, operationView.servers.size()); + assertEquals(URI.create("http://root.example.com:80"), operationView.servers.get(0).url); + } + + @Test + public void shouldResolveServerUrlVariableDefaultWhenNoOverride() throws Exception + { + OpenapiServerVariable variable = new OpenapiServerVariable(); + variable.values = List.of("prod", "staging"); + variable.defaultValue = "prod"; + + OpenapiServer server = new OpenapiServer(); + server.url = "http://{env}.example.com"; + server.variables = Map.of("env", variable); + + Openapi model = new Openapi(); + model.servers = List.of(server); + + OpenapiServerConfig config = OpenapiServerConfig.builder().build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); + + assertEquals(URI.create("http://prod.example.com:80"), view.servers.get(0).url); + } + + @Test + public void shouldResolveServerUrlVariableOverrideMatchingPattern() throws Exception + { + OpenapiServerVariable variable = new OpenapiServerVariable(); + variable.values = List.of("prod", "staging"); + variable.defaultValue = "prod"; + + OpenapiServer server = new OpenapiServer(); + server.url = "http://{env}.example.com"; + server.variables = Map.of("env", variable); + + Openapi model = new Openapi(); + model.servers = List.of(server); + + OpenapiServerConfig config = OpenapiServerConfig.builder() + .url("http://staging.example.com") + .build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); + + assertEquals(URI.create("http://staging.example.com:80"), view.servers.get(0).url); + } + + @Test(expected = OpenapiException.class) + public void shouldRejectServerUrlOverrideNotMatchingVariablePattern() throws Exception + { + OpenapiServerVariable variable = new OpenapiServerVariable(); + variable.values = List.of("prod", "staging"); + variable.defaultValue = "prod"; + + OpenapiServer server = new OpenapiServer(); + server.url = "http://{env}.example.com"; + server.variables = Map.of("env", variable); + + Openapi model = new Openapi(); + model.servers = List.of(server); + + OpenapiServerConfig config = OpenapiServerConfig.builder() + .url("http://dev.example.com") + .build(); + + OpenapiView.of(model, List.of(config)); + } + public static final class SampleExtension { public String key; From cf4bbcf2ace061f8e94ec7b6c4c342682b506b4c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 19:20:40 +0000 Subject: [PATCH 2/2] fix(common-openapi): keep silent default fallback for server url variable mismatch Revert the OpenapiException-on-mismatch behavior added for server URL variable resolution. binding-mcp-openapi already treats a configured server URL as an unconditional override elsewhere (resolveServerOverride bypasses common-openapi's matcher entirely), and OpenapiOperationView.servers cross-products every model server against every configured override, so a non-matching pairing is the expected case whenever more than one server or config is present, not a misconfiguration. Throwing there would break that case rather than catch a real error. --- .../openapi/view/OpenapiServerView.java | 23 +------------------ .../common/openapi/view/OpenapiViewTest.java | 8 +++---- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java index 4e32495b89..c3569080f8 100644 --- a/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java +++ b/runtime/common-openapi/src/main/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiServerView.java @@ -28,7 +28,6 @@ import org.agrona.LangUtil; -import io.aklivity.zilla.runtime.common.openapi.config.OpenapiException; import io.aklivity.zilla.runtime.common.openapi.config.OpenapiServerConfig; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiServer; import io.aklivity.zilla.runtime.common.openapi.model.resolver.OpenapiResolver; @@ -92,31 +91,13 @@ public static final class VariableMatcher { private static final Pattern VARIABLE = Pattern.compile("\\{([^}]*.?)\\}"); - private final String template; - private final boolean templated; private final Matcher matcher; private final String defaultValue; public String resolve( String value) { - String resolved; - - if (!templated || value == null) - { - resolved = defaultValue; - } - else if (matcher.reset(value).matches()) - { - resolved = value; - } - else - { - throw new OpenapiException(String.format( - "openapi server url \"%s\" does not match variable pattern of template \"%s\"", value, template)); - } - - return resolved; + return value != null && matcher.reset(value).matches() ? value : defaultValue; } private VariableMatcher( @@ -127,8 +108,6 @@ private VariableMatcher( .replaceAll(mr -> resolver.apply(mr.group(1)).values.stream() .collect(joining("|", "(", ")"))); - this.template = value; - this.templated = VARIABLE.matcher(value).find(); this.matcher = Pattern.compile(regex).matcher(""); this.defaultValue = VARIABLE.matcher(value) .replaceAll(mr -> resolver.apply(mr.group(1)).defaultValue); diff --git a/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java b/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java index e72d8d257b..9c41db01f7 100644 --- a/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java +++ b/runtime/common-openapi/src/test/java/io/aklivity/zilla/runtime/common/openapi/view/OpenapiViewTest.java @@ -27,7 +27,6 @@ import org.junit.Test; -import io.aklivity.zilla.runtime.common.openapi.config.OpenapiException; import io.aklivity.zilla.runtime.common.openapi.config.OpenapiServerConfig; import io.aklivity.zilla.runtime.common.openapi.model.Openapi; import io.aklivity.zilla.runtime.common.openapi.model.OpenapiComponents; @@ -433,8 +432,8 @@ public void shouldResolveServerUrlVariableOverrideMatchingPattern() throws Excep assertEquals(URI.create("http://staging.example.com:80"), view.servers.get(0).url); } - @Test(expected = OpenapiException.class) - public void shouldRejectServerUrlOverrideNotMatchingVariablePattern() throws Exception + @Test + public void shouldDefaultServerUrlVariableWhenOverrideDoesNotMatchPattern() throws Exception { OpenapiServerVariable variable = new OpenapiServerVariable(); variable.values = List.of("prod", "staging"); @@ -450,8 +449,9 @@ public void shouldRejectServerUrlOverrideNotMatchingVariablePattern() throws Exc OpenapiServerConfig config = OpenapiServerConfig.builder() .url("http://dev.example.com") .build(); + OpenapiView view = OpenapiView.of(model, List.of(config)); - OpenapiView.of(model, List.of(config)); + assertEquals(URI.create("http://prod.example.com:80"), view.servers.get(0).url); } public static final class SampleExtension