diff --git a/src/main/java/dev/kruhlmann/imgfloat/controller/RestExceptionHandler.java b/src/main/java/dev/kruhlmann/imgfloat/controller/RestExceptionHandler.java new file mode 100644 index 0000000..e6e6578 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/controller/RestExceptionHandler.java @@ -0,0 +1,61 @@ +package dev.kruhlmann.imgfloat.controller; + +import dev.kruhlmann.imgfloat.model.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +@RestControllerAdvice +public class RestExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(RestExceptionHandler.class); + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleStatusException( + ResponseStatusException exception, + HttpServletRequest request + ) { + String path = request.getRequestURI(); + HttpStatusCode statusCode = exception.getStatusCode(); + LOG.warn( + "Request {} {} failed with status {}: {}", + request.getMethod(), + path, + statusCode.value(), + exception.getReason(), + exception + ); + String message = exception.getReason(); + if (message == null || message.isBlank()) { + message = "Request failed with status " + statusCode.value(); + } + return ResponseEntity.status(statusCode).body(new ErrorResponse(statusCode.value(), message, path)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleUnexpectedException(Exception exception, HttpServletRequest request) { + String path = request.getRequestURI(); + LOG.error( + "Unhandled exception while processing {} {}: {}", + request.getMethod(), + path, + exception.getMessage(), + exception + ); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "An unexpected error occurred while handling the request.", + path + ) + ); + } +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/ErrorResponse.java b/src/main/java/dev/kruhlmann/imgfloat/model/ErrorResponse.java new file mode 100644 index 0000000..1f02056 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/model/ErrorResponse.java @@ -0,0 +1,3 @@ +package dev.kruhlmann.imgfloat.model; + +public record ErrorResponse(int status, String message, String path) {} diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index 0048047..5086f9f 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -316,6 +316,7 @@ public class ChannelDirectoryService { ); } + @Transactional(rollbackFor = IOException.class) public Optional createAsset(String broadcaster, MultipartFile file, String actor) throws IOException { long fileSize = file.getSize(); if (fileSize > uploadLimitBytes) { @@ -412,6 +413,7 @@ public class ChannelDirectoryService { return Optional.of(view); } + @Transactional public Optional createCodeAsset(String broadcaster, CodeAssetRequest request, String actor) { validateCodeAssetSource(request.getSource()); Channel channel = getOrCreateChannel(broadcaster); @@ -459,6 +461,7 @@ public class ChannelDirectoryService { return Optional.of(view); } + @Transactional public Optional updateCodeAsset(String broadcaster, String assetId, CodeAssetRequest request, String actor) { validateCodeAssetSource(request.getSource()); String normalized = normalize(broadcaster); @@ -526,6 +529,7 @@ public class ChannelDirectoryService { }); } + @Transactional(rollbackFor = IOException.class) public Optional updateScriptLogo(String broadcaster, String assetId, MultipartFile file, String actor) throws IOException { Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId); @@ -576,6 +580,7 @@ public class ChannelDirectoryService { return Optional.of(view); } + @Transactional public Optional clearScriptLogo(String broadcaster, String assetId, String actor) { Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId); ScriptAsset script = scriptAssetRepository @@ -809,6 +814,7 @@ public class ChannelDirectoryService { } } + @Transactional public Optional importMarketplaceScript(String targetBroadcaster, String scriptId, String actor) { Optional seedScript = marketplaceScriptSeedLoader.findById(scriptId); Optional imported; @@ -1014,6 +1020,7 @@ public class ChannelDirectoryService { return SAFE_FILENAME.matcher(stripped).replaceAll("_"); } + @Transactional public Optional updateTransform(String broadcaster, String assetId, TransformRequest req, String actor) { String normalized = normalize(broadcaster); @@ -1240,6 +1247,7 @@ public class ChannelDirectoryService { }); } + @Transactional public Optional updateVisibility( String broadcaster, String assetId, @@ -1363,6 +1371,7 @@ public class ChannelDirectoryService { return loadScriptAttachments(normalize(broadcaster), asset.getId(), null); } + @Transactional(rollbackFor = IOException.class) public Optional createScriptAttachment( String broadcaster, String scriptAssetId, @@ -1446,6 +1455,7 @@ public class ChannelDirectoryService { return Optional.of(view); } + @Transactional public boolean deleteScriptAttachment( String broadcaster, String scriptAssetId,