Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance exception messages with request IDs and metadata #1048

Merged
merged 2 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/8571dd20-b6a8-4cab-9e2a-567e273a016f.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
8 changes: 8 additions & 0 deletions .changes/de403702-531d-4f43-bc33-aa5bc2fc000f.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions runtime/runtime-core/api/runtime-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,6 +59,7 @@ public class aws/smithy/kotlin/runtime/ServiceException : aws/smithy/kotlin/runt
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String>
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()
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading