Repository: smithy-lang/smithy
Pull request #2674 (2025-06-23 16:47:19)

Validate that query errors are unique
This adds validation that ensures that no two errors in a service that have the awsQuery or awsQueryCompatible trait have the same code. This is necessary in these cases because query services may change the error code with a trait, and so do not benefit from Smithy's general restriction that every shape in the closure of a service must have a unique name.

There is at least one instance of this in the wild, they'll have to suppress it for this case.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/QueryErrorCodeValidator.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/QueryErrorCodeValidator.java
new file mode 100644
index 000000000..f97455a9b
--- /dev/null
+++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/QueryErrorCodeValidator.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package software.amazon.smithy.aws.traits.protocols;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.TopDownIndex;
+import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.SmithyInternalApi;
+
+/**
+ * Emits a warning if two or more error shapes share the same error code.
+ */
+@SmithyInternalApi
+public class QueryErrorCodeValidator extends AbstractValidator {
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> events = new ArrayList<>();
+        for (ServiceShape service : model.getServiceShapes()) {
+            if (service.hasTrait(AwsQueryCompatibleTrait.class) || service.hasTrait(AwsQueryTrait.class)
+                    || service.hasTrait(Ec2QueryTrait.class)) {
+                events.addAll(validateService(model, service));
+            }
+        }
+        return events;
+    }
+
+    private List<ValidationEvent> validateService(Model model, ServiceShape service) {
+        List<ValidationEvent> events = new ArrayList<>();
+        TopDownIndex index = TopDownIndex.of(model);
+        List<ShapeId> errors = new ArrayList<>(service.getErrors());
+        index.getContainedOperations(service).forEach(operation -> errors.addAll(operation.getErrors()));
+
+        Map<String, Set<ShapeId>> errorCodeBindings = new HashMap<>();
+        for (ShapeId errorId : errors) {
+            String errorCode = errorId.getName();
+            Shape errorShape = model.expectShape(errorId);
+            if (errorShape.hasTrait(AwsQueryErrorTrait.class)) {
+                AwsQueryErrorTrait trait = errorShape.expectTrait(AwsQueryErrorTrait.class);
+                errorCode = trait.getCode();
+            }
+            errorCodeBindings.computeIfAbsent(errorCode, k -> new TreeSet<>()).add(errorId);
+        }
+
+        for (Map.Entry<String, Set<ShapeId>> entry : errorCodeBindings.entrySet()) {
+            if (entry.getValue().size() == 1) {
+                continue;
+            }
+            String errorCode = entry.getKey();
+            String shapes = entry.getValue().stream().map(Objects::toString).collect(Collectors.joining(", "));
+            events.add(danger(service,
+                    String.format("Multiple error shapes with the error code `%s` found: [%s]. This error code "
+                            + "ambiguity will result in the wrong error shape being deserialized when running the "
+                            + "query protocol. This pain will be carried forward when migrating to a new protocol "
+                            + "when using the awsQueryCompatible trait.", errorCode, shapes)));
+        }
+        return events;
+    }
+}
diff --git a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index 133106fc8..76bf941d9 100644
--- a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -5,6 +5,7 @@ software.amazon.smithy.aws.traits.SdkServiceIdValidator
 software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryValidator
 software.amazon.smithy.aws.traits.protocols.ProtocolHttpValidator
 software.amazon.smithy.aws.traits.EventSourceValidator
+software.amazon.smithy.aws.traits.protocols.QueryErrorCodeValidator
 software.amazon.smithy.aws.traits.protocols.ProtocolHttpPayloadValidator
 software.amazon.smithy.aws.traits.tagging.ServiceTaggingValidator
 software.amazon.smithy.aws.traits.tagging.TagEnabledServiceValidator


Test patch:
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.errors
new file mode 100644
index 000000000..9406380d9
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.errors
@@ -0,0 +1,2 @@
+[DANGER] smithy.example#QueryCompatibleService: Multiple error shapes with the error code `ResourceNotFound` found: [smithy.example#DocumentNotFound, smithy.example#ResourceNotFound]. This error code ambiguity will result in the wrong error shape being deserialized when running the query protocol. This pain will be carried forward when migrating to a new protocol when using the awsQueryCompatible trait. | QueryErrorCode
+[DANGER] smithy.example#QueryService: Multiple error shapes with the error code `ResourceNotFound` found: [smithy.example#DocumentNotFound, smithy.example#ResourceNotFound]. This error code ambiguity will result in the wrong error shape being deserialized when running the query protocol. This pain will be carried forward when migrating to a new protocol when using the awsQueryCompatible trait. | QueryErrorCode
diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.smithy
new file mode 100644
index 000000000..c6052402e
--- /dev/null
+++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/query-errors-must-be-unique.smithy
@@ -0,0 +1,39 @@
+$version: "2.0"
+
+namespace smithy.example
+
+use aws.protocols#awsJson1_0
+use aws.protocols#awsQuery
+use aws.protocols#awsQueryCompatible
+use aws.protocols#awsQueryError
+
+@awsQueryCompatible
+@awsJson1_0
+service QueryCompatibleService {
+    version: "2020-02-05"
+    errors: [
+        DocumentNotFound
+        ResourceNotFound
+    ]
+}
+
+@awsQueryError(code: "ResourceNotFound", httpResponseCode: 400)
+@error("client")
+structure DocumentNotFound {
+    message: String
+}
+
+@error("client")
+structure ResourceNotFound {
+    message: String
+}
+
+@awsQuery
+@xmlNamespace(uri: "https://example.com/")
+service QueryService {
+    version: "2020-02-05"
+    errors: [
+        DocumentNotFound
+        ResourceNotFound
+    ]
+}