DefaultMinioErrorHandler.java

/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.bremersee.data.minio;

import io.minio.errors.BucketPolicyTooLargeException;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import io.minio.messages.ErrorResponse;
import java.io.IOException;
import java.util.Optional;
import okhttp3.Response;
import org.springframework.util.StringUtils;

/**
 * The default minio error handler.
 *
 * @author Christian Bremer
 */
public class DefaultMinioErrorHandler extends AbstractMinioErrorHandler {

  static final String ERROR_CODE_PREFIX = "MINIO_";

  @Override
  public MinioException map(Throwable t) {
    if (t instanceof IllegalArgumentException) {
      return new MinioException(
          400,
          ERROR_CODE_PREFIX + "BAD_REQUEST",
          StringUtils.hasText(t.getMessage()) ? t.getMessage() : "Bad request.",
          t);
    }
    if (t instanceof IOException) {
      return new MinioException(
          500,
          ERROR_CODE_PREFIX + "IO_ERROR",
          StringUtils.hasText(t.getMessage()) ? t.getMessage() : "IO operation failed.",
          t);
    }
    if (t instanceof io.minio.errors.MinioException) {
      return mapMinioException((io.minio.errors.MinioException) t);
    }
    return new MinioException(
        500,
        ERROR_CODE_PREFIX + "UNSPECIFIED",
        StringUtils.hasText(t.getMessage()) ? t.getMessage() : "Unmapped minio error.",
        t);
  }

  private MinioException mapMinioException(io.minio.errors.MinioException e) {
    int status = 500;
    String errorCode = ERROR_CODE_PREFIX + "UNSPECIFIED";
    String message = StringUtils.hasText(e.getMessage())
        ? e.getMessage()
        : "Unspecified minio error.";

    if (e instanceof BucketPolicyTooLargeException) {
      status = 400;
      errorCode = ERROR_CODE_PREFIX + "BUCKET_POLICY_TOO_LARGE";
      message = e.toString();

    } else if (e instanceof ErrorResponseException) {
      ErrorResponseException ere = (ErrorResponseException) e;
      status = getStatus(ere);
      errorCode = getErrorCode(ere);
      message = getMessage(ere);

    } else if (e instanceof InsufficientDataException) {
      status = 400;
      errorCode = ERROR_CODE_PREFIX + "INSUFFICIENT_DATA";

    } else if (e instanceof InternalException) {
      errorCode = ERROR_CODE_PREFIX + "INTERNAL_ERROR";

    } else if (e instanceof InvalidResponseException) {
      errorCode = ERROR_CODE_PREFIX + "INVALID_RESPONSE";

    } else if (e instanceof ServerException) {
      errorCode = ERROR_CODE_PREFIX + "SERVER_EXCEPTION";

    } else if (e instanceof XmlParserException) {
      errorCode = ERROR_CODE_PREFIX + "XML_PARSER_ERROR";
    }
    return new MinioException(status, errorCode, message, e);
  }

  private int getStatus(ErrorResponseException e) {
    return Optional.of(e)
        .map(ErrorResponseException::errorResponse)
        .map(ErrorResponse::code)
        .map(code -> getStatus(code, e.response()))
        .orElse(500);
  }

  private int getStatus(String errorCode, Response response) {
    // see https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
    switch (errorCode) {
      case "AmbiguousGrantByEmailAddress":
      case "AuthorizationHeaderMalformed":
      case "BadDigest":
      case "CredentialsNotSupported":
      case "EntityTooSmall":
      case "EntityTooLarge":
      case "ExpiredToken":
      case "IllegalLocationConstraintException":
      case "IllegalVersioningConfigurationException":
      case "IncompleteBody":
      case "IncorrectNumberOfFilesInPostRequest":
      case "InlineDataTooLarge":
      case "InvalidAccessPoint":
      case "InvalidArgument":
      case "InvalidBucketName":
      case "InvalidDigest":
      case "InvalidEncryptionAlgorithmError":
      case "InvalidLocationConstraint":
      case "InvalidPart":
      case "InvalidPartOrder":
      case "InvalidPolicyDocument":
      case "InvalidRequest":
      case "InvalidSOAPRequest":
      case "InvalidStorageClass":
      case "InvalidTargetBucketForLogging":
      case "InvalidToken":
      case "InvalidURI":
      case "KeyTooLongError":
      case "MalformedACLError":
      case "MalformedPOSTRequest":
      case "MalformedXML":
      case "MaxMessageLengthExceeded":
      case "MaxPostPreDataLengthExceededError":
      case "MetadataTooLarge":
      case "MissingRequestBodyError":
      case "MissingSecurityElement":
      case "MissingSecurityHeader":
      case "NoLoggingStatusForKey":
      case "RequestIsNotMultiPartContent":
      case "RequestTimeout":
      case "RequestTorrentOfBucketError":
      case "ServerSideEncryptionConfigurationNotFoundError":
      case "TokenRefreshRequired":
      case "TooManyAccessPoints":
      case "TooManyBuckets":
      case "UnexpectedContent":
      case "UnresolvableGrantByEmailAddress":
      case "UserKeyMustBeSpecified":
      case "InvalidTag":
      case "MalformedPolicy":
        return 400;
      case "UnauthorizedAccess":
        return 401;
      case "AccessDenied":
      case "AccountProblem":
      case "AllAccessDisabled":
      case "CrossLocationLoggingProhibited":
      case "InvalidAccessKeyId":
      case "InvalidObjectState":
      case "InvalidPayer":
      case "InvalidSecurity":
      case "NotSignedUp":
      case "RequestTimeTooSkewed":
      case "SignatureDoesNotMatch":
        return 403;
      case "ResourceNotFound": // not in list
      case "NoSuchAccessPoint":
      case "NoSuchBucket":
      case "NoSuchKey":
      case "NoSuchObject": // not in list
      case "NoSuchUpload":
      case "NoSuchVersion":
      case "NoSuchLifecycleConfiguration":
      case "NoSuchBucketPolicy":
      case "NoSuchObjectLockConfiguration": // not in list
      case "NoSuchOutpost":
      case "NoSuchTagSet":
      case "UnsupportedOperation":
        return 404;
      case "MethodNotAllowed":
        return 405;
      case "BucketAlreadyExists":
      case "BucketAlreadyOwnedByYou":
      case "BucketNotEmpty":
      case "InvalidBucketState":
      case "OperationAborted":
      case "InvalidOutpostState":
        return 409;
      case "MissingContentLength":
        return 411;
      case "PreconditionFailed":
        return 412;
      case "InvalidRange":
        return 416;
      case "InternalError":
        return 500;
      case "NotImplemented":
        return 501;
      case "ServiceUnavailable":
      case "SlowDown":
        return 503;
      default:
        return Optional.ofNullable(response)
            .map(Response::code)
            .filter(code -> code >= 400)
            .orElse(400);
    }
  }

  private String getErrorCode(ErrorResponseException e) {
    return Optional.of(e)
        .map(ErrorResponseException::errorResponse)
        .map(ErrorResponse::code)
        .orElse(ERROR_CODE_PREFIX + "UNSPECIFIED_ERROR_RESPONSE");
  }

  private String getMessage(ErrorResponseException e) {
    return Optional.of(e)
        .map(ErrorResponseException::errorResponse)
        .map(ErrorResponse::message)
        .orElseGet(() -> StringUtils.hasText(e.getMessage())
            ? e.getMessage()
            : "Unspecified error response.");
  }

}