Skip to content

Commit

Permalink
fix: [SRTP-190] fix openapi error response descriptions for activatio…
Browse files Browse the repository at this point in the history
…n api (#64)

Co-authored-by: Francesco Muscianisi <[email protected]>
  • Loading branch information
fmuscianisi and Francesco Muscianisi authored Feb 4, 2025
1 parent 6d1acf8 commit 5b113af
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 31 deletions.
196 changes: 180 additions & 16 deletions openapi/activation.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,28 @@ paths:
$ref: '#/components/responses/Error'
"401":
#description: Wrong credentials.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/UnauthorizedError'
"403":
#description: Forbidden
$ref: '#/components/responses/Error'
$ref: '#/components/responses/ForbiddenError'
"406":
#description: Not acceptable. Did you require application/json?
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
"409":
#description: Conflict.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/ConflictError'
"415":
#description: Unsupported media type. Did you provide application/json?
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
"429":
#description: Too many request.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
"500":
#description: Server error.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
default:
#description: Unexpected error.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'

get:
operationId: getActivations
Expand Down Expand Up @@ -294,22 +294,22 @@ paths:
$ref: '#/components/responses/Error'
"401":
#description: Access token is missing or invalid.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/UnauthorizedError'
"403":
#description: Forbidden.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/ForbiddenError'
"406":
#description: Not acceptable. Did you require application/json?
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
"429":
#description: Too many request.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
"500":
#description: Server error.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'
default:
#description: Unexpected error.
$ref: '#/components/responses/Error'
$ref: '#/components/responses/GenericError'

components:
# ============================================================================
Expand Down Expand Up @@ -555,7 +555,57 @@ components:
errors:
- code: "01000000F"
description: "Wrong party identifier"


SyntheticError:
description: Synthetic error details.
type: object
additionalProperties: false
properties:
statusCode:
$ref: '#/components/schemas/ErrorCode'
message:
$ref: '#/components/schemas/ErrorDescription'
required:
- code
- description
example:
code: "401"
description: "Invalid JWT."

GenericError:
description: Generic error.
type: object
additionalProperties: false
properties:
timestamp:
type: string
format: date-time
description: The time the error occurred.
path:
type: string
description: The API path where the error occurred.
status:
type: integer
description: HTTP status code of the error.
statusCode:
type: integer
description: HTTP status code of the error.
error:
type: string
description: Description of the error.
message:
type: string
description: Description of the error.
requestId:
type: string
description: Unique identifier for the request.
example:
timestamp: "2024-12-31T09:54:01.763+00:00"
path: "/activations"
status: 415
error: "Unsupported Media Type"
requestId: "3fb00d0f-416"

# ------------------------------------------------------
# Domain specific complex types.
# ------------------------------------------------------
Expand Down Expand Up @@ -767,8 +817,86 @@ components:
schema:
$ref: '#/components/schemas/ActivationLocation'

UnauthorizedError:
description: Response returned when the credentials of the user are missing or invalid.
headers:
Access-Control-Allow-Origin:
description: |
Indicates whether the response can be shared with requesting code
from the given origin.
required: false
schema:
$ref: '#/components/schemas/AccessControlAllowOrigin'
RateLimit-Limit:
description: The number of allowed requests in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitLimit'
RateLimit-Reset:
description: The number of seconds left in the current period
required: false
schema:
$ref: '#/components/schemas/RateLimitReset'
Retry-After:
description: |
The number of seconds to wait before allowing a follow-up request.
required: false
schema:
$ref: '#/components/schemas/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/SyntheticError'
text/*:
schema:
type: string
pattern: "^[ -~]{0,65535}$"
maxLength: 65535

ForbiddenError:
description: Response returned when the user does not have the right permissions.
headers:
Access-Control-Allow-Origin:
description: |
Indicates whether the response can be shared with requesting code
from the given origin.
required: false
schema:
$ref: '#/components/schemas/AccessControlAllowOrigin'
RateLimit-Limit:
description: The number of allowed requests in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitLimit'
RateLimit-Reset:
description: The number of seconds left in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitReset'

ConflictError:
description: Response returned when a RTP payer is already active.
headers:
Access-Control-Allow-Origin:
description: |
Indicates whether the response can be shared with requesting code
from the given origin.
required: false
schema:
$ref: '#/components/schemas/AccessControlAllowOrigin'
RateLimit-Limit:
description: The number of allowed requests in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitLimit'
RateLimit-Reset:
description: The number of seconds left in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitReset'

Error:
description: Error response.
description: Error response detailing constraint validations.
headers:
Access-Control-Allow-Origin:
description: |
Expand Down Expand Up @@ -803,6 +931,42 @@ components:
pattern: "^[ -~]{0,65535}$"
maxLength: 65535

GenericError:
description: Generic Error response.
headers:
Access-Control-Allow-Origin:
description: |
Indicates whether the response can be shared with requesting code
from the given origin.
required: false
schema:
$ref: '#/components/schemas/AccessControlAllowOrigin'
RateLimit-Limit:
description: The number of allowed requests in the current period.
required: false
schema:
$ref: '#/components/schemas/RateLimitLimit'
RateLimit-Reset:
description: The number of seconds left in the current period
required: false
schema:
$ref: '#/components/schemas/RateLimitReset'
Retry-After:
description: |
The number of seconds to wait before allowing a follow-up request.
required: false
schema:
$ref: '#/components/schemas/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
text/*:
schema:
type: string
pattern: "^[ -~]{0,65535}$"
maxLength: 65535

NoContent:
description: No content response.
headers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,109 @@
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.support.WebExchangeBindException;

import java.util.List;
import java.util.Objects;
import java.util.Optional;


/**
* Global exception handler for request validation errors in the activation controller.
* <p>
* This class provides centralized handling for validation-related exceptions occurring
* in controllers under the package {@code it.gov.pagopa.rtp.activator.controller.activation}.
* </p>
*
* <p><strong>Handled Exceptions:</strong></p>
* <ul>
* <li>{@link ConstraintViolationException} - Occurs when method-level constraints are violated.</li>
* <li>{@link WebExchangeBindException} - Occurs when request body validation fails.</li>
* </ul>
*
* <p>For each exception, an {@link ErrorsDto} object is returned, encapsulating a list of
* {@link ErrorDto} objects describing validation errors.</p>
*/
@RestControllerAdvice(basePackages = "it.gov.pagopa.rtp.activator.controller.activation")
public class ActivationExceptionHandler {

/**
* Handles {@link ConstraintViolationException}, which occurs when method-level validation constraints fail.
* <p>
* This method extracts constraint violations, converts them into a list of {@link ErrorDto} objects,
* and returns an {@link ErrorsDto} with HTTP status {@code 400 Bad Request}.
* </p>
*
* @param ex the {@link ConstraintViolationException} containing the validation errors.
* @return a {@link ResponseEntity} with {@code 400 Bad Request} status and an {@link ErrorsDto}
* listing the validation errors.
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorsDto> handleConstraintViolation(ConstraintViolationException ex) {
var errors = ex.getConstraintViolations().stream()
.map(cv -> new ErrorDto()
.code(cv.getMessageTemplate())
.description(cv.getInvalidValue() + " " + cv.getMessage()))
.toList();
ErrorsDto errorsDto = new ErrorsDto();
errorsDto.setErrors(errors);
.map(cv -> new ErrorDto()
.code(cv.getMessageTemplate())
.description(cv.getInvalidValue() + " " + cv.getMessage()))
.toList();

return handleBadRequest(errors);
}


/**
* Handles {@link WebExchangeBindException}, which occurs when request body validation fails.
* <p>
* This method extracts field errors from the binding result, converts them into {@link ErrorDto} objects,
* and returns an {@link ErrorsDto} with HTTP status {@code 400 Bad Request}.
* </p>
*
* @param ex the {@link WebExchangeBindException} containing the validation errors.
* @return a {@link ResponseEntity} with {@code 400 Bad Request} status and an {@link ErrorsDto}
* listing the validation errors.
*/
@ExceptionHandler(WebExchangeBindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@NonNull
public ResponseEntity<ErrorsDto> handleWebExchangeBindException(@NonNull final WebExchangeBindException ex) {
final var errors = Optional.of(ex)
.map(WebExchangeBindException::getBindingResult)
.map(Errors::getFieldErrors)
.stream()
.flatMap(List::stream)
.map(error -> new ErrorDto()
.code(error.getCode())
.description(error.getRejectedValue() + " " + error.getDefaultMessage()))
.toList();

return handleBadRequest(errors);
}


/**
* Constructs a standardized {@link ErrorsDto} response for bad requests.
* <p>
* This utility method is used to generate a consistent error response format
* for validation exceptions.
* </p>
*
* @param errorsList the list of {@link ErrorDto} objects representing validation errors.
* @return a {@link ResponseEntity} with {@code 400 Bad Request} status and an {@link ErrorsDto}
* containing the provided validation errors.
*/
@NonNull
private ResponseEntity<ErrorsDto> handleBadRequest(@NonNull final List<ErrorDto> errorsList) {
Objects.requireNonNull(errorsList, "Errors list must not be null");

final var errorsDto = new ErrorsDto();
errorsDto.setErrors(errorsList);

return ResponseEntity.badRequest().body(errorsDto);
}
}

Loading

0 comments on commit 5b113af

Please sign in to comment.