1 /*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License").
5 * You may not use this file except in compliance with the License.
6 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15
16 package software.amazon.awssdk.protocols.query.unmarshall;
17
18 import java.io.IOException;
19 import java.util.List;
20 import java.util.Optional;
21 import java.util.function.Function;
22 import java.util.function.Supplier;
23 import software.amazon.awssdk.annotations.SdkProtectedApi;
24 import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
25 import software.amazon.awssdk.awscore.exception.AwsServiceException;
26 import software.amazon.awssdk.core.SdkBytes;
27 import software.amazon.awssdk.core.SdkPojo;
28 import software.amazon.awssdk.core.http.HttpResponseHandler;
29 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
30 import software.amazon.awssdk.http.SdkHttpFullResponse;
31 import software.amazon.awssdk.protocols.core.ExceptionMetadata;
32 import software.amazon.awssdk.protocols.query.internal.unmarshall.AwsXmlErrorUnmarshaller;
33 import software.amazon.awssdk.utils.Pair;
34
35 /**
36 * Error unmarshaller for Query/EC2/XML based protocols. Some examples of error responses from
37 * the various protocols are below.
38 *
39 * <h3>Legacy Query (SimpleDB/EC2)</h3>
40 * <pre>
41 * {@code
42 * <Response>
43 * <Errors>
44 * <Error>
45 * <Code>MissingParameter</Code>
46 * <Message>The request must contain the parameter DomainName</Message>
47 * <BoxUsage>0.0055590278</BoxUsage>
48 * </Error>
49 * </Errors>
50 * <RequestID>ad3280dd-5ac1-efd1-b9b0-a86969a9234d</RequestID>
51 * </Response>
52 * }
53 * </pre>
54 *
55 * <h3>Traditional Query/Rest-XML (Cloudfront)</h3>
56 * <pre>
57 * {@code
58 * <ErrorResponse xmlns="http://cloudfront.amazonaws.com/doc/2017-10-30/">
59 * <Error>
60 * <Type>Sender</Type>
61 * <Code>MalformedInput</Code>
62 * <Message>Invalid XML document</Message>
63 * </Error>
64 * <RequestId>7c8da4af-de44-11e8-a60e-1b2014315455</RequestId>
65 * </ErrorResponse>
66 * }
67 * </pre>
68 *
69 * <h3>Amazon S3</h3>
70 * <pre>
71 * {@code
72 * <Error>
73 * <Code>NoSuchBucket</Code>
74 * <Message>The specified bucket does not exist</Message>
75 * <BucketName>flajdfadjfladjf</BucketName>
76 * <RequestId>D9DBB9F267849CA3</RequestId>
77 * <HostId>fn8B1fUvWzg7I3CIeMT4UMqCZDF4+QO1JlbOJlQAVOosACZsLWv/K2dapVncz34a2mArhp11PjI=</HostId>
78 * </Error>
79 * }
80 * </pre>
81 */
82 @SdkProtectedApi
83 public final class AwsXmlErrorProtocolUnmarshaller implements HttpResponseHandler<AwsServiceException> {
84
85 private final AwsXmlErrorUnmarshaller awsXmlErrorUnmarshaller;
86 private final Function<XmlElement, Optional<XmlElement>> errorRootExtractor;
87
88 private AwsXmlErrorProtocolUnmarshaller(Builder builder) {
89 this.errorRootExtractor = builder.errorRootExtractor;
90 this.awsXmlErrorUnmarshaller = AwsXmlErrorUnmarshaller.builder()
91 .defaultExceptionSupplier(builder.defaultExceptionSupplier)
92 .exceptions(builder.exceptions)
93 .errorUnmarshaller(builder.errorUnmarshaller)
94 .build();
95 }
96
97 @Override
98 public AwsServiceException handle(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
99 Pair<XmlElement, SdkBytes> xmlAndBytes = parseXml(response);
100 XmlElement document = xmlAndBytes.left();
101 Optional<XmlElement> errorRoot = errorRootExtractor.apply(document);
102 return awsXmlErrorUnmarshaller.unmarshall(document, errorRoot, Optional.of(xmlAndBytes.right()), response,
103 executionAttributes);
104 }
105
106 /**
107 * Parse the response content into an XML document. If we fail to read the content or the XML is malformed
108 * we still return an empty {@link XmlElement} so that unmarshalling can proceed. In those failure
109 * cases we can't parse out the error code so we'd unmarshall into a generic service exception.
110 *
111 * @param response HTTP response.
112 * @return Pair of the parsed {@link XmlElement} and the raw bytes of the response.
113 */
114 private Pair<XmlElement, SdkBytes> parseXml(SdkHttpFullResponse response) {
115 SdkBytes bytes = getResponseBytes(response);
116 try {
117 return Pair.of(XmlDomParser.parse(bytes.asInputStream()), bytes);
118 } catch (Exception e) {
119 return Pair.of(XmlElement.empty(), bytes);
120 }
121 }
122
123 /**
124 * Buffers the response content into an {@link SdkBytes} object. Used to fill the {@link AwsErrorDetails}. If
125 * an {@link IOException} occurs then this will return {@link #emptyXmlBytes()} so that unmarshalling can proceed.
126 *
127 * @param response HTTP response.
128 * @return Raw bytes of response.
129 */
130 private SdkBytes getResponseBytes(SdkHttpFullResponse response) {
131 try {
132 return response.content()
133 .map(SdkBytes::fromInputStream)
134 .orElseGet(this::emptyXmlBytes);
135 } catch (Exception e) {
136 return emptyXmlBytes();
137 }
138 }
139
140 /**
141 * @return Dummy XML document to allow unmarshalling when response can't be read/parsed.
142 */
143 private SdkBytes emptyXmlBytes() {
144 return SdkBytes.fromUtf8String("<eof/>");
145 }
146
147
148 /**
149 * @return New Builder instance.
150 */
151 public static Builder builder() {
152 return new Builder();
153 }
154
155 /**
156 * Builder for {@link AwsXmlErrorProtocolUnmarshaller}.
157 */
158 public static final class Builder {
159
160 private List<ExceptionMetadata> exceptions;
161 private Supplier<SdkPojo> defaultExceptionSupplier;
162 private Function<XmlElement, Optional<XmlElement>> errorRootExtractor;
163 private XmlErrorUnmarshaller errorUnmarshaller;
164
165 private Builder() {
166 }
167
168 /**
169 * List of {@link ExceptionMetadata} to represent the modeled exceptions for the service.
170 * For AWS services the error type is a string representing the type of the modeled exception.
171 *
172 * @return This builder for method chaining.
173 */
174 public Builder exceptions(List<ExceptionMetadata> exceptions) {
175 this.exceptions = exceptions;
176 return this;
177 }
178
179 /**
180 * Default exception type if "error code" does not match any known modeled exception. This is the generated
181 * base exception for the service (i.e. DynamoDbException).
182 *
183 * @return This builder for method chaining.
184 */
185 public Builder defaultExceptionSupplier(Supplier<SdkPojo> defaultExceptionSupplier) {
186 this.defaultExceptionSupplier = defaultExceptionSupplier;
187 return this;
188 }
189
190 /**
191 * Extracts the <Error/> element from the top level XML document. Different protocols have slightly
192 * different formats for where the Error element is located. The error root of the XML document contains
193 * the code, message and any modeled fields of the exception. See javadocs of
194 * {@link AwsXmlErrorProtocolUnmarshaller} for examples.
195 *
196 * @param errorRootExtractor Function that extracts the <Error/> element from the root XML document.
197 * @return This builder for method chaining.
198 */
199 public Builder errorRootExtractor(Function<XmlElement, Optional<XmlElement>> errorRootExtractor) {
200 this.errorRootExtractor = errorRootExtractor;
201 return this;
202 }
203
204 /**
205 * The unmarshaller to use. The unmarshaller only unmarshalls any modeled fields of the exception,
206 * additional metadata is extracted by {@link AwsXmlErrorProtocolUnmarshaller}.
207 *
208 * @param errorUnmarshaller Error unmarshaller to use.
209 * @return This builder for method chaining.
210 */
211 public Builder errorUnmarshaller(XmlErrorUnmarshaller errorUnmarshaller) {
212 this.errorUnmarshaller = errorUnmarshaller;
213 return this;
214 }
215
216 /**
217 * @return New instance of {@link AwsXmlErrorProtocolUnmarshaller}.
218 */
219 public AwsXmlErrorProtocolUnmarshaller build() {
220 return new AwsXmlErrorProtocolUnmarshaller(this);
221 }
222 }
223 }
224