Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.iemr.ecd.dto.ErrorResponse;
import com.iemr.ecd.dto.RequestBeneficiaryRegistrationDTO;
import com.iemr.ecd.service.associate.BeneficiaryRegistrationServiceImpl;
import com.iemr.ecd.utils.advice.exception_handler.CustomExceptionResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

Expand All @@ -54,10 +56,14 @@ public class BeneficiaryRegistrationController {
@ApiResponses(value = {
@ApiResponse(responseCode = CustomExceptionResponse.SUCCESS_SC_V, description = CustomExceptionResponse.SUCCESS_SC, content = {
@Content(mediaType = "application/json") }),
@ApiResponse(responseCode = CustomExceptionResponse.NOT_FOUND_SC_V, description = CustomExceptionResponse.NOT_FOUND_SC),
@ApiResponse(responseCode = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC_V, description = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC),
@ApiResponse(responseCode = CustomExceptionResponse.DB_EXCEPTION_SC_V, description = CustomExceptionResponse.DB_EXCEPTION_SC),
@ApiResponse(responseCode = CustomExceptionResponse.BAD_REQUEST_SC_V, description = CustomExceptionResponse.BAD_REQUEST_SC) })
@ApiResponse(responseCode = CustomExceptionResponse.NOT_FOUND_SC_V, description = CustomExceptionResponse.NOT_FOUND_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC_V, description = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.DB_EXCEPTION_SC_V, description = CustomExceptionResponse.DB_EXCEPTION_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.BAD_REQUEST_SC_V, description = CustomExceptionResponse.BAD_REQUEST_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }) })
Comment on lines +59 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🌐 Web query:

In OpenAPI 3.x, if an operation defines two responses with the same HTTP status code (e.g., two "500" entries), how is that represented in the final schema and UI?

πŸ’‘ Result:

In OpenAPI 3.x, the operation’s responses are a map keyed by HTTP status code (plus ranges/default). That means you cannot represent β€œtwo separate 500 response objects” under the same status code key; only one responses['500'] entry can exist in the resulting parsed schema/JSON/YAML model because each status code key is unique in the map [1][2]. So if you try to write two responses: { '500': ... , '500': ... } in YAML/JSON, the spec structure can only keep one of them (the later one overwrites the earlier one in typical parsers), and there is no native way in OAS3 to merge them as two distinct response definitions under the same HTTP status code. How to model β€œmultiple shapes/examples for the same status code” instead 1) Put the variability inside the single responses['500'] object: - Use a single response with one content[<media-type>].schema that can represent alternatives, e.g. oneOf (or anyOf / discriminators) [3][4]. - Optionally provide multiple examples within that one response/media type (supported by Swagger UI for β€œmultiple examples” in the cited discussions) [5]. 2) In the UI (e.g., Swagger UI): - The UI will show one response block for status 500, because there is only one response entry keyed by 500 in the parsed spec [1][2]. - If you use oneOf/anyOf at the response schema level, many UIs will typically render/select one alternative schema (often the first item) rather than two distinct 500 responses; Swagger UI’s behavior is discussed in its issue/PR history for oneOf/anyOf handling [6]. - If you instead supply multiple examples for that same 500 response, some Swagger UI versions show them as selectable alternatives (again per the cited guidance) [5]. Practical takeaway - You should not define two 500 entries at the top level under responses. - Define a single 500 response, and then model multiple possible body shapes and/or examples within it using oneOf (and/or discriminator mapping) and examples [3][5].

Citations:


🏁 Script executed:

fd -t f CustomExceptionResponse

Repository: PSMRI/ECD-API

Length of output: 144


🏁 Script executed:

find . -name "CustomExceptionResponse*" -type f

Repository: PSMRI/ECD-API

Length of output: 146


🏁 Script executed:

rg -t java "INTERNAL_SERVER_ERROR_SC_V|DB_EXCEPTION_SC_V" -A 1 -B 1

Repository: PSMRI/ECD-API

Length of output: 50369


🏁 Script executed:

wc -l src/main/java/com/iemr/ecd/controller/associate/BeneficiaryRegistrationController.java

Repository: PSMRI/ECD-API

Length of output: 147


🏁 Script executed:

