All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.protocols.json.internal.unmarshall.AwsJsonProtocolErrorUnmarshaller Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.awssdk.protocols.json.internal.unmarshall;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.core.http.HttpResponseHandler;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
import software.amazon.awssdk.http.SdkHttpFullResponse;
import software.amazon.awssdk.protocols.core.ExceptionMetadata;
import software.amazon.awssdk.protocols.json.ErrorCodeParser;
import software.amazon.awssdk.protocols.json.JsonContent;
import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory;
import software.amazon.awssdk.utils.StringUtils;

/**
 * Unmarshaller for AWS specific error responses. All errors are unmarshalled into a subtype of
 * {@link AwsServiceException} (more specifically a subtype generated for each AWS service).
 */
@SdkInternalApi
public final class AwsJsonProtocolErrorUnmarshaller implements HttpResponseHandler {

    private static final String QUERY_COMPATIBLE_ERRORCODE_DELIMITER = ";";
    private static final String X_AMZN_QUERY_ERROR = "x-amzn-query-error";
    private final JsonProtocolUnmarshaller jsonProtocolUnmarshaller;
    private final List exceptions;
    private final ErrorMessageParser errorMessageParser;
    private final JsonFactory jsonFactory;
    private final Supplier defaultExceptionSupplier;
    private final ErrorCodeParser errorCodeParser;
    private final Function> exceptionMetadataMapper;
    private final boolean hasAwsQueryCompatible;

    private AwsJsonProtocolErrorUnmarshaller(Builder builder) {
        this.jsonProtocolUnmarshaller = builder.jsonProtocolUnmarshaller;
        this.errorCodeParser = builder.errorCodeParser;
        this.errorMessageParser = builder.errorMessageParser;
        this.jsonFactory = builder.jsonFactory;
        this.defaultExceptionSupplier = builder.defaultExceptionSupplier;
        this.exceptions = builder.exceptions;
        this.exceptionMetadataMapper = builder.exceptionMetadataMapper != null
                                       ? builder.exceptionMetadataMapper
                                       : this::defaultMapToExceptionMetadata;
        this.hasAwsQueryCompatible = builder.hasAwsQueryCompatible;
    }

    @Override
    public AwsServiceException handle(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
        return unmarshall(response, executionAttributes);
    }

    private AwsServiceException unmarshall(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
        JsonContent jsonContent = JsonContent.createJsonContent(response, jsonFactory);
        String errorCode = errorCodeParser.parseErrorCode(response, jsonContent);

        Optional modeledExceptionMetadata = exceptionMetadataMapper.apply(errorCode);

        SdkPojo sdkPojo = modeledExceptionMetadata.map(ExceptionMetadata::exceptionBuilderSupplier)
                                                  .orElse(defaultExceptionSupplier)
                                                  .get();

        AwsServiceException.Builder exception = ((AwsServiceException) jsonProtocolUnmarshaller
            .unmarshall(sdkPojo, response, jsonContent.getJsonNode())).toBuilder();
        String errorMessage = errorMessageParser.parseErrorMessage(response, jsonContent.getJsonNode());
        exception.awsErrorDetails(extractAwsErrorDetails(response, executionAttributes, jsonContent,
                                                         getEffectiveErrorCode(response, errorCode), errorMessage));
        exception.clockSkew(getClockSkew(executionAttributes));
        // Status code and request id are sdk level fields
        exception.message(errorMessageForException(errorMessage, errorCode, response.statusCode()));
        exception.statusCode(statusCode(response, modeledExceptionMetadata));
        exception.requestId(response.firstMatchingHeader(X_AMZN_REQUEST_ID_HEADERS).orElse(null));
        exception.extendedRequestId(response.firstMatchingHeader(X_AMZ_ID_2_HEADER).orElse(null));
        return exception.build();
    }

    private String getEffectiveErrorCode(SdkHttpFullResponse response, String errorCode) {
        if (this.hasAwsQueryCompatible) {
            String compatibleErrorCode = queryCompatibleErrorCodeFromResponse(response);
            if (!StringUtils.isEmpty(compatibleErrorCode)) {
                return compatibleErrorCode;
            }
        }
        return errorCode;
    }

    private String queryCompatibleErrorCodeFromResponse(SdkHttpFullResponse response) {
        Optional headerValue = response.firstMatchingHeader(X_AMZN_QUERY_ERROR);
        return headerValue.map(this::parseQueryErrorCodeFromDelimiter).orElse(null);
    }

    private String parseQueryErrorCodeFromDelimiter(String queryHeaderValue) {
        int delimiter = queryHeaderValue.indexOf(QUERY_COMPATIBLE_ERRORCODE_DELIMITER);
        if (delimiter > 0) {
            return queryHeaderValue.substring(0, delimiter);
        }
        return null;
    }

    private String errorMessageForException(String errorMessage, String errorCode, int statusCode) {
        if (StringUtils.isNotBlank(errorMessage)) {
            return errorMessage;
        }

        if (StringUtils.isNotBlank(errorCode)) {
            return "Service returned error code " + errorCode;
        }

        return "Service returned HTTP status code " + statusCode;
    }

