WorkflowRunHttpExceptionHandler.java
package org.hammer.audio.workflow.execution.http;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.Locale;
import org.hammer.audio.workflow.execution.WorkflowRunException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** RFC 9457 mapping for typed workflow-run failures. */
@RestControllerAdvice
public final class WorkflowRunHttpExceptionHandler {
private static final String PROBLEM_BASE = "https://audio-analyzer.dev/problems/";
/** Maps run lifecycle and preflight failures to stable problem responses. */
@ExceptionHandler(WorkflowRunException.class)
public ProblemDetail handleRunFailure(
WorkflowRunException exception, HttpServletRequest request) {
HttpStatus status = status(exception.code());
String problemName = exception.code().name().toLowerCase(Locale.ROOT).replace('_', '-');
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, exception.getMessage());
problem.setTitle(title(problemName));
problem.setType(URI.create(PROBLEM_BASE + problemName));
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("code", exception.code().name());
if (exception.runId() != null) {
problem.setProperty("runId", exception.runId());
}
if (exception.startCommandId() != null) {
problem.setProperty("startCommandId", exception.startCommandId());
}
if (!exception.violations().isEmpty()) {
problem.setProperty(
"violations",
exception.violations().stream()
.map(WorkflowRunApiModels.ViolationResponse::from)
.toList());
}
return problem;
}
private static HttpStatus status(WorkflowRunException.Code code) {
return switch (code) {
case UNKNOWN_RUN -> HttpStatus.NOT_FOUND;
case SOURCE_UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
case VALIDATION_FAILED, UNSUPPORTED_NODE -> HttpStatus.UNPROCESSABLE_ENTITY;
case DUPLICATE_START_COMMAND, ILLEGAL_TRANSITION -> HttpStatus.CONFLICT;
case RESULT_NOT_AVAILABLE -> HttpStatus.TOO_EARLY;
case BACKEND_FAILURE -> HttpStatus.INTERNAL_SERVER_ERROR;
};
}
private static String title(String problemName) {
StringBuilder result = new StringBuilder();
for (String word : problemName.split("-")) {
if (!result.isEmpty()) {
result.append(' ');
}
result.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return result.toString();
}
}