sed -n '50,90p' src/main/java/com/iemr/ecd/controller/associate/BeneficiaryRegistrationController.java

Repository: PSMRI/ECD-API

Length of output: 3661


Merge duplicate 500 responses into a single entry.

Both INTERNAL_SERVER_ERROR_SC_V and DB_EXCEPTION_SC_V map to 500. In OpenAPI 3.x, response objects are keyed by HTTP status codeβ€”having two entries with the same code violates the spec structure, with the latter overwriting the former in generated documentation, resulting in inconsistent Swagger output.

Consolidate the two 500 responses into one with a unified description indicating both error scenarios, or use a single schema that can represent both error types.

This pattern appears across multiple endpoints in the codebase.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/iemr/ecd/controller/associate/BeneficiaryRegistrationController.java`
around lines 59 - 66, The OpenAPI annotations in
BeneficiaryRegistrationController declare two separate ApiResponse entries for
the same HTTP status (CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC_V and
CustomExceptionResponse.DB_EXCEPTION_SC_V), which conflicts with OpenAPI keys;
update the annotation block to merge these into a single ApiResponse for the 500
status (use either INTERNAL_SERVER_ERROR_SC_V or DB_EXCEPTION_SC_V constant) and
combine their descriptions into one unified message (or reference a shared
schema/ErrorResponse) so only one ApiResponse exists for 500; apply the same
consolidation pattern wherever you see duplicate 500 ApiResponse constants.

public ResponseEntity<Object> beneficiaryRegistration(@RequestBody RequestBeneficiaryRegistrationDTO request,
@RequestHeader(value = "Authorization") String Authorization) {

Expand All @@ -70,10 +76,14 @@ public ResponseEntity<Object> beneficiaryRegistration(@RequestBody RequestBenefi
@ApiResponses(value = {
@ApiResponse(responseCode = CustomExceptionResponse.SUCCESS_SC_V, description = CustomExceptionResponse.SUCCESS_SC, content = {
@Content(mediaType = "application/json") }),
@ApiResponse(responseCode = CustomExceptionResponse.NOT_FOUND_SC_V, description = CustomExceptionResponse.NOT_FOUND_SC),
@ApiResponse(responseCode = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC_V, description = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC),
@ApiResponse(responseCode = CustomExceptionResponse.DB_EXCEPTION_SC_V, description = CustomExceptionResponse.DB_EXCEPTION_SC),
@ApiResponse(responseCode = CustomExceptionResponse.BAD_REQUEST_SC_V, description = CustomExceptionResponse.BAD_REQUEST_SC) })
@ApiResponse(responseCode = CustomExceptionResponse.NOT_FOUND_SC_V, description = CustomExceptionResponse.NOT_FOUND_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC_V, description = CustomExceptionResponse.INTERNAL_SERVER_ERROR_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.DB_EXCEPTION_SC_V, description = CustomExceptionResponse.DB_EXCEPTION_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }),
@ApiResponse(responseCode = CustomExceptionResponse.BAD_REQUEST_SC_V, description = CustomExceptionResponse.BAD_REQUEST_SC, content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)) }) })
public ResponseEntity<Object> updateBeneficiaryDetails(@RequestBody RequestBeneficiaryRegistrationDTO request,
@RequestHeader(value = "Authorization") String Authorization) {

Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/iemr/ecd/dto/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.iemr.ecd.dto;

import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ErrorResponse {

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;

public ErrorResponse(LocalDateTime timestamp, int status, String error, String message, String path) {
this.timestamp = timestamp;
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,34 +42,36 @@
private Object data;
private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());
public static final int SUCCESS = 200;
public static final int GENERIC_FAILURE = 5000;
public static final int OBJECT_FAILURE = 5001;
public static final int USERID_FAILURE = 5002;
public static final int PASSWORD_FAILURE = 5003;
public static final int PREVILAGE_FAILURE = 5004;
public static final int CODE_EXCEPTION = 5005;
public static final int ENVIRONMENT_EXCEPTION = 5006;
public static final int PARSE_EXCEPTION = 5007;
public static final int DB_EXCEPTION = 5008;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int CONFLICT = 409;
public static final int INTERNAL_SERVER_ERROR = 500;
public static final int DB_EXCEPTION = 500;

public static final String SUCCESS_SC = "SUCCESS";
public static final String NOT_FOUND_SC = "NOT_FOUND";
public static final String DB_EXCEPTION_SC = "DB_EXCEPTION";
public static final String BAD_REQUEST_SC = "BAD_REQUEST";
public static final String UNAUTHORIZED_SC = "UNAUTHORIZED";
public static final String FORBIDDEN_SC = "FORBIDDEN";
public static final String NOT_FOUND_SC = "NOT_FOUND";
public static final String CONFLICT_SC = "CONFLICT";
public static final String INTERNAL_SERVER_ERROR_SC = "INTERNAL_SERVER_ERROR";
public static final String DB_EXCEPTION_SC = "DATABASE_ERROR";

public static final String SUCCESS_SC_V = "200";
public static final String NOT_FOUND_SC_V = "404";
public static final String DB_EXCEPTION_SC_V = "5008";
public static final String BAD_REQUEST_SC_V = "400";
public static final String UNAUTHORIZED_SC_V = "401";
public static final String FORBIDDEN_SC_V = "403";
public static final String NOT_FOUND_SC_V = "404";
public static final String CONFLICT_SC_V = "409";
public static final String INTERNAL_SERVER_ERROR_SC_V = "500";
public static final String DB_EXCEPTION_SC_V = "500";

@Expose
private int statusCode = GENERIC_FAILURE;
private int statusCode = INTERNAL_SERVER_ERROR;
@Expose
private String errorMessage = "Failed with generic error";
private String errorMessage = "Internal Server Error";
@Expose
private String status = "FAILURE";
private static final String RESPONSE = "{\"response\":\"$$STRING\"}";
Expand Down Expand Up @@ -102,85 +104,54 @@
Date currDate = Calendar.getInstance().getTime();
logger.info("error happened due to " + thrown.getClass().getSimpleName() + " at " + currDate.toString());

switch (thrown.getCause().getClass().getSimpleName()) {
String causeClass = thrown.getClass().getSimpleName();
if (thrown.getCause() != null) {
causeClass = thrown.getCause().getClass().getSimpleName();
}

switch (causeClass) {
case "IEMRException":
this.statusCode = USERID_FAILURE;
this.statusCode = UNAUTHORIZED;
status = "User login failed";
errorMessage = thrown.getMessage();
break;
case "JSONException":
this.statusCode = OBJECT_FAILURE;
this.statusCode = BAD_REQUEST;
status = "Invalid object conversion";
errorMessage = "Invalid object conversion";
break;

case "SQLException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "SQLGrammarException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "DataException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "ConstraintViolationException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "GenericJDBCException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "JDBCConnectionException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "LockAcquisitionException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
break;
case "InvalidDataAccessResourceUsageException":
this.statusCode = DB_EXCEPTION;
case "JDBCException":

Check warning on line 132 in src/main/java/com/iemr/ecd/utils/advice/exception_handler/CustomExceptionResponse.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge the previous cases into this one using comma-separated label.

See more on https://sonarcloud.io/project/issues?id=PSMRI_ECD-API&issues=AZ35jhiOLLWurOR5gvdX&open=AZ35jhiOLLWurOR5gvdX&pullRequest=132
this.statusCode = INTERNAL_SERVER_ERROR;
status = DB_EXCEPTION_SC;
errorMessage = thrown.getMessage();
errorMessage = "Database error occurred";
break;

case "ParseException":
case "NullPointerException":

case "ArrayIndexOutOfBoundsException":

case "IOException":
case "ConnectException":
case "ConnectIOException":
this.statusCode = ENVIRONMENT_EXCEPTION;
status = "Failed with connection issues at " + currDate.toString() + "Please try after some time. "
+ "If error is still seen, contact your administrator.";
errorMessage = thrown.getMessage();
break;
case "JDBCException":
this.statusCode = DB_EXCEPTION;
status = DB_EXCEPTION_SC;
this.statusCode = INTERNAL_SERVER_ERROR;
status = "Environment or connection error";
errorMessage = thrown.getMessage();
break;
default:
this.statusCode = GENERIC_FAILURE;
status = "Failed with " + thrown.getMessage() + " at " + currDate.toString()
+ ".Please try after some time. If error is still seen, contact your administrator.";
this.statusCode = INTERNAL_SERVER_ERROR;
status = "An unexpected error occurred";
errorMessage = thrown.getMessage();
break;
}
logger.error("Failure happend with " + thrown.getMessage() + "at " + currDate.toString(), thrown);
logger.error("Failure happened with " + thrown.getMessage() + "at " + currDate.toString(), thrown);
}

public void setError(int errorCode, String message, String status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,79 @@
*/
package com.iemr.ecd.utils.advice.exception_handler;

import java.time.LocalDateTime;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestControllerAdvice
public class EcdGlobalExceptionHandler extends ResponseEntityExceptionHandler {
import com.iemr.ecd.dto.ErrorResponse;

Logger logger = LoggerFactory.getLogger(this.getClass().getName());
import jakarta.servlet.http.HttpServletRequest;

@ExceptionHandler
public CustomExceptionResponse handleInvalidRequestParameterException(InvalidRequestException e) {
logger.error("invalid request exception : " + e);
CustomExceptionResponse customExceptionResponse = new CustomExceptionResponse();
customExceptionResponse.setError(e);
@RestControllerAdvice
public class EcdGlobalExceptionHandler {

return customExceptionResponse;
private static final Logger logger = LoggerFactory.getLogger(EcdGlobalExceptionHandler.class);

@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<ErrorResponse> handleInvalidRequestException(InvalidRequestException ex, HttpServletRequest request) {
logger.error("Invalid request: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler
public CustomExceptionResponse handleGeneralException(ECDException e) {
logger.error("ECD exception : " + e);
CustomExceptionResponse customExceptionResponse = new CustomExceptionResponse();
customExceptionResponse.setError(e);
@ExceptionHandler(ECDException.class)
public ResponseEntity<ErrorResponse> handleECDException(ECDException ex, HttpServletRequest request) {
logger.error("ECD error: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}

return customExceptionResponse;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex, HttpServletRequest request) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));

logger.error("Validation error: {}", errorMessage);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message(errorMessage)
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex, HttpServletRequest request) {
logger.error("Unexpected error: ", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.message("An unexpected error occurred. Please contact administrator.")
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
Comment on lines +87 to +98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether explicit handlers exist for common client-input exceptions
# Expected: no matches currently, indicating they fall through to generic Exception->500.

rg -nC2 --type=java '(`@ExceptionHandler`\()|(HttpMessageNotReadableException|MethodArgumentTypeMismatchException|MissingServletRequestParameterException|ConstraintViolationException)' \
  src/main/java/com/iemr/ecd/utils/advice/exception_handler/EcdGlobalExceptionHandler.java

Repository: PSMRI/ECD-API

Length of output: 1103


🏁 Script executed:

cat -n src/main/java/com/iemr/ecd/utils/advice/exception_handler/EcdGlobalExceptionHandler.java

Repository: PSMRI/ECD-API

Length of output: 4781


Add explicit handlers for client-input exceptions before generic 500 fallback.

Malformed JSON, type mismatches, missing parameters, and constraint violations currently fall through the generic Exception handler and return 500. These should return 400 to preserve REST error semantics.

Add handlers for HttpMessageNotReadableException, MethodArgumentTypeMismatchException, MissingServletRequestParameterException, and ConstraintViolationException with HttpStatus.BAD_REQUEST before the catch-all @ExceptionHandler(Exception.class).

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/iemr/ecd/utils/advice/exception_handler/EcdGlobalExceptionHandler.java`
around lines 87 - 98, In EcdGlobalExceptionHandler, add specific
`@ExceptionHandler` methods for HttpMessageNotReadableException,
MethodArgumentTypeMismatchException, MissingServletRequestParameterException,
and ConstraintViolationException that construct and return an ErrorResponse
(same shape as used in handleGeneralException) with HttpStatus.BAD_REQUEST and
the request path, and place these handlers before the existing
handleGeneralException(Exception ex, HttpServletRequest request) catch-all so
client input errors return 400 instead of falling through to 500; reuse the
ErrorResponse.builder() pattern and logger to log the exception details in each
new handler.

}