diff --git a/.changes/8571dd20-b6a8-4cab-9e2a-567e273a016f.json b/.changes/8571dd20-b6a8-4cab-9e2a-567e273a016f.json new file mode 100644 index 000000000..66bb19be5 --- /dev/null +++ b/.changes/8571dd20-b6a8-4cab-9e2a-567e273a016f.json @@ -0,0 +1,8 @@ +{ + "id": "8571dd20-b6a8-4cab-9e2a-567e273a016f", + "type": "feature", + "description": "Add request IDs to exception messages where available", + "issues": [ + "awslabs/aws-sdk-kotlin#1212" + ] +} \ No newline at end of file diff --git a/.changes/de403702-531d-4f43-bc33-aa5bc2fc000f.json b/.changes/de403702-531d-4f43-bc33-aa5bc2fc000f.json new file mode 100644 index 000000000..9be9250e2 --- /dev/null +++ b/.changes/de403702-531d-4f43-bc33-aa5bc2fc000f.json @@ -0,0 +1,8 @@ +{ + "id": "de403702-531d-4f43-bc33-aa5bc2fc000f", + "type": "feature", + "description": "Add error metadata to ServiceException messages when a service-provided message isn't available", + "issues": [ + "awslabs/aws-sdk-kotlin#1212" + ] +} diff --git a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpResponse.kt b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpResponse.kt index f8ca9b2bf..e8490306c 100644 --- a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpResponse.kt +++ b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpResponse.kt @@ -46,7 +46,9 @@ private data class DefaultHttpResponse( override val status: HttpStatusCode, override val headers: Headers, override val body: HttpBody, -) : HttpResponse +) : HttpResponse { + override val summary: String = "HTTP ${status.value} ${status.description}" +} /** * Replace the response body diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index bfe9a1f31..c9a68703f 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -25,6 +25,7 @@ public abstract interface annotation class aws/smithy/kotlin/runtime/InternalApi } public abstract interface class aws/smithy/kotlin/runtime/ProtocolResponse { + public abstract fun getSummary ()Ljava/lang/String; } public class aws/smithy/kotlin/runtime/SdkBaseException : java/lang/RuntimeException { @@ -58,6 +59,7 @@ public class aws/smithy/kotlin/runtime/ServiceException : aws/smithy/kotlin/runt public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V public fun (Ljava/lang/Throwable;)V + protected fun getDisplayMetadata ()Ljava/util/List; public fun getMessage ()Ljava/lang/String; public synthetic fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ErrorMetadata; public fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ServiceErrorMetadata; diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt index 53a24bdf4..87697ec7d 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt @@ -70,9 +70,16 @@ public open class ClientException : SdkBaseException { * Generic interface that any protocol (e.g. HTTP, MQTT, etc) can extend to provide additional access to * protocol specific details. */ -public interface ProtocolResponse +public interface ProtocolResponse { + /** + * A short string summarizing the response, suitable for display in exception messages or debug scenarios + */ + public val summary: String +} -private object EmptyProtocolResponse : ProtocolResponse +private object EmptyProtocolResponse : ProtocolResponse { + override val summary: String = "(empty response)" +} public open class ServiceErrorMetadata : ErrorMetadata() { public companion object { @@ -149,8 +156,21 @@ public open class ServiceException : SdkBaseException { public constructor(cause: Throwable?) : super(cause) - override val message: String? - get() = super.message ?: sdkErrorMetadata.errorMessage + protected open val displayMetadata: List + get() = buildList { + val serviceProvidedMessage = super.message ?: sdkErrorMetadata.errorMessage + if (serviceProvidedMessage == null) { + sdkErrorMetadata.errorCode?.let { add("Service returned error code $it") } + add("Error type: ${sdkErrorMetadata.errorType}") + add("Protocol response: ${sdkErrorMetadata.protocolResponse.summary}") + } else { + add(serviceProvidedMessage) + } + sdkErrorMetadata.requestId?.let { add("Request ID: $it") } + } + + override val message: String + get() = displayMetadata.joinToString() override val sdkErrorMetadata: ServiceErrorMetadata = ServiceErrorMetadata() } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt new file mode 100644 index 000000000..551718c36 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime + +import aws.smithy.kotlin.runtime.collections.MutableAttributes +import kotlin.test.Test +import kotlin.test.assertEquals + +private const val ERROR_CODE = "ErrorWithNoMessage" +private const val METADATA_MESSAGE = "This is a message included in metadata but not the regular response" +private const val PROTOCOL_RESPONSE_SUMMARY = "HTTP 418 I'm a teapot" +private const val REQUEST_ID = "abcd-1234" +private const val SERVICE_MESSAGE = "This is an service-provided message" + +private val ERROR_TYPE = ServiceException.ErrorType.Server +private val PROTOCOL_RESPONSE = object : ProtocolResponse { + override val summary: String = PROTOCOL_RESPONSE_SUMMARY +} + +class ExceptionsTest { + @Test + fun testRegularMessage() { + val e = FooServiceException(SERVICE_MESSAGE) + assertEquals(SERVICE_MESSAGE, e.message) + } + + @Test + fun testMetadataMessage() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorMessage, METADATA_MESSAGE) + } + assertEquals(METADATA_MESSAGE, e.message) + } + + @Test + fun testRegularMessageWithRequestId() { + val e = FooServiceException(SERVICE_MESSAGE) { + set(ServiceErrorMetadata.RequestId, REQUEST_ID) + } + assertEquals("$SERVICE_MESSAGE, Request ID: $REQUEST_ID", e.message) + } + + @Test + fun testMetadataMessageWithRequestId() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorMessage, METADATA_MESSAGE) + set(ServiceErrorMetadata.RequestId, REQUEST_ID) + } + assertEquals("$METADATA_MESSAGE, Request ID: $REQUEST_ID", e.message) + } + + @Test + fun testErrorCodeNoMessage() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorCode, ERROR_CODE) + set(ServiceErrorMetadata.ErrorType, ERROR_TYPE) + set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE) + } + assertEquals( + "Service returned error code $ERROR_CODE, " + + "Error type: $ERROR_TYPE, " + + "Protocol response: $PROTOCOL_RESPONSE_SUMMARY", + e.message, + ) + } + + @Test + fun testErrorCodeNoMessageWithRequestId() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorCode, ERROR_CODE) + set(ServiceErrorMetadata.ErrorType, ERROR_TYPE) + set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE) + set(ServiceErrorMetadata.RequestId, REQUEST_ID) + } + assertEquals( + "Service returned error code $ERROR_CODE, " + + "Error type: $ERROR_TYPE, " + + "Protocol response: $PROTOCOL_RESPONSE_SUMMARY, " + + "Request ID: $REQUEST_ID", + e.message, + ) + } + + @Test + fun testNoErrorCodeNoMessage() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorType, ERROR_TYPE) + set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE) + } + assertEquals("Error type: $ERROR_TYPE, Protocol response: $PROTOCOL_RESPONSE_SUMMARY", e.message) + } + + @Test + fun testNoErrorCodeNoMessageWithRequestId() { + val e = FooServiceException { + set(ServiceErrorMetadata.ErrorType, ERROR_TYPE) + set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE) + set(ServiceErrorMetadata.RequestId, REQUEST_ID) + } + assertEquals( + "Error type: $ERROR_TYPE, Protocol response: $PROTOCOL_RESPONSE_SUMMARY, Request ID: $REQUEST_ID", + e.message, + ) + } +} + +private class FooServiceException( + message: String? = null, + attributes: MutableAttributes.() -> Unit = { }, +) : ServiceException(message) { + init { + sdkErrorMetadata.attributes.apply(attributes) + } +}