diff --git a/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java b/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java index 352871dfe3..c72cb29c24 100644 --- a/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java +++ b/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java @@ -68,11 +68,13 @@ public Mono create(Comment comment) { .flatMap(commentSetting -> { if (Boolean.FALSE.equals(commentSetting.getEnable())) { return Mono.error( - new AccessDeniedException("The comment function has been turned off.")); + new AccessDeniedException("The comment function has been turned off.", + "problemDetail.comment.turnedOff", null)); } if (checkCommentOwner(comment, commentSetting.getSystemUserOnly())) { return Mono.error( - new AccessDeniedException("Allow system user comments only.")); + new AccessDeniedException("Allow only system users to comment.", + "problemDetail.comment.systemUsersOnly", null)); } if (comment.getSpec().getTop() == null) { diff --git a/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java b/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java index 59aff87e9e..4cff27e633 100644 --- a/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java +++ b/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java @@ -55,10 +55,12 @@ public Mono create(String commentName, Reply reply) { .map(commentSetting -> { if (Boolean.FALSE.equals(commentSetting.getEnable())) { throw new AccessDeniedException( - "The comment function has been turned off."); + "The comment function has been turned off.", + "problemDetail.comment.turnedOff", null); } if (checkReplyOwner(reply, commentSetting.getSystemUserOnly())) { - throw new AccessDeniedException("Allow system user reply only."); + throw new AccessDeniedException("Allow only system users to comment.", + "problemDetail.comment.systemUsersOnly", null); } reply.getSpec().setApproved( Boolean.FALSE.equals(commentSetting.getRequireReviewForNew())); diff --git a/src/main/java/run/halo/app/core/extension/attachment/endpoint/LocalAttachmentUploadHandler.java b/src/main/java/run/halo/app/core/extension/attachment/endpoint/LocalAttachmentUploadHandler.java index 002c562614..6e97211efe 100644 --- a/src/main/java/run/halo/app/core/extension/attachment/endpoint/LocalAttachmentUploadHandler.java +++ b/src/main/java/run/halo/app/core/extension/attachment/endpoint/LocalAttachmentUploadHandler.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -26,6 +27,7 @@ import run.halo.app.core.extension.attachment.Constant; import run.halo.app.core.extension.attachment.Policy; import run.halo.app.extension.Metadata; +import run.halo.app.infra.exception.AttachmentAlreadyExistsException; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.utils.JsonUtils; @@ -108,7 +110,9 @@ public Mono upload(UploadContext uploadOption) { attachment.setMetadata(metadata); attachment.setSpec(spec); return attachment; - })); + })) + .onErrorMap(FileAlreadyExistsException.class, + e -> new AttachmentAlreadyExistsException(e.getFile())); }); } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index d9aa78dd9c..108d6434cf 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -28,6 +28,8 @@ import run.halo.app.core.extension.User; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.exception.ExtensionNotFoundException; +import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.infra.utils.JsonUtils; @Component @@ -115,7 +117,9 @@ Mono me(ServerRequest request) { return ReactiveSecurityContextHolder.getContext() .flatMap(ctx -> { var name = ctx.getAuthentication().getName(); - return client.get(User.class, name); + return client.get(User.class, name) + .onErrorMap(ExtensionNotFoundException.class, + e -> new UserNotFoundException(name)); }) .flatMap(user -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) diff --git a/src/main/java/run/halo/app/core/extension/theme/ThemeServiceImpl.java b/src/main/java/run/halo/app/core/extension/theme/ThemeServiceImpl.java index 993695b4b0..285c0d3cfe 100644 --- a/src/main/java/run/halo/app/core/extension/theme/ThemeServiceImpl.java +++ b/src/main/java/run/halo/app/core/extension/theme/ThemeServiceImpl.java @@ -19,7 +19,6 @@ import java.util.zip.ZipInputStream; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.retry.RetryException; import org.springframework.stereotype.Service; @@ -39,8 +38,7 @@ import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Unstructured; import run.halo.app.infra.ThemeRootGetter; -import run.halo.app.infra.exception.AsyncRequestTimeoutException; -import run.halo.app.infra.exception.ThemeInstallationException; +import run.halo.app.infra.exception.ThemeUpgradeException; @Slf4j @Service @@ -80,9 +78,10 @@ public Mono upgrade(String themeName, InputStream is) { .flatMap(oldTheme -> { try (var zis = new ZipInputStream(is)) { unzip(zis, tempDir.get()); - return locateThemeManifest(tempDir.get()) - .switchIfEmpty(Mono.error(() -> new ThemeInstallationException( - "Missing theme manifest file: theme.yaml or theme.yml"))); + return locateThemeManifest(tempDir.get()).switchIfEmpty(Mono.error( + () -> new ThemeUpgradeException( + "Missing theme manifest file: theme.yaml or theme.yml", + "problemDetail.theme.upgrade.missingManifest", null))); } catch (IOException e) { return Mono.error(e); } @@ -100,7 +99,9 @@ public Mono upgrade(String themeName, InputStream is) { log.error("Want theme name: {}, but provided: {}", themeName, newTheme.getMetadata().getName()); } - throw new ServerWebInputException("please make sure the theme name is correct"); + throw new ThemeUpgradeException("Please make sure the theme name is correct", + "problemDetail.theme.upgrade.nameMismatch", + new Object[] {newTheme.getMetadata().getName(), themeName}); } }) .flatMap(newTheme -> { @@ -182,18 +183,7 @@ public Mono reloadTheme(String name) { .flatMap(oldTheme -> { String settingName = oldTheme.getSpec().getSettingName(); return waitForSettingDeleted(settingName) - .doOnError(error -> { - log.error("Failed to delete setting: {}", settingName, - ExceptionUtils.getRootCause(error)); - throw new AsyncRequestTimeoutException("Reload theme timeout."); - }) - .then(waitForAnnotationSettingsDeleted(name) - .doOnError(error -> { - log.error("Failed to delete AnnotationSetting by theme [{}]", name, - ExceptionUtils.getRootCause(error)); - throw new AsyncRequestTimeoutException("Reload theme timeout."); - }) - ); + .then(waitForAnnotationSettingsDeleted(name)); }) .then(Mono.defer(() -> { Path themePath = themeRoot.get().resolve(name); @@ -261,9 +251,8 @@ private Mono waitForSettingDeleted(String settingName) { return client.fetch(Setting.class, settingName) .flatMap(setting -> client.delete(setting) .flatMap(deleted -> client.fetch(Setting.class, settingName) - .doOnNext(latest -> { - throw new RetryException("Setting is not deleted yet."); - }) + .flatMap(s -> Mono.error( + () -> new RetryException("Re-check if the setting is deleted."))) .retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100)) .filter(t -> t instanceof RetryException)) ) @@ -317,9 +306,9 @@ Mono waitForThemeDeleted(String themeName) { throw new RetryException("Re-check if the theme is deleted successfully"); }) .retryWhen(Retry.fixedDelay(20, Duration.ofMillis(100)) - .filter(t -> t instanceof RetryException)) - .onErrorMap(Exceptions::isRetryExhausted, - throwable -> new ServerErrorException("Wait timeout for theme deleted", throwable)) + .filter(t -> t instanceof RetryException) + .onRetryExhaustedThrow((spec, signal) -> + new ServerErrorException("Wait timeout for theme deleted", null))) .then(); } } diff --git a/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java b/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java index 3673385488..6aa86a3320 100644 --- a/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java +++ b/src/main/java/run/halo/app/core/extension/theme/ThemeUtils.java @@ -122,14 +122,16 @@ static Mono unzipThemeTo(InputStream inputStream, Path themeWorkDi }) .flatMap(is -> ThemeUtils.locateThemeManifest(tempDir.get())) .switchIfEmpty( - Mono.error(() -> new ThemeInstallationException("Missing theme manifest"))) + Mono.error(() -> new ThemeInstallationException("Missing theme manifest", + "problemDetail.theme.install.missingManifest", null))) .map(themeManifestPath -> { var theme = loadThemeManifest(themeManifestPath); var themeName = theme.getMetadata().getName(); var themeTargetPath = themeWorkDir.resolve(themeName); try { if (!override && !FileUtils.isEmpty(themeTargetPath)) { - throw new ThemeInstallationException("Theme already exists."); + throw new ThemeInstallationException("Theme already exists.", + "problemDetail.theme.install.alreadyExists", new Object[] {themeName}); } // install theme to theme work dir copyRecursively(themeManifestPath.getParent(), themeTargetPath); diff --git a/src/main/java/run/halo/app/extension/GroupVersionKind.java b/src/main/java/run/halo/app/extension/GroupVersionKind.java index 3066aa524d..2686ca8df2 100644 --- a/src/main/java/run/halo/app/extension/GroupVersionKind.java +++ b/src/main/java/run/halo/app/extension/GroupVersionKind.java @@ -49,7 +49,7 @@ public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String k return new GroupVersionKind(gv.group(), gv.version(), kind); } - public static GroupVersionKind fromExtension(Class extension) { + public static GroupVersionKind fromExtension(Class extension) { GVK gvk = extension.getAnnotation(GVK.class); return new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()); } diff --git a/src/main/java/run/halo/app/extension/JSONExtensionConverter.java b/src/main/java/run/halo/app/extension/JSONExtensionConverter.java index a61b7efc01..a77b3c4dc4 100644 --- a/src/main/java/run/halo/app/extension/JSONExtensionConverter.java +++ b/src/main/java/run/halo/app/extension/JSONExtensionConverter.java @@ -62,8 +62,8 @@ public ExtensionStore convertTo(E extension) { if (!validation.isValid()) { log.debug("Failed to validate Extension: {}, and errors were: {}", extension.getClass(), validation.results()); - throw new SchemaViolationException("Failed to validate Extension " - + extension.getClass(), validation.results()); + throw new SchemaViolationException(extension.groupVersionKind(), + validation.results()); } var version = extension.getMetadata().getVersion(); diff --git a/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java b/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java index 9afbf1cef0..741d024429 100644 --- a/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java +++ b/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java @@ -84,14 +84,15 @@ public Mono fetch(GroupVersionKind gvk, String name) { @Override public Mono get(Class type, String name) { return fetch(type, name) - .switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException( - "Extension " + type.getName() + " with name " + name + " not found"))); + .switchIfEmpty(Mono.error(() -> { + var gvk = GroupVersionKind.fromExtension(type); + return new ExtensionNotFoundException(gvk, name); + })); } private Mono get(GroupVersionKind gvk, String name) { return fetch(gvk, name) - .switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException( - "Extension " + gvk + " with name " + name + " not found"))); + .switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(gvk, name))); } @Override diff --git a/src/main/java/run/halo/app/extension/Scheme.java b/src/main/java/run/halo/app/extension/Scheme.java index a50c627332..db1e2ebf83 100644 --- a/src/main/java/run/halo/app/extension/Scheme.java +++ b/src/main/java/run/halo/app/extension/Scheme.java @@ -68,11 +68,8 @@ public static Scheme buildFromType(Class type) { @NonNull public static GVK getGvkFromType(@NonNull Class type) { var gvk = type.getAnnotation(GVK.class); - if (gvk == null) { - throw new ExtensionException( - String.format("Annotation %s needs to be on Extension %s", GVK.class.getName(), - type.getName())); - } + Assert.notNull(gvk, + "Missing annotation " + GVK.class.getName() + " on type " + type.getName()); return gvk; } } diff --git a/src/main/java/run/halo/app/extension/SchemeManager.java b/src/main/java/run/halo/app/extension/SchemeManager.java index 041c776e24..993a222053 100644 --- a/src/main/java/run/halo/app/extension/SchemeManager.java +++ b/src/main/java/run/halo/app/extension/SchemeManager.java @@ -40,7 +40,7 @@ default Optional fetch(@NonNull GroupVersionKind gvk) { @NonNull default Scheme get(@NonNull GroupVersionKind gvk) { return fetch(gvk).orElseThrow( - () -> new SchemeNotFoundException("Scheme was not found for " + gvk)); + () -> new SchemeNotFoundException(gvk)); } @NonNull diff --git a/src/main/java/run/halo/app/extension/exception/ExtensionConvertException.java b/src/main/java/run/halo/app/extension/exception/ExtensionConvertException.java index 406a4cd993..109d15329b 100644 --- a/src/main/java/run/halo/app/extension/exception/ExtensionConvertException.java +++ b/src/main/java/run/halo/app/extension/exception/ExtensionConvertException.java @@ -7,23 +7,11 @@ */ public class ExtensionConvertException extends ExtensionException { - public ExtensionConvertException() { + public ExtensionConvertException(String reason) { + super(reason); } - public ExtensionConvertException(String message) { - super(message); - } - - public ExtensionConvertException(String message, Throwable cause) { - super(message, cause); - } - - public ExtensionConvertException(Throwable cause) { - super(cause); - } - - public ExtensionConvertException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); + public ExtensionConvertException(String reason, Throwable cause) { + super(reason, cause); } } diff --git a/src/main/java/run/halo/app/extension/exception/ExtensionException.java b/src/main/java/run/halo/app/extension/exception/ExtensionException.java index 66ad8d6999..722297446e 100644 --- a/src/main/java/run/halo/app/extension/exception/ExtensionException.java +++ b/src/main/java/run/halo/app/extension/exception/ExtensionException.java @@ -1,30 +1,26 @@ package run.halo.app.extension.exception; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.web.server.ResponseStatusException; + /** * ExtensionException is the superclass of those exceptions that can be thrown by Extension module. * * @author johnniang */ -public class ExtensionException extends RuntimeException { - - public ExtensionException() { - } +public class ExtensionException extends ResponseStatusException { - public ExtensionException(String message) { - super(message); + public ExtensionException(String reason) { + this(reason, null); } - public ExtensionException(String message, Throwable cause) { - super(message, cause); + public ExtensionException(String reason, Throwable cause) { + this(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause, null, new Object[] {reason}); } - public ExtensionException(Throwable cause) { - super(cause); + protected ExtensionException(HttpStatusCode status, String reason, Throwable cause, + String messageDetailCode, Object[] messageDetailArguments) { + super(status, reason, cause, messageDetailCode, messageDetailArguments); } - - public ExtensionException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - } diff --git a/src/main/java/run/halo/app/extension/exception/ExtensionNotFoundException.java b/src/main/java/run/halo/app/extension/exception/ExtensionNotFoundException.java index 08e4c2ed29..a298704fed 100644 --- a/src/main/java/run/halo/app/extension/exception/ExtensionNotFoundException.java +++ b/src/main/java/run/halo/app/extension/exception/ExtensionNotFoundException.java @@ -1,24 +1,13 @@ package run.halo.app.extension.exception; -public class ExtensionNotFoundException extends ExtensionException { - - public ExtensionNotFoundException() { - } - - public ExtensionNotFoundException(String message) { - super(message); - } +import org.springframework.http.HttpStatus; +import run.halo.app.extension.GroupVersionKind; - public ExtensionNotFoundException(String message, Throwable cause) { - super(message, cause); - } +public class ExtensionNotFoundException extends ExtensionException { - public ExtensionNotFoundException(Throwable cause) { - super(cause); + public ExtensionNotFoundException(GroupVersionKind gvk, String name) { + super(HttpStatus.NOT_FOUND, "Extension " + gvk + "/" + name + " was not found.", + null, null, new Object[] {gvk, name}); } - public ExtensionNotFoundException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } } diff --git a/src/main/java/run/halo/app/extension/exception/SchemaViolationException.java b/src/main/java/run/halo/app/extension/exception/SchemaViolationException.java index acae0de9ef..b1697a01a7 100644 --- a/src/main/java/run/halo/app/extension/exception/SchemaViolationException.java +++ b/src/main/java/run/halo/app/extension/exception/SchemaViolationException.java @@ -1,6 +1,8 @@ package run.halo.app.extension.exception; import org.openapi4j.core.validation.ValidationResults; +import org.springframework.http.HttpStatus; +import run.halo.app.extension.GroupVersionKind; /** * This exception is thrown when Schema is violation. @@ -14,28 +16,9 @@ public class SchemaViolationException extends ExtensionException { */ private final ValidationResults errors; - public SchemaViolationException(ValidationResults errors) { - this.errors = errors; - } - - public SchemaViolationException(String message, ValidationResults errors) { - super(message); - this.errors = errors; - } - - public SchemaViolationException(String message, Throwable cause, ValidationResults errors) { - super(message, cause); - this.errors = errors; - } - - public SchemaViolationException(Throwable cause, ValidationResults errors) { - super(cause); - this.errors = errors; - } - - public SchemaViolationException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace, ValidationResults errors) { - super(message, cause, enableSuppression, writableStackTrace); + public SchemaViolationException(GroupVersionKind gvk, ValidationResults errors) { + super(HttpStatus.BAD_REQUEST, "Failed to validate " + gvk, null, null, + new Object[] {gvk, errors}); this.errors = errors; } diff --git a/src/main/java/run/halo/app/extension/exception/SchemeNotFoundException.java b/src/main/java/run/halo/app/extension/exception/SchemeNotFoundException.java index 9eb52ff087..ee6b01d6d6 100644 --- a/src/main/java/run/halo/app/extension/exception/SchemeNotFoundException.java +++ b/src/main/java/run/halo/app/extension/exception/SchemeNotFoundException.java @@ -1,5 +1,8 @@ package run.halo.app.extension.exception; +import org.springframework.http.HttpStatus; +import run.halo.app.extension.GroupVersionKind; + /** * SchemeNotFoundException is thrown while we try to get a scheme but not found. * @@ -7,23 +10,9 @@ */ public class SchemeNotFoundException extends ExtensionException { - public SchemeNotFoundException() { - } - - public SchemeNotFoundException(String message) { - super(message); - } - - public SchemeNotFoundException(String message, Throwable cause) { - super(message, cause); - } - - public SchemeNotFoundException(Throwable cause) { - super(cause); + public SchemeNotFoundException(GroupVersionKind gvk) { + super(HttpStatus.INTERNAL_SERVER_ERROR, "Scheme not found for " + gvk, null, null, + new Object[] {gvk}); } - public SchemeNotFoundException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } } diff --git a/src/main/java/run/halo/app/infra/exception/AccessDeniedException.java b/src/main/java/run/halo/app/infra/exception/AccessDeniedException.java index 8a111eea6d..2b1911d6fa 100644 --- a/src/main/java/run/halo/app/infra/exception/AccessDeniedException.java +++ b/src/main/java/run/halo/app/infra/exception/AccessDeniedException.java @@ -1,24 +1,24 @@ package run.halo.app.infra.exception; -public class AccessDeniedException extends HaloException { +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; - public AccessDeniedException() { - } +/** + * AccessDeniedException will resolve i18n message and response 403 status. + * + * @author johnniang + */ +public class AccessDeniedException extends ResponseStatusException { - public AccessDeniedException(String message) { - super(message); - } - - public AccessDeniedException(String message, Throwable cause) { - super(message, cause); + public AccessDeniedException() { + this("Access to the resource is forbidden"); } - public AccessDeniedException(Throwable cause) { - super(cause); + public AccessDeniedException(String reason) { + this(reason, null, null); } - public AccessDeniedException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); + public AccessDeniedException(String reason, String detailCode, Object[] detailArgs) { + super(HttpStatus.FORBIDDEN, reason, null, detailCode, detailArgs); } } diff --git a/src/main/java/run/halo/app/infra/exception/AsyncRequestTimeoutException.java b/src/main/java/run/halo/app/infra/exception/AsyncRequestTimeoutException.java deleted file mode 100644 index e7b1565062..0000000000 --- a/src/main/java/run/halo/app/infra/exception/AsyncRequestTimeoutException.java +++ /dev/null @@ -1,49 +0,0 @@ -package run.halo.app.infra.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ProblemDetail; -import org.springframework.lang.NonNull; -import org.springframework.web.ErrorResponse; - -/** - *

Exception to be thrown when an async request times out.

- * By default the exception will be handled as a {@link HttpStatus#REQUEST_TIMEOUT} error. - * - * @author guqing - * @since 2.0.0 - */ -public class AsyncRequestTimeoutException extends RuntimeException implements ErrorResponse { - public AsyncRequestTimeoutException() { - super(); - } - - public AsyncRequestTimeoutException(String message) { - super(message); - } - - public AsyncRequestTimeoutException(String message, Throwable cause) { - super(message, cause); - } - - public AsyncRequestTimeoutException(Throwable cause) { - super(cause); - } - - protected AsyncRequestTimeoutException(String message, Throwable cause, - boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - - @Override - @NonNull - public HttpStatusCode getStatusCode() { - return HttpStatus.REQUEST_TIMEOUT; - } - - @Override - @NonNull - public ProblemDetail getBody() { - return ProblemDetail.forStatusAndDetail(getStatusCode(), getMessage()); - } -} diff --git a/src/main/java/run/halo/app/infra/exception/AttachmentAlreadyExistsException.java b/src/main/java/run/halo/app/infra/exception/AttachmentAlreadyExistsException.java new file mode 100644 index 0000000000..2bcda1c6e7 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/AttachmentAlreadyExistsException.java @@ -0,0 +1,16 @@ +package run.halo.app.infra.exception; + +import org.springframework.web.server.ServerWebInputException; + +/** + * AttachmentAlreadyExistsException accepts filename parameter as detail message arguments. + * + * @author johnniang + */ +public class AttachmentAlreadyExistsException extends ServerWebInputException { + + public AttachmentAlreadyExistsException(String filename) { + super("File " + filename + " already exists.", null, null, null, new Object[] {filename}); + } + +} diff --git a/src/main/java/run/halo/app/infra/exception/HaloException.java b/src/main/java/run/halo/app/infra/exception/HaloException.java deleted file mode 100644 index dfb781fd12..0000000000 --- a/src/main/java/run/halo/app/infra/exception/HaloException.java +++ /dev/null @@ -1,24 +0,0 @@ -package run.halo.app.infra.exception; - -public class HaloException extends RuntimeException { - - public HaloException() { - } - - public HaloException(String message) { - super(message); - } - - public HaloException(String message, Throwable cause) { - super(message, cause); - } - - public HaloException(Throwable cause) { - super(cause); - } - - public HaloException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/src/main/java/run/halo/app/infra/exception/NotFoundException.java b/src/main/java/run/halo/app/infra/exception/NotFoundException.java index 2e9936329d..b7ff8c5ebf 100644 --- a/src/main/java/run/halo/app/infra/exception/NotFoundException.java +++ b/src/main/java/run/halo/app/infra/exception/NotFoundException.java @@ -6,7 +6,7 @@ * @author guqing * @since 2.0.0 */ -public class NotFoundException extends HaloException { +public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); } diff --git a/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java b/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java index 787204035e..2be1145285 100644 --- a/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java +++ b/src/main/java/run/halo/app/infra/exception/ThemeInstallationException.java @@ -1,15 +1,17 @@ package run.halo.app.infra.exception; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + /** * @author guqing + * @author johnniang * @since 2.0.0 */ -public class ThemeInstallationException extends HaloException { - public ThemeInstallationException(String message) { - super(message); - } +public class ThemeInstallationException extends ResponseStatusException { - public ThemeInstallationException(String message, Throwable cause) { - super(message, cause); + public ThemeInstallationException(String reason, String detailCode, Object[] detailArgs) { + super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs); } + } diff --git a/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java b/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java index c04af41317..fd337bab28 100644 --- a/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java +++ b/src/main/java/run/halo/app/infra/exception/ThemeUninstallException.java @@ -4,7 +4,7 @@ * @author guqing * @since 2.0.0 */ -public class ThemeUninstallException extends HaloException { +public class ThemeUninstallException extends RuntimeException { public ThemeUninstallException(String message) { super(message); diff --git a/src/main/java/run/halo/app/infra/exception/ThemeUpgradeException.java b/src/main/java/run/halo/app/infra/exception/ThemeUpgradeException.java new file mode 100644 index 0000000000..c040eb10de --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/ThemeUpgradeException.java @@ -0,0 +1,17 @@ +package run.halo.app.infra.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +/** + * ThemeUpgradeException will response bad request status if failed to upgrade theme. + * + * @author johnniang + */ +public class ThemeUpgradeException extends ResponseStatusException { + + public ThemeUpgradeException(String reason, String detailCode, Object[] detailArgs) { + super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs); + } + +} diff --git a/src/main/java/run/halo/app/infra/exception/UserNotFoundException.java b/src/main/java/run/halo/app/infra/exception/UserNotFoundException.java new file mode 100644 index 0000000000..2142bef4f2 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/UserNotFoundException.java @@ -0,0 +1,13 @@ +package run.halo.app.infra.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public class UserNotFoundException extends ResponseStatusException { + + public UserNotFoundException(String username) { + super(HttpStatus.NOT_FOUND, "User " + username + " was not found", null, null, + new Object[] {username}); + } + +} diff --git a/src/main/java/run/halo/app/infra/exception/handlers/ExceptionHandlingProblemDetailsHandler.java b/src/main/java/run/halo/app/infra/exception/handlers/ExceptionHandlingProblemDetailsHandler.java deleted file mode 100644 index 09599a3697..0000000000 --- a/src/main/java/run/halo/app/infra/exception/handlers/ExceptionHandlingProblemDetailsHandler.java +++ /dev/null @@ -1,113 +0,0 @@ -package run.halo.app.infra.exception.handlers; - -import java.net.URI; -import java.util.List; -import java.util.stream.Collectors; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ProblemDetail; -import org.springframework.lang.Nullable; -import org.springframework.web.ErrorResponse; -import org.springframework.web.ErrorResponseException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.server.ServerWebExchange; -import run.halo.app.extension.exception.ExtensionConvertException; -import run.halo.app.extension.exception.ExtensionNotFoundException; -import run.halo.app.extension.exception.SchemaViolationException; -import run.halo.app.extension.exception.SchemeNotFoundException; -import run.halo.app.infra.exception.AccessDeniedException; -import run.halo.app.infra.exception.NotFoundException; -import run.halo.app.infra.exception.ThemeInstallationException; -import run.halo.app.infra.exception.ThemeUninstallException; - -/** - * Handle exceptions and convert them to {@link ProblemDetail}. - * - * @author guqing - * @since 2.0.0 - */ -public class ExceptionHandlingProblemDetailsHandler { - - @ExceptionHandler({SchemeNotFoundException.class, ExtensionNotFoundException.class, - NotFoundException.class}) - public ProblemDetail handleNotFoundException(Throwable error, - ServerWebExchange exchange) { - return createProblemDetail(error, HttpStatus.NOT_FOUND, - error.getMessage(), exchange); - } - - @ExceptionHandler(SchemaViolationException.class) - public ProblemDetail handleSchemaViolationException(SchemaViolationException exception, - ServerWebExchange exchange) { - List invalidParams = - exception.getErrors().items() - .stream() - .map(item -> new InvalidParam(item.dataCrumbs(), - item.message()) - ) - .collect(Collectors.toList()); - ProblemDetail problemDetail = createProblemDetail(exception, HttpStatus.BAD_REQUEST, - exception.getMessage(), exchange); - problemDetail.setTitle("Your request parameters didn't validate."); - problemDetail.setProperty("invalidParams", invalidParams); - return problemDetail; - } - - @ExceptionHandler({ExtensionConvertException.class, ThemeInstallationException.class, - ThemeUninstallException.class, IllegalArgumentException.class, IllegalStateException.class}) - public ProblemDetail handleBadRequestException(Throwable error, - ServerWebExchange exchange) { - return createProblemDetail(error, HttpStatus.BAD_REQUEST, - error.getMessage(), exchange); - } - - @ExceptionHandler(AccessDeniedException.class) - public ProblemDetail handleAccessDeniedException(AccessDeniedException e, - ServerWebExchange exchange) { - return createProblemDetail(e, HttpStatus.FORBIDDEN, e.getMessage(), exchange); - } - - @ExceptionHandler(OptimisticLockingFailureException.class) - public ProblemDetail handleOptimisticLockingFailureException( - OptimisticLockingFailureException e, ServerWebExchange exchange) { - return createProblemDetail(e, HttpStatus.CONFLICT, - e.getMessage(), exchange); - } - - public record InvalidParam(String name, String reason) { - } - - protected ProblemDetail createProblemDetail(Throwable ex, HttpStatusCode status, - String defaultDetail, ServerWebExchange exchange) { - - ErrorResponse response = createFor( - ex, status, null, defaultDetail, null, null); - - ProblemDetail problemDetail = response.getBody(); - problemDetail.setInstance(URI.create(exchange.getRequest().getPath() - .pathWithinApplication().value())); - return problemDetail; - } - - static ErrorResponse createFor( - Throwable ex, HttpStatusCode status, @Nullable HttpHeaders headers, - String defaultDetail, @Nullable String detailMessageCode, - @Nullable Object[] detailMessageArguments) { - - if (detailMessageCode == null) { - detailMessageCode = ErrorResponse.getDefaultDetailMessageCode(ex.getClass(), null); - } - - ErrorResponseException errorResponse = new ErrorResponseException( - status, ProblemDetail.forStatusAndDetail(status, defaultDetail), null, - detailMessageCode, detailMessageArguments); - - if (headers != null) { - errorResponse.getHeaders().putAll(headers); - } - - return errorResponse; - } -} diff --git a/src/main/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandler.java b/src/main/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandler.java deleted file mode 100644 index 79ceed3efe..0000000000 --- a/src/main/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandler.java +++ /dev/null @@ -1,161 +0,0 @@ -package run.halo.app.infra.exception.handlers; - -import java.lang.reflect.Method; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.WebProperties; -import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; -import org.springframework.boot.web.reactive.error.ErrorAttributes; -import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.http.ProblemDetail; -import org.springframework.web.ErrorResponse; -import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; -import org.springframework.web.reactive.BindingContext; -import org.springframework.web.reactive.function.BodyInserters; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.reactive.result.method.InvocableHandlerMethod; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import run.halo.app.theme.ThemeResolver; - -/** - * Global error web exception handler. - * - * @author guqing - * @see DefaultErrorWebExceptionHandler - * @see ExceptionHandlingProblemDetailsHandler - * @see ExceptionHandlerMethodResolver - * @since 2.0.0 - */ -@Slf4j -public class GlobalErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { - private static final MediaType TEXT_HTML_UTF8 = - new MediaType("text", "html", StandardCharsets.UTF_8); - - private static final Map SERIES_VIEWS; - - private final ExceptionHandlingProblemDetailsHandler exceptionHandler = - new ExceptionHandlingProblemDetailsHandler(); - private final ExceptionHandlerMethodResolver handlerMethodResolver = - new ExceptionHandlerMethodResolver(ExceptionHandlingProblemDetailsHandler.class); - - private final ErrorProperties errorProperties; - - private final ThemeResolver themeResolver; - - static { - Map views = new EnumMap<>(HttpStatus.Series.class); - views.put(HttpStatus.Series.CLIENT_ERROR, "4xx"); - views.put(HttpStatus.Series.SERVER_ERROR, "5xx"); - SERIES_VIEWS = Collections.unmodifiableMap(views); - } - - /** - * Create a new {@code DefaultErrorWebExceptionHandler} instance. - * - * @param errorAttributes the error attributes - * @param resources the resources configuration properties - * @param errorProperties the error configuration properties - * @param applicationContext the current application context - * @since 2.4.0 - */ - public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, - WebProperties.Resources resources, - ErrorProperties errorProperties, - ApplicationContext applicationContext) { - super(errorAttributes, resources, errorProperties, applicationContext); - this.errorProperties = errorProperties; - this.themeResolver = applicationContext.getBean(ThemeResolver.class); - } - - @Override - protected Mono renderErrorResponse(ServerRequest request) { - Throwable error = getError(request); - - if (error instanceof ErrorResponse errorResponse) { - return ServerResponse.status(errorResponse.getStatusCode()) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(errorResponse.getBody())); - } - Method exceptionHandlerMethod = handlerMethodResolver.resolveMethodByThrowable(error); - if (exceptionHandlerMethod == null) { - return noMatchExceptionHandler(error); - } - - InvocableHandlerMethod invocable = - new InvocableHandlerMethod(exceptionHandler, exceptionHandlerMethod); - BindingContext bindingContext = new BindingContext(); - ServerWebExchange exchange = request.exchange(); - return invocable.invoke(exchange, bindingContext, error, exchange) - .mapNotNull(handleResult -> (ProblemDetail) handleResult.getReturnValue()) - .flatMap(problemDetail -> ServerResponse.status(problemDetail.getStatus()) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(problemDetail))) - .switchIfEmpty(Mono.defer(() -> noMatchExceptionHandler(error))); - } - - protected Mono renderErrorView(ServerRequest request) { - Map errorAttributes = - getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)); - int errorStatus = getHttpStatus(errorAttributes); - - ProblemDetail problemDetail = - ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(errorStatus), - (String) errorAttributes.get("message")); - problemDetail.setInstance(URI.create(request.path())); - Map error = Map.of("error", problemDetail); - - ServerResponse.BodyBuilder responseBody = - ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8); - return Flux.just(getData(errorStatus).toArray(new String[] {})) - .flatMap((viewName) -> renderErrorViewBy(request, viewName, responseBody, error)) - .switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled() - ? renderDefaultErrorView(responseBody, error) : Mono.error(getError(request))) - .next(); - } - - private Mono renderErrorViewBy(ServerRequest request, String viewName, - ServerResponse.BodyBuilder responseBody, - Map error) { - return themeResolver.isTemplateAvailable(request.exchange().getRequest(), viewName) - .flatMap(isAvailable -> { - if (isAvailable) { - return responseBody.render(viewName, error); - } - return super.renderErrorView(viewName, responseBody, error); - }); - } - - private List getData(int errorStatus) { - List data = new ArrayList<>(); - data.add("error/" + errorStatus); - HttpStatus.Series series = HttpStatus.Series.resolve(errorStatus); - if (series != null) { - data.add("error/" + SERIES_VIEWS.get(series)); - } - data.add("error/error"); - return data; - } - - Mono noMatchExceptionHandler(Throwable error) { - return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) - .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue( - ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, - error.getMessage()) - ) - ); - } -} diff --git a/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorConfiguration.java b/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorConfiguration.java new file mode 100644 index 0000000000..a59c298894 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorConfiguration.java @@ -0,0 +1,64 @@ +package run.halo.app.infra.exception.handlers; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.context.ApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.web.reactive.result.view.ViewResolver; + +/** + * Configuration to render errors via a WebFlux + * {@link org.springframework.web.server.WebExceptionHandler}. + *
+ *
+ * See + * {@link org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration} + * for more. + * + * @author guqing + * @author johnniang + * @since 2.1.0 + */ +@Configuration +public class HaloErrorConfiguration { + + /** + * This bean will replace ErrorWebExceptionHandler defined at + * {@link ErrorWebFluxAutoConfiguration#errorWebExceptionHandler}. + */ + @Bean + @Order(-1) + ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, + WebProperties webProperties, + ObjectProvider viewResolvers, + ServerCodecConfigurer serverCodecConfigurer, + ApplicationContext applicationContext, + ServerProperties serverProperties) { + var exceptionHandler = new HaloErrorWebExceptionHandler( + errorAttributes, + webProperties.getResources(), + serverProperties.getError(), + applicationContext); + exceptionHandler.setViewResolvers(viewResolvers.orderedStream().toList()); + exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); + exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); + return exceptionHandler; + } + + /** + * This bean will replace ErrorAttributes defined at + * {@link ErrorWebFluxAutoConfiguration#errorAttributes}. + */ + @Bean + ErrorAttributes errorAttributes(MessageSource messageSource) { + return new ProblemDetailErrorAttributes(messageSource); + } +} diff --git a/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebExceptionHandler.java b/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebExceptionHandler.java new file mode 100644 index 0000000000..1affe59008 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebExceptionHandler.java @@ -0,0 +1,48 @@ +package run.halo.app.infra.exception.handlers; + +import java.util.Map; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.ApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.http.ProblemDetail; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +public class HaloErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { + + /** + * Create a new {@code DefaultErrorWebExceptionHandler} instance. + * + * @param errorAttributes the error attributes + * @param resources the resources configuration properties + * @param errorProperties the error configuration properties + * @param applicationContext the current application context + * @since 2.4.0 + */ + public HaloErrorWebExceptionHandler( + ErrorAttributes errorAttributes, + WebProperties.Resources resources, + ErrorProperties errorProperties, + ApplicationContext applicationContext) { + super(errorAttributes, resources, errorProperties, applicationContext); + } + + @Override + protected int getHttpStatus(Map errorAttributes) { + var problemDetail = (ProblemDetail) errorAttributes.get("error"); + return problemDetail.getStatus(); + } + + @Override + protected Mono renderErrorResponse(ServerRequest request) { + var errorAttributes = + getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); + return ServerResponse.status(getHttpStatus(errorAttributes)) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .bodyValue(errorAttributes.get("error")); + } +} diff --git a/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebFluxAutoConfiguration.java b/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebFluxAutoConfiguration.java deleted file mode 100644 index d535a6de41..0000000000 --- a/src/main/java/run/halo/app/infra/exception/handlers/HaloErrorWebFluxAutoConfiguration.java +++ /dev/null @@ -1,54 +0,0 @@ -package run.halo.app.infra.exception.handlers; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.WebProperties; -import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.reactive.error.ErrorAttributes; -import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.web.reactive.result.view.ViewResolver; - -/** - * Global exception handler auto configuration. - * - * @author guqing - * @see GlobalErrorWebExceptionHandler - * @see - * getErrorAttributes(ServerRequest request, + ErrorAttributeOptions options) { + var errAttributes = new LinkedHashMap(); + + var error = getError(request); + var responseStatusAnno = from(error.getClass(), SearchStrategy.TYPE_HIERARCHY) + .get(ResponseStatus.class); + + var status = determineHttpStatus(error, responseStatusAnno); + final ErrorResponse errorResponse; + if (error instanceof ErrorResponse er) { + errorResponse = er; + } else { + var reason = responseStatusAnno.getValue("reason", String.class) + .orElse(error.getMessage()); + errorResponse = ErrorResponse.create(error, status, reason); + } + var problemDetail = + errorResponse.updateAndGetBody(messageSource, getLocale(request.exchange())); + + problemDetail.setInstance(URI.create(request.path())); + problemDetail.setProperty("requestId", request.exchange().getRequest().getId()); + problemDetail.setProperty("timestamp", Instant.now()); + + // For backward compatibility(rendering view need) + errAttributes.put("error", problemDetail); + return errAttributes; + } + + private HttpStatusCode determineHttpStatus(Throwable t, + MergedAnnotation responseStatusAnno) { + if (t instanceof ErrorResponse rse) { + return rse.getStatusCode(); + } + return responseStatusAnno.getValue("code", HttpStatus.class) + .orElse(HttpStatus.INTERNAL_SERVER_ERROR); + } + + private Locale getLocale(ServerWebExchange exchange) { + var locale = exchange.getLocaleContext().getLocale(); + return locale != null ? locale : Locale.getDefault(); + } + + @Override + public Throwable getError(ServerRequest request) { + return (Throwable) request.attribute(ERROR_INTERNAL_ATTRIBUTE).stream() + .peek(error -> request.attributes().putIfAbsent(ERROR_ATTRIBUTE, error)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Missing exception attribute in ServerWebExchange")); + } + + @Override + public void storeErrorInformation(Throwable error, ServerWebExchange exchange) { + exchange.getAttributes().putIfAbsent(ERROR_INTERNAL_ATTRIBUTE, error); + } + + +} diff --git a/src/main/java/run/halo/app/infra/utils/FileUtils.java b/src/main/java/run/halo/app/infra/utils/FileUtils.java index 8df1f42753..be7740135c 100644 --- a/src/main/java/run/halo/app/infra/utils/FileUtils.java +++ b/src/main/java/run/halo/app/infra/utils/FileUtils.java @@ -194,7 +194,8 @@ public static void checkDirectoryTraversal(@NonNull Path parentPath, return; } - throw new AccessDeniedException(pathToCheck.toString()); + throw new AccessDeniedException("Directory traversal detected: " + pathToCheck, + "problemDetail.directoryTraversal", new Object[] {parentPath, pathToCheck}); } /** diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 1ff0b8e292..b9390b993f 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -28,6 +28,7 @@ halo: logging: level: run.halo.app: DEBUG + org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler: DEBUG springdoc: api-docs: enabled: true diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b0a0f848e3..fdf547946f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,6 +2,9 @@ server: port: 8090 compression: enabled: true + error: + whitelabel: + enabled: false spring: output: ansi: @@ -16,6 +19,8 @@ spring: platform: h2 codec: max-in-memory-size: 10MB + messages: + basename: config.i18n.messages halo: external-url: "http://${server.address:localhost}:${server.port}" diff --git a/src/main/resources/config/i18n/messages.properties b/src/main/resources/config/i18n/messages.properties new file mode 100644 index 0000000000..127f178992 --- /dev/null +++ b/src/main/resources/config/i18n/messages.properties @@ -0,0 +1,36 @@ +# Title definitions +problemDetail.title.org.springframework.web.server.ServerWebInputException=Bad Request +problemDetail.title.org.springframework.web.server.UnsupportedMediaTypeStatusException=Unsupported Media Type +problemDetail.title.org.springframework.web.server.MissingRequestValueException=Missing Request Value +problemDetail.title.org.springframework.web.server.UnsatisfiedRequestParameterException=Unsatisfied Request Parameter +problemDetail.title.org.springframework.web.bind.support.WebExchangeBindException=Data Binding or Validation Failure +problemDetail.title.org.springframework.web.server.NotAcceptableStatusException=Not Acceptable +problemDetail.title.org.springframework.web.server.ServerErrorException=Server Error +problemDetail.title.org.springframework.web.server.MethodNotAllowedException=Method Not Allowed +problemDetail.title.run.halo.app.extension.exception.SchemaViolationException=Schema Violation +problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=Attachment Already Exists +problemDetail.title.run.halo.app.infra.exception.AccessDeniedException=Access Denied +problemDetail.title.reactor.core.Exceptions.RetryExhaustedException=Retry Exhausted +problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=Theme Install Error +problemDetail.title.run.halo.app.infra.exception.ThemeUpgradeException=Theme Upgrade Error + +# Detail definitions +problemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException=Content type {0} is not supported. Supported media types: {1}. +problemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException.parseError=Could not parse Content-Type. +problemDetail.org.springframework.web.server.MissingRequestValueException=Required {0} '{1}' is not present. +problemDetail.org.springframework.web.server.UnsatisfiedRequestParameterException=Parameter conditions "{0}" not met for actual request parameters. +problemDetail.org.springframework.web.bind.support.WebExchangeBindException=Invalid request content. Global errors: {0}. Field errors: {1}. +problemDetail.org.springframework.web.server.NotAcceptableStatusException=Acceptable representations: {0}. +problemDetail.org.springframework.web.server.NotAcceptableStatusException.parseError=Could not parse Accept header. +problemDetail.org.springframework.web.server.ServerErrorException={0}. +problemDetail.org.springframework.web.server.MethodNotAllowedException=Request method {0} is not supported. Supported methods: {1}. +problemDetail.run.halo.app.extension.exception.SchemaViolationException={1} of schema {0}. +problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=File {0} already exists, please rename it and try again. + +problemDetail.comment.turnedOff=The comment function has been turned off. +problemDetail.comment.systemUsersOnly=Allow only system users to comment +problemDetail.theme.upgrade.missingManifest=Missing theme manifest file "theme.yaml" or "theme.yml". +problemDetail.theme.upgrade.nameMismatch=The current theme name {0} did not match the installed theme name. +problemDetail.theme.install.missingManifest=Missing theme manifest file "theme.yaml" or "theme.yml". +problemDetail.theme.install.alreadyExists=Theme {0} already exists. +problemDetail.directoryTraversal=Directory traversal detected. Base path is {0}, but real path is {1}. diff --git a/src/main/resources/config/i18n/messages_zh.properties b/src/main/resources/config/i18n/messages_zh.properties new file mode 100644 index 0000000000..22682218db --- /dev/null +++ b/src/main/resources/config/i18n/messages_zh.properties @@ -0,0 +1,4 @@ +problemDetail.title.org.springframework.web.server.ServerWebInputException=请求参数有误 +problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=附件已存在 + +problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文件 {0} 已存在,建议更名后重试。 diff --git a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java index d6b06ff231..a00f6904cf 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import java.util.List; import java.util.Set; @@ -72,7 +73,8 @@ class GetUserDetailTest { @Test void shouldResponseErrorIfUserNotFound() { when(client.get(User.class, "fake-user")) - .thenReturn(Mono.error(new ExtensionNotFoundException())); + .thenReturn(Mono.error( + new ExtensionNotFoundException(fromExtension(User.class), "fake-user"))); webClient.get().uri("/apis/api.console.halo.run/v1alpha1/users/-") .exchange() .expectStatus().isNotFound(); @@ -142,7 +144,8 @@ class GrantPermissionEndpointTest { void setUp() { when(client.list(same(RoleBinding.class), any(), any())).thenReturn(Flux.empty()); when(client.get(User.class, "fake-user")) - .thenReturn(Mono.error(new ExtensionNotFoundException())); + .thenReturn(Mono.error( + new ExtensionNotFoundException(fromExtension(User.class), "fake-user"))); } @Test diff --git a/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java b/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java index 6f07b677c2..a07c1cef50 100644 --- a/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java +++ b/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import java.util.List; import java.util.Set; @@ -51,7 +52,7 @@ class UserServiceImplTest { @Test void shouldThrowExceptionIfUserNotFoundInExtension() { when(client.get(User.class, "faker")).thenReturn( - Mono.error(new ExtensionNotFoundException())); + Mono.error(new ExtensionNotFoundException(fromExtension(User.class), "faker"))); StepVerifier.create(userService.getUser("faker")) .verifyError(ExtensionNotFoundException.class); @@ -275,7 +276,8 @@ void shouldDoNothingIfPasswordNotChanged() { @Test void shouldThrowExceptionIfUserNotFound() { when(client.get(User.class, "fake-user")) - .thenReturn(Mono.error(new ExtensionNotFoundException())); + .thenReturn(Mono.error( + new ExtensionNotFoundException(fromExtension(User.class), "fake-user"))); StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password")) .verifyError(ExtensionNotFoundException.class); @@ -301,7 +303,8 @@ class GrantRolesTest { @Test void shouldGetNotFoundIfUserNotFound() { when(client.get(User.class, "invalid-user")) - .thenReturn(Mono.error(new ExtensionNotFoundException())); + .thenReturn(Mono.error( + new ExtensionNotFoundException(fromExtension(User.class), "invalid-user"))); var grantRolesMono = userService.grantRoles("invalid-user", Set.of("fake-role")); StepVerifier.create(grantRolesMono) diff --git a/src/test/java/run/halo/app/extension/DefaultSchemeManagerTest.java b/src/test/java/run/halo/app/extension/DefaultSchemeManagerTest.java index f00f58c5f3..5b2fb12670 100644 --- a/src/test/java/run/halo/app/extension/DefaultSchemeManagerTest.java +++ b/src/test/java/run/halo/app/extension/DefaultSchemeManagerTest.java @@ -20,7 +20,6 @@ import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered; import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered; import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher; -import run.halo.app.extension.exception.ExtensionException; import run.halo.app.extension.exception.SchemeNotFoundException; @ExtendWith(MockitoExtension.class) @@ -37,7 +36,7 @@ void shouldThrowExceptionWhenNoGvkAnnotation() { class WithoutGvkExtension extends AbstractExtension { } - assertThrows(ExtensionException.class, + assertThrows(IllegalArgumentException.class, () -> schemeManager.register(WithoutGvkExtension.class)); } diff --git a/src/test/java/run/halo/app/extension/SchemeTest.java b/src/test/java/run/halo/app/extension/SchemeTest.java index e0072b806c..d74c37a67f 100644 --- a/src/test/java/run/halo/app/extension/SchemeTest.java +++ b/src/test/java/run/halo/app/extension/SchemeTest.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; -import run.halo.app.extension.exception.ExtensionException; class SchemeTest { @@ -40,9 +39,9 @@ void shouldThrowExceptionWhenTypeHasNoGvkAnno() { class NoGvkExtension extends AbstractExtension { } - assertThrows(ExtensionException.class, + assertThrows(IllegalArgumentException.class, () -> Scheme.getGvkFromType(NoGvkExtension.class)); - assertThrows(ExtensionException.class, + assertThrows(IllegalArgumentException.class, () -> Scheme.buildFromType(NoGvkExtension.class)); } diff --git a/src/test/java/run/halo/app/extension/router/ExtensionDeleteHandlerTest.java b/src/test/java/run/halo/app/extension/router/ExtensionDeleteHandlerTest.java index 6c00ae7156..52fe0bbbd3 100644 --- a/src/test/java/run/halo/app/extension/router/ExtensionDeleteHandlerTest.java +++ b/src/test/java/run/halo/app/extension/router/ExtensionDeleteHandlerTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -92,7 +93,8 @@ void shouldReturnErrorWhenExtensionNotFound() { .pathVariable("name", "my-fake") .build(); when(client.get(FakeExtension.class, "my-fake")).thenReturn( - Mono.error(new ExtensionNotFoundException())); + Mono.error( + new ExtensionNotFoundException(fromExtension(FakeExtension.class), "my-fake"))); var scheme = Scheme.buildFromType(FakeExtension.class); var deleteHandler = new ExtensionDeleteHandler(scheme, client); diff --git a/src/test/java/run/halo/app/extension/router/ExtensionGetHandlerTest.java b/src/test/java/run/halo/app/extension/router/ExtensionGetHandlerTest.java index e1e4fcd6a0..8cc4e430d3 100644 --- a/src/test/java/run/halo/app/extension/router/ExtensionGetHandlerTest.java +++ b/src/test/java/run/halo/app/extension/router/ExtensionGetHandlerTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,8 +65,8 @@ void shouldThrowExceptionWhenExtensionNotFound() { var serverRequest = MockServerRequest.builder() .pathVariable("name", "my-fake") .build(); - when(client.get(eq(FakeExtension.class), eq("my-fake"))).thenReturn( - Mono.error(new ExtensionNotFoundException())); + when(client.get(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Mono.error( + new ExtensionNotFoundException(fromExtension(FakeExtension.class), "my-fake"))); Mono responseMono = getHandler.handle(serverRequest); StepVerifier.create(responseMono) diff --git a/src/test/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandlerTest.java b/src/test/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandlerTest.java deleted file mode 100644 index 1163a0c844..0000000000 --- a/src/test/java/run/halo/app/infra/exception/handlers/GlobalErrorWebExceptionHandlerTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package run.halo.app.infra.exception.handlers; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.infra.exception.AccessDeniedException; -import run.halo.app.infra.exception.NotFoundException; -import run.halo.app.infra.exception.ThemeUninstallException; - -/** - * Tests for {@link GlobalErrorWebExceptionHandler}. - * - * @author guqing - * @since 2.0.0 - */ -@SpringBootTest -@AutoConfigureWebTestClient -class GlobalErrorWebExceptionHandlerTest { - @Autowired - private WebTestClient client; - - @Test - void renderErrorResponseWhenNotFoundError() { - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "notFound") - .build()) - .exchange() - .expectStatus().isNotFound() - .expectBody() - .jsonPath("$.title").isEqualTo("Not Found") - .jsonPath("$.detail").isEqualTo("Not Found") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(404); - } - - @Test - void renderErrorResponseWhenBadRequestError() { - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "badRequest1") - .build()) - .exchange() - .expectStatus().isBadRequest() - .expectBody() - .jsonPath("$.title").isEqualTo("Bad Request") - .jsonPath("$.detail").isEqualTo("Bad Request") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(400); - - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "badRequest2") - .build()) - .exchange() - .expectStatus().isBadRequest() - .expectBody() - .jsonPath("$.title").isEqualTo("Bad Request") - .jsonPath("$.detail").isEqualTo("Bad Request for state") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(400); - - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "badRequest3") - .build()) - .exchange() - .expectStatus().isBadRequest() - .expectBody() - .jsonPath("$.title").isEqualTo("Bad Request") - .jsonPath("$.detail").isEqualTo("theme install error") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(400); - } - - @Test - void renderErrorResponseWhenAccessDeniedError() { - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "accessDenied") - .build()) - .exchange() - .expectStatus().isForbidden() - .expectBody() - .jsonPath("$.title").isEqualTo("Forbidden") - .jsonPath("$.detail").isEqualTo("Access Denied") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(403); - } - - @Test - void renderErrorResponseWhenOptimisticLockingFailureError() { - client.get() - .uri(uriBuilder -> uriBuilder.path("/hello/errors") - .queryParam("type", "conflict") - .build()) - .exchange() - .expectStatus().isEqualTo(HttpStatus.CONFLICT) - .expectBody() - .jsonPath("$.title").isEqualTo("Conflict") - .jsonPath("$.detail").isEqualTo("Version conflict") - .jsonPath("$.instance").isEqualTo("/hello/errors") - .jsonPath("$.status").isEqualTo(409); - } - - @TestConfiguration - static class TestConfig { - @Bean - public RouterFunction routerFunction() { - return RouterFunctions.route() - .GET("/hello/errors", request -> { - String type = request.queryParam("type").orElse("other"); - if (type.equals("notFound")) { - throw new NotFoundException("Not Found"); - } - if (type.equals("badRequest1")) { - throw new IllegalArgumentException("Bad Request"); - } - if (type.equals("badRequest2")) { - throw new IllegalStateException("Bad Request for state"); - } - if (type.equals("badRequest3")) { - throw new ThemeUninstallException("theme install error"); - } - if (type.equals("conflict")) { - throw new OptimisticLockingFailureException("Version conflict"); - } - if (type.equals("accessDenied")) { - throw new AccessDeniedException("Access Denied"); - } - throw new RuntimeException("Unknown Error"); - }) - .build(); - } - } -} diff --git a/src/test/java/run/halo/app/infra/exception/handlers/I18nExceptionTest.java b/src/test/java/run/halo/app/infra/exception/handlers/I18nExceptionTest.java new file mode 100644 index 0000000000..a6ef11b9c8 --- /dev/null +++ b/src/test/java/run/halo/app/infra/exception/handlers/I18nExceptionTest.java @@ -0,0 +1,172 @@ +package run.halo.app.infra.exception.handlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@SpringBootTest +@AutoConfigureWebTestClient +class I18nExceptionTest { + + @Autowired + WebTestClient webClient; + + + @Test + void shouldBeOkForGreetingEndpoint() { + webClient.get().uri("/response-entity/greet") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello Halo"); + } + + @Test + void shouldGetErrorIfErrorResponseThrow() { + webClient.get().uri("/response-entity/error-response") + .exchange() + .expectStatus().isBadRequest() + .expectBody(ProblemDetail.class) + .value(problemDetail -> { + assertEquals("Error Response", problemDetail.getTitle()); + assertEquals("Message argument is {0}.", problemDetail.getDetail()); + }); + } + + + @Test + void shouldGetErrorIfErrorResponseThrowWithMessageCode() { + webClient.get().uri("/response-entity/error-response/with-message-code") + .exchange() + .expectStatus().isBadRequest() + .expectBody(ProblemDetail.class) + .value(problemDetail -> { + assertEquals("Error Response", problemDetail.getTitle()); + assertEquals("Something went wrong, argument is fake-arg.", + problemDetail.getDetail()); + }); + } + + @Test + void shouldGetErrorIfErrorResponseThrowWithMessageCodeAndLocaleIsChinese() { + webClient.get().uri("/response-entity/error-response/with-message-code") + .header(HttpHeaders.ACCEPT_LANGUAGE, "zh-CN,zh") + .exchange() + .expectStatus().isBadRequest() + .expectBody(ProblemDetail.class) + .value(problemDetail -> { + assertEquals("发生错误", problemDetail.getTitle()); + assertEquals("发生了一些错误,参数:fake-arg。", + problemDetail.getDetail()); + }); + + } + + @Test + void shouldGetErrorIfThrowingResponseStatusException() { + webClient.get().uri("/response-entity/with-response-status-error") + .exchange() + .expectStatus().isEqualTo(HttpStatus.GONE) + .expectBody(ProblemDetail.class) + .value(problemDetail -> { + assertEquals("Gone", problemDetail.getTitle()); + assertEquals("Something went wrong", + problemDetail.getDetail()); + }); + } + + @Test + void shouldGetErrorIfThrowingGeneralException() { + webClient.get().uri("/response-entity/general-error") + .exchange() + .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .expectBody(ProblemDetail.class) + .value(problemDetail -> { + assertEquals("Internal Server Error", problemDetail.getTitle()); + assertEquals("Something went wrong", + problemDetail.getDetail()); + }); + } + + @TestConfiguration + static class TestConfig { + + @RestController + @RequestMapping("/response-entity") + static class ResponseEntityController { + + @GetMapping("/greet") + ResponseEntity greet() { + return ResponseEntity.ok("Hello Halo"); + } + + @GetMapping("/error-response") + ResponseEntity throwErrorResponseException() { + throw new ErrorResponseException(); + } + + @GetMapping("/error-response/with-message-args") + ResponseEntity throwErrorResponseExceptionWithMessageArgs() { + throw new ErrorResponseException("Something went wrong.", + null, new Object[] {"fake-arg"}); + } + + @GetMapping("/error-response/with-message-code") + ResponseEntity throwErrorResponseExceptionWithMessageCode() { + throw new ErrorResponseException("Something went wrong.", + "error.somethingWentWrong", new Object[] {"fake-arg"}); + } + + @GetMapping("/with-response-status-error") + ResponseEntity throwWithResponseStatusException() { + throw new WithResponseStatusException(); + } + + @GetMapping("/general-error") + ResponseEntity throwGeneralException() { + throw new GeneralException("Something went wrong"); + } + + } + } + + static class ErrorResponseException extends ResponseStatusException { + + public ErrorResponseException() { + this("Something went wrong."); + } + + public ErrorResponseException(String reason) { + this(reason, null, null); + } + + public ErrorResponseException(String reason, String detailCode, Object[] detailArgs) { + super(HttpStatus.BAD_REQUEST, reason, null, detailCode, detailArgs); + } + } + + @ResponseStatus(value = HttpStatus.GONE, reason = "Something went wrong") + static class WithResponseStatusException extends RuntimeException { + + } + + static class GeneralException extends RuntimeException { + + public GeneralException(String message) { + super(message); + } + } +} diff --git a/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java b/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java index ccccdbc9a4..888c9c5074 100644 --- a/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java +++ b/src/test/java/run/halo/app/plugin/PluginStartedListenerTest.java @@ -52,8 +52,10 @@ void lookupFromJar() throws IOException { Set unstructuredFilePathFromJar = PluginStartedListener.PluginExtensionLoaderUtils.lookupFromJar(targetJarPath); assertThat(unstructuredFilePathFromJar).hasSize(3); - assertThat(unstructuredFilePathFromJar).containsAll(Set.of("extensions/roles.yaml", - "extensions/reverseProxy.yaml", "extensions/test.yml")); + assertThat(unstructuredFilePathFromJar).containsAll(Set.of( + Path.of("extensions/roles.yaml").toString(), + Path.of("extensions/reverseProxy.yaml").toString(), + Path.of("extensions/test.yml").toString())); } finally { FileSystemUtils.deleteRecursively(tempDirectory); } diff --git a/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java b/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java index 7364597305..10573f36e2 100644 --- a/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java +++ b/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import java.util.List; import java.util.stream.Collectors; @@ -163,7 +164,8 @@ void shouldFindUserDetailsByExistingUsernameButWithoutAnyRoles() { @Test void shouldNotFindUserDetailsByNonExistingUsername() { when(userService.getUser("non-existing-user")).thenReturn( - Mono.error(() -> new ExtensionNotFoundException("The user was not found"))); + Mono.error(() -> new ExtensionNotFoundException( + fromExtension(run.halo.app.core.extension.User.class), "non-existing-user"))); var userDetailsMono = userDetailService.findByUsername("non-existing-user"); diff --git a/src/test/java/run/halo/app/security/authorization/AuthorizationTest.java b/src/test/java/run/halo/app/security/authorization/AuthorizationTest.java index f105366c69..2a8ddbea5c 100644 --- a/src/test/java/run/halo/app/security/authorization/AuthorizationTest.java +++ b/src/test/java/run/halo/app/security/authorization/AuthorizationTest.java @@ -10,6 +10,7 @@ import static org.springframework.web.reactive.function.server.RequestPredicates.PUT; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RouterFunctions.route; +import static run.halo.app.extension.GroupVersionKind.fromExtension; import java.util.ArrayList; import java.util.List; @@ -92,7 +93,8 @@ void accessProtectedApiWithSufficientRole() { when(roleService.getMonoRole("post.read")).thenReturn(Mono.just(role)); when(roleService.getMonoRole("authenticated")).thenReturn( - Mono.error(ExtensionNotFoundException::new)); + Mono.error( + () -> new ExtensionNotFoundException(fromExtension(Role.class), "authenticated"))); var token = LoginUtils.login(webClient, "user", "password").block(); webClient.get().uri("/apis/fake.halo.run/v1/posts") diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index a45f4ea1c3..6d9d74379b 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -11,6 +11,8 @@ spring: init: mode: always platform: h2 + messages: + basename: config.i18n.messages halo: work-dir: ${user.home}/halo-next-test diff --git a/src/test/resources/config/i18n/messages.properties b/src/test/resources/config/i18n/messages.properties new file mode 100644 index 0000000000..50faa3be4f --- /dev/null +++ b/src/test/resources/config/i18n/messages.properties @@ -0,0 +1,3 @@ +problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Error Response +problemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Message argument is {0}. +error.somethingWentWrong=Something went wrong, argument is {0}. diff --git a/src/test/resources/config/i18n/messages_zh.properties b/src/test/resources/config/i18n/messages_zh.properties new file mode 100644 index 0000000000..c79351daa1 --- /dev/null +++ b/src/test/resources/config/i18n/messages_zh.properties @@ -0,0 +1,3 @@ +problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=发生错误 +problemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=参数:{0}。 +error.somethingWentWrong=发生了一些错误,参数:{0}。