    private Duration getClockSkew(ExecutionAttributes executionAttributes) {
        Integer timeOffset = executionAttributes.getAttribute(SdkExecutionAttribute.TIME_OFFSET);
        return timeOffset == null ? null : Duration.ofSeconds(timeOffset);
    }

    private int statusCode(SdkHttpFullResponse response, Optional modeledExceptionMetadata) {
        if (response.statusCode() != 0) {
            return response.statusCode();
        }

        return modeledExceptionMetadata.filter(m -> m.httpStatusCode() != null)
                                       .map(ExceptionMetadata::httpStatusCode)
                                       .orElse(500);
    }

    /**
     * Build the {@link AwsErrorDetails} from the metadata in the response.
     *
     * @param response HTTP response.
     * @param executionAttributes Execution attributes.
     * @param jsonContent Parsed JSON content.
     * @param errorCode Parsed error code/type.
     * @param errorMessage Parsed error message.
     * @return AwsErrorDetails
     */
    private AwsErrorDetails extractAwsErrorDetails(SdkHttpFullResponse response,
                                                   ExecutionAttributes executionAttributes,
                                                   JsonContent jsonContent,
                                                   String errorCode,
                                                   String errorMessage) {
        AwsErrorDetails.Builder errorDetails =
            AwsErrorDetails.builder()
                           .errorCode(errorCode)
                           .serviceName(executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME))
                           .sdkHttpResponse(response);

        if (jsonContent.getRawContent() != null) {
            errorDetails.rawResponse(SdkBytes.fromByteArray(jsonContent.getRawContent()));
        }

        errorDetails.errorMessage(errorMessage);
        return errorDetails.build();
    }

    private Optional defaultMapToExceptionMetadata(String errorCode) {
        return exceptions.stream()
                         .filter(e -> e.errorCode().equals(errorCode))
                         .findAny();
    }

    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder for {@link AwsJsonProtocolErrorUnmarshaller}.
     */
    public static final class Builder {

        private JsonProtocolUnmarshaller jsonProtocolUnmarshaller;
        private List exceptions;
        private ErrorMessageParser errorMessageParser;
        private JsonFactory jsonFactory;
        private Supplier defaultExceptionSupplier;
        private ErrorCodeParser errorCodeParser;
        private Function> exceptionMetadataMapper;
        private boolean hasAwsQueryCompatible;

        private Builder() {
        }

        /**
         * Underlying response unmarshaller. Exceptions for the JSON protocol are follow the same unmarshalling logic
         * as success responses but with an additional "error type" that allows for polymorphic deserialization.
         *
         * @return This builder for method chaining.
         */
        public Builder jsonProtocolUnmarshaller(JsonProtocolUnmarshaller jsonProtocolUnmarshaller) {
            this.jsonProtocolUnmarshaller = jsonProtocolUnmarshaller;
            return this;
        }

        /**
         * List of {@link ExceptionMetadata} to represent the modeled exceptions for the service.
         * For AWS services the error type is a string representing the type of the modeled exception.
         *
         * @return This builder for method chaining.
         */
        @Deprecated
        public Builder exceptions(List exceptions) {
            this.exceptions = exceptions;
            return this;
        }

        public Builder exceptionMetadataSupplier(Function> exceptionMetadataSupplier) {
            this.exceptionMetadataMapper = exceptionMetadataSupplier;
            return this;
        }

        /**
         * Implementation that can extract an error message from the JSON response. Implementations may look for a
         * specific field in the JSON document or a specific header for example.
         *
         * @return This builder for method chaining.
         */
        public Builder errorMessageParser(ErrorMessageParser errorMessageParser) {
            this.errorMessageParser = errorMessageParser;
            return this;
        }

        /**
         * JSON Factory to create a JSON parser.
         *
         * @return This builder for method chaining.
         */
        public Builder jsonFactory(JsonFactory jsonFactory) {
            this.jsonFactory = jsonFactory;
            return this;
        }

        /**
         * Default exception type if "error code" does not match any known modeled exception. This is the generated
         * base exception for the service (i.e. DynamoDbException).
         *
         * @return This builder for method chaining.
         */
        public Builder defaultExceptionSupplier(Supplier defaultExceptionSupplier) {
            this.defaultExceptionSupplier = defaultExceptionSupplier;
            return this;
        }

        /**
         * Implementation of {@link ErrorCodeParser} that can extract an error code or type from the JSON response.
         * Implementations may look for a specific field in the JSON document or a specific header for example.
         *
         * @return This builder for method chaining.
         */
        public Builder errorCodeParser(ErrorCodeParser errorCodeParser) {
            this.errorCodeParser = errorCodeParser;
            return this;
        }

        public AwsJsonProtocolErrorUnmarshaller build() {
            return new AwsJsonProtocolErrorUnmarshaller(this);
        }

        /**
         * Provides a check on whether AwsQueryCompatible trait is found in Metadata.
         * If true, error code will be derived from custom header. Otherwise, error code will be retrieved from its
         * original source
         *
         * @param hasAwsQueryCompatible boolean of whether the AwsQueryCompatible trait is found
         * @return This builder for method chaining.
         */
        public Builder hasAwsQueryCompatible(boolean hasAwsQueryCompatible) {
            this.hasAwsQueryCompatible = hasAwsQueryCompatible;
            return this;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy