Skip to content

Commit 4143c7f

Browse files
authored
feat: enhance exception messages with request IDs and metadata (#1048)
1 parent 2dd9abc commit 4143c7f

File tree

6 files changed

+161
-5
lines changed

6 files changed

+161
-5
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "8571dd20-b6a8-4cab-9e2a-567e273a016f",
3+
"type": "feature",
4+
"description": "Add request IDs to exception messages where available",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#1212"
7+
]
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "de403702-531d-4f43-bc33-aa5bc2fc000f",
3+
"type": "feature",
4+
"description": "Add error metadata to ServiceException messages when a service-provided message isn't available",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#1212"
7+
]
8+
}

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/response/HttpResponse.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ private data class DefaultHttpResponse(
4646
override val status: HttpStatusCode,
4747
override val headers: Headers,
4848
override val body: HttpBody,
49-
) : HttpResponse
49+
) : HttpResponse {
50+
override val summary: String = "HTTP ${status.value} ${status.description}"
51+
}
5052

5153
/**
5254
* Replace the response body

runtime/runtime-core/api/runtime-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public abstract interface annotation class aws/smithy/kotlin/runtime/InternalApi
2525
}
2626

2727
public abstract interface class aws/smithy/kotlin/runtime/ProtocolResponse {
28+
public abstract fun getSummary ()Ljava/lang/String;
2829
}
2930

3031
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
5859
public fun <init> (Ljava/lang/String;)V
5960
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
6061
public fun <init> (Ljava/lang/Throwable;)V
62+
protected fun getDisplayMetadata ()Ljava/util/List;
6163
public fun getMessage ()Ljava/lang/String;
6264
public synthetic fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ErrorMetadata;
6365
public fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ServiceErrorMetadata;

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,16 @@ public open class ClientException : SdkBaseException {
7070
* Generic interface that any protocol (e.g. HTTP, MQTT, etc) can extend to provide additional access to
7171
* protocol specific details.
7272
*/
73-
public interface ProtocolResponse
73+
public interface ProtocolResponse {
74+
/**
75+
* A short string summarizing the response, suitable for display in exception messages or debug scenarios
76+
*/
77+
public val summary: String
78+
}
7479

75-
private object EmptyProtocolResponse : ProtocolResponse
80+
private object EmptyProtocolResponse : ProtocolResponse {
81+
override val summary: String = "(empty response)"
82+
}
7683

7784
public open class ServiceErrorMetadata : ErrorMetadata() {
7885
public companion object {
@@ -149,8 +156,21 @@ public open class ServiceException : SdkBaseException {
149156

150157
public constructor(cause: Throwable?) : super(cause)
151158

152-
override val message: String?
153-
get() = super.message ?: sdkErrorMetadata.errorMessage
159+
protected open val displayMetadata: List<String>
160+
get() = buildList {
161+
val serviceProvidedMessage = super.message ?: sdkErrorMetadata.errorMessage
162+
if (serviceProvidedMessage == null) {
163+
sdkErrorMetadata.errorCode?.let { add("Service returned error code $it") }
164+
add("Error type: ${sdkErrorMetadata.errorType}")
165+
add("Protocol response: ${sdkErrorMetadata.protocolResponse.summary}")
166+
} else {
167+
add(serviceProvidedMessage)
168+
}
169+
sdkErrorMetadata.requestId?.let { add("Request ID: $it") }
170+
}
171+
172+
override val message: String
173+
get() = displayMetadata.joinToString()
154174

155175
override val sdkErrorMetadata: ServiceErrorMetadata = ServiceErrorMetadata()
156176
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime
6+
7+
import aws.smithy.kotlin.runtime.collections.MutableAttributes
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
11+
private const val ERROR_CODE = "ErrorWithNoMessage"
12+
private const val METADATA_MESSAGE = "This is a message included in metadata but not the regular response"
13+
private const val PROTOCOL_RESPONSE_SUMMARY = "HTTP 418 I'm a teapot"
14+
private const val REQUEST_ID = "abcd-1234"
15+
private const val SERVICE_MESSAGE = "This is an service-provided message"
16+
17+
private val ERROR_TYPE = ServiceException.ErrorType.Server
18+
private val PROTOCOL_RESPONSE = object : ProtocolResponse {
19+
override val summary: String = PROTOCOL_RESPONSE_SUMMARY
20+
}
21+
22+
class ExceptionsTest {
23+
@Test
24+
fun testRegularMessage() {
25+
val e = FooServiceException(SERVICE_MESSAGE)
26+
assertEquals(SERVICE_MESSAGE, e.message)
27+
}
28+
29+
@Test
30+
fun testMetadataMessage() {
31+
val e = FooServiceException {
32+
set(ServiceErrorMetadata.ErrorMessage, METADATA_MESSAGE)
33+
}
34+
assertEquals(METADATA_MESSAGE, e.message)
35+
}
36+
37+
@Test
38+
fun testRegularMessageWithRequestId() {
39+
val e = FooServiceException(SERVICE_MESSAGE) {
40+
set(ServiceErrorMetadata.RequestId, REQUEST_ID)
41+
}
42+
assertEquals("$SERVICE_MESSAGE, Request ID: $REQUEST_ID", e.message)
43+
}
44+
45+
@Test
46+
fun testMetadataMessageWithRequestId() {
47+
val e = FooServiceException {
48+
set(ServiceErrorMetadata.ErrorMessage, METADATA_MESSAGE)
49+
set(ServiceErrorMetadata.RequestId, REQUEST_ID)
50+
}
51+
assertEquals("$METADATA_MESSAGE, Request ID: $REQUEST_ID", e.message)
52+
}
53+
54+
@Test
55+
fun testErrorCodeNoMessage() {
56+
val e = FooServiceException {
57+
set(ServiceErrorMetadata.ErrorCode, ERROR_CODE)
58+
set(ServiceErrorMetadata.ErrorType, ERROR_TYPE)
59+
set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE)
60+
}
61+
assertEquals(
62+
"Service returned error code $ERROR_CODE, " +
63+
"Error type: $ERROR_TYPE, " +
64+
"Protocol response: $PROTOCOL_RESPONSE_SUMMARY",
65+
e.message,
66+
)
67+
}
68+
69+
@Test
70+
fun testErrorCodeNoMessageWithRequestId() {
71+
val e = FooServiceException {
72+
set(ServiceErrorMetadata.ErrorCode, ERROR_CODE)
73+
set(ServiceErrorMetadata.ErrorType, ERROR_TYPE)
74+
set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE)
75+
set(ServiceErrorMetadata.RequestId, REQUEST_ID)
76+
}
77+
assertEquals(
78+
"Service returned error code $ERROR_CODE, " +
79+
"Error type: $ERROR_TYPE, " +
80+
"Protocol response: $PROTOCOL_RESPONSE_SUMMARY, " +
81+
"Request ID: $REQUEST_ID",
82+
e.message,
83+
)
84+
}
85+
86+
@Test
87+
fun testNoErrorCodeNoMessage() {
88+
val e = FooServiceException {
89+
set(ServiceErrorMetadata.ErrorType, ERROR_TYPE)
90+
set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE)
91+
}
92+
assertEquals("Error type: $ERROR_TYPE, Protocol response: $PROTOCOL_RESPONSE_SUMMARY", e.message)
93+
}
94+
95+
@Test
96+
fun testNoErrorCodeNoMessageWithRequestId() {
97+
val e = FooServiceException {
98+
set(ServiceErrorMetadata.ErrorType, ERROR_TYPE)
99+
set(ServiceErrorMetadata.ProtocolResponse, PROTOCOL_RESPONSE)
100+
set(ServiceErrorMetadata.RequestId, REQUEST_ID)
101+
}
102+
assertEquals(
103+
"Error type: $ERROR_TYPE, Protocol response: $PROTOCOL_RESPONSE_SUMMARY, Request ID: $REQUEST_ID",
104+
e.message,
105+
)
106+
}
107+
}
108+
109+
private class FooServiceException(
110+
message: String? = null,
111+
attributes: MutableAttributes.() -> Unit = { },
112+
) : ServiceException(message) {
113+
init {
114+
sdkErrorMetadata.attributes.apply(attributes)
115+
}
116+
}

0 commit comments

Comments
 (0)