mirror of https://github.com/halo-dev/halo
Refactor setting of problem detail type (#4099)
#### What type of PR is this? /kind improvement /area core #### What this PR does / why we need it: Define a global map to mapping exception to problem detail type. #### Does this PR introduce a user-facing change? ```release-note None ```pull/4105/head
parent
12a426c9ae
commit
5e9e87582e
|
@ -1,8 +1,29 @@
|
||||||
package run.halo.app.infra.exception;
|
package run.halo.app.infra.exception;
|
||||||
|
|
||||||
|
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;
|
||||||
|
|
||||||
|
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.core.annotation.MergedAnnotations;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.web.ErrorResponse;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public enum Exceptions {
|
public enum Exceptions {
|
||||||
;
|
;
|
||||||
|
|
||||||
|
public static final String DEFAULT_TYPE = "about:blank";
|
||||||
|
|
||||||
public static final String THEME_ALREADY_EXISTS_TYPE =
|
public static final String THEME_ALREADY_EXISTS_TYPE =
|
||||||
"https://halo.run/probs/theme-alreay-exists";
|
"https://halo.run/probs/theme-alreay-exists";
|
||||||
|
|
||||||
|
@ -12,4 +33,46 @@ public enum Exceptions {
|
||||||
public static final String REQUEST_NOT_PERMITTED_TYPE =
|
public static final String REQUEST_NOT_PERMITTED_TYPE =
|
||||||
"https://halo.run/probs/request-not-permitted";
|
"https://halo.run/probs/request-not-permitted";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-ErrorResponse exception to type map.
|
||||||
|
*/
|
||||||
|
public static final Map<Class<? extends Throwable>, String> EXCEPTION_TYPE_MAP = Map.of(
|
||||||
|
RequestNotPermitted.class, REQUEST_NOT_PERMITTED_TYPE,
|
||||||
|
BadCredentialsException.class, INVALID_CREDENTIAL_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
public static ErrorResponse createErrorResponse(Throwable t, @Nullable HttpStatusCode status,
|
||||||
|
ServerWebExchange exchange, MessageSource messageSource) {
|
||||||
|
final ErrorResponse errorResponse;
|
||||||
|
if (t instanceof ErrorResponse er) {
|
||||||
|
errorResponse = er;
|
||||||
|
} else {
|
||||||
|
var responseStatusAnno =
|
||||||
|
MergedAnnotations.from(t.getClass(), TYPE_HIERARCHY).get(ResponseStatus.class);
|
||||||
|
if (status == null) {
|
||||||
|
status = responseStatusAnno.getValue("code", HttpStatus.class)
|
||||||
|
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
var type = EXCEPTION_TYPE_MAP.getOrDefault(t.getClass(), DEFAULT_TYPE);
|
||||||
|
var detail = responseStatusAnno.getValue("reason", String.class)
|
||||||
|
.orElseGet(t::getMessage);
|
||||||
|
var builder = ErrorResponse.builder(t, status, detail)
|
||||||
|
.type(URI.create(type));
|
||||||
|
if (status.is5xxServerError()) {
|
||||||
|
builder.detailMessageCode("problemDetail.internalServerError")
|
||||||
|
.titleMessageCode("problemDetail.title.internalServerError");
|
||||||
|
}
|
||||||
|
errorResponse = builder.build();
|
||||||
|
}
|
||||||
|
var problemDetail = errorResponse.updateAndGetBody(messageSource, getLocale(exchange));
|
||||||
|
problemDetail.setInstance(exchange.getRequest().getURI());
|
||||||
|
problemDetail.setProperty("requestId", exchange.getRequest().getId());
|
||||||
|
problemDetail.setProperty("timestamp", Instant.now());
|
||||||
|
return errorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Locale getLocale(ServerWebExchange exchange) {
|
||||||
|
var locale = exchange.getLocaleContext().getLocale();
|
||||||
|
return locale == null ? Locale.getDefault() : locale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,13 @@
|
||||||
package run.halo.app.infra.exception.handlers;
|
package run.halo.app.infra.exception.handlers;
|
||||||
|
|
||||||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
import static run.halo.app.infra.exception.Exceptions.createErrorResponse;
|
||||||
import static org.springframework.core.annotation.MergedAnnotations.from;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||||
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
|
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
|
||||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.core.annotation.MergedAnnotation;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.HttpStatusCode;
|
|
||||||
import org.springframework.web.ErrorResponse;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
|
||||||
|
@ -41,50 +31,12 @@ public class ProblemDetailErrorAttributes implements ErrorAttributes {
|
||||||
public Map<String, Object> getErrorAttributes(ServerRequest request,
|
public Map<String, Object> getErrorAttributes(ServerRequest request,
|
||||||
ErrorAttributeOptions options) {
|
ErrorAttributeOptions options) {
|
||||||
final var errAttributes = new LinkedHashMap<String, Object>();
|
final var errAttributes = new LinkedHashMap<String, Object>();
|
||||||
|
|
||||||
var error = getError(request);
|
var error = getError(request);
|
||||||
var responseStatusAnno = from(error.getClass(), SearchStrategy.TYPE_HIERARCHY)
|
var errorResponse = createErrorResponse(error, null, request.exchange(), messageSource);
|
||||||
.get(ResponseStatus.class);
|
errAttributes.put("error", errorResponse.getBody());
|
||||||
|
|
||||||
var status = determineHttpStatus(error, responseStatusAnno);
|
|
||||||
final ErrorResponse errorResponse;
|
|
||||||
if (error instanceof ErrorResponse er) {
|
|
||||||
errorResponse = er;
|
|
||||||
} else {
|
|
||||||
var reason = Optional.of(status)
|
|
||||||
.filter(HttpStatusCode::is5xxServerError)
|
|
||||||
.map(s -> "Something went wrong, please try again later.")
|
|
||||||
.orElseGet(() -> 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;
|
return errAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpStatusCode determineHttpStatus(Throwable t,
|
|
||||||
MergedAnnotation<ResponseStatus> 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
|
@Override
|
||||||
public Throwable getError(ServerRequest request) {
|
public Throwable getError(ServerRequest request) {
|
||||||
return (Throwable) request.attribute(ERROR_INTERNAL_ATTRIBUTE).stream()
|
return (Throwable) request.attribute(ERROR_INTERNAL_ATTRIBUTE).stream()
|
||||||
|
|
|
@ -3,21 +3,16 @@ package run.halo.app.security.authentication.login;
|
||||||
import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
|
import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
|
||||||
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
||||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||||
import static run.halo.app.infra.exception.Exceptions.INVALID_CREDENTIAL_TYPE;
|
import static run.halo.app.infra.exception.Exceptions.createErrorResponse;
|
||||||
import static run.halo.app.infra.exception.Exceptions.REQUEST_NOT_PERMITTED_TYPE;
|
|
||||||
import static run.halo.app.security.authentication.WebExchangeMatchers.ignoringMediaTypeAll;
|
import static run.halo.app.security.authentication.WebExchangeMatchers.ignoringMediaTypeAll;
|
||||||
|
|
||||||
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
||||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||||
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
||||||
import io.micrometer.observation.ObservationRegistry;
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
import java.net.URI;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Locale;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.authentication.ObservationReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ObservationReactiveAuthenticationManager;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
|
@ -143,30 +138,16 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
|
||||||
|
|
||||||
private Mono<Void> handleRequestNotPermitted(RequestNotPermitted e,
|
private Mono<Void> handleRequestNotPermitted(RequestNotPermitted e,
|
||||||
ServerWebExchange exchange) {
|
ServerWebExchange exchange) {
|
||||||
var errorResponse =
|
var errorResponse = createErrorResponse(e, TOO_MANY_REQUESTS, exchange, messageSource);
|
||||||
createErrorResponse(e, TOO_MANY_REQUESTS, REQUEST_NOT_PERMITTED_TYPE, exchange);
|
|
||||||
return writeErrorResponse(errorResponse, exchange);
|
return writeErrorResponse(errorResponse, exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> handleAuthenticationException(AuthenticationException exception,
|
private Mono<Void> handleAuthenticationException(AuthenticationException exception,
|
||||||
ServerWebExchange exchange) {
|
ServerWebExchange exchange) {
|
||||||
var errorResponse =
|
var errorResponse = createErrorResponse(exception, UNAUTHORIZED, exchange, messageSource);
|
||||||
createErrorResponse(exception, UNAUTHORIZED, INVALID_CREDENTIAL_TYPE, exchange);
|
|
||||||
return writeErrorResponse(errorResponse, exchange);
|
return writeErrorResponse(errorResponse, exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ErrorResponse createErrorResponse(Throwable t, HttpStatus status, String type,
|
|
||||||
ServerWebExchange exchange) {
|
|
||||||
var errorResponse =
|
|
||||||
ErrorResponse.create(t, status, t.getMessage());
|
|
||||||
var problemDetail = errorResponse.updateAndGetBody(messageSource, getLocale(exchange));
|
|
||||||
problemDetail.setType(URI.create(type));
|
|
||||||
problemDetail.setInstance(exchange.getRequest().getURI());
|
|
||||||
problemDetail.setProperty("requestId", exchange.getRequest().getId());
|
|
||||||
problemDetail.setProperty("timestamp", Instant.now());
|
|
||||||
return errorResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<Void> writeErrorResponse(ErrorResponse errorResponse,
|
private Mono<Void> writeErrorResponse(ErrorResponse errorResponse,
|
||||||
ServerWebExchange exchange) {
|
ServerWebExchange exchange) {
|
||||||
return ServerResponse.status(errorResponse.getStatusCode())
|
return ServerResponse.status(errorResponse.getStatusCode())
|
||||||
|
@ -175,11 +156,6 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter {
|
||||||
.flatMap(response -> response.writeTo(exchange, context));
|
.flatMap(response -> response.writeTo(exchange, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Locale getLocale(ServerWebExchange exchange) {
|
|
||||||
var locale = exchange.getLocaleContext().getLocale();
|
|
||||||
return locale == null ? Locale.getDefault() : locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UsernamePasswordAuthenticationWebFilter extends AuthenticationWebFilter {
|
private class UsernamePasswordAuthenticationWebFilter extends AuthenticationWebFilter {
|
||||||
|
|
||||||
public UsernamePasswordAuthenticationWebFilter(
|
public UsernamePasswordAuthenticationWebFilter(
|
||||||
|
|
|
@ -19,6 +19,7 @@ problemDetail.title.run.halo.app.infra.exception.PluginInstallationException=Plu
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin Already Exists Error
|
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin Already Exists Error
|
||||||
problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Duplicate Name Error
|
problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Duplicate Name Error
|
||||||
problemDetail.title.io.github.resilience4j.ratelimiter.RequestNotPermitted=Request Not Permitted
|
problemDetail.title.io.github.resilience4j.ratelimiter.RequestNotPermitted=Request Not Permitted
|
||||||
|
problemDetail.title.internalServerError=Internal Server Error
|
||||||
|
|
||||||
# Detail definitions
|
# Detail definitions
|
||||||
problemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException=Content type {0} is not supported. Supported media types: {1}.
|
problemDetail.org.springframework.web.server.UnsupportedMediaTypeStatusException=Content type {0} is not supported. Supported media types: {1}.
|
||||||
|
@ -50,3 +51,4 @@ problemDetail.directoryTraversal=Directory traversal detected. Base path is {0},
|
||||||
|
|
||||||
problemDetail.plugin.version.unsatisfied.requires=Plugin requires a minimum system version of {0}, but the current version is {1}.
|
problemDetail.plugin.version.unsatisfied.requires=Plugin requires a minimum system version of {0}, but the current version is {1}.
|
||||||
problemDetail.plugin.missingManifest=Missing plugin manifest file "plugin.yaml" or manifest file does not conform to the specification.
|
problemDetail.plugin.missingManifest=Missing plugin manifest file "plugin.yaml" or manifest file does not conform to the specification.
|
||||||
|
problemDetail.internalServerError=Something went wrong, please try again later.
|
||||||
|
|
|
@ -7,6 +7,7 @@ problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=名称
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=插件已存在
|
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=插件已存在
|
||||||
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败
|
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败
|
||||||
problemDetail.title.io.github.resilience4j.ratelimiter.RequestNotPermitted=请求限制
|
problemDetail.title.io.github.resilience4j.ratelimiter.RequestNotPermitted=请求限制
|
||||||
|
problemDetail.title.internalServerError=服务器内部错误
|
||||||
|
|
||||||
problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。
|
problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。
|
||||||
problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文件 {0} 已存在,建议更名后重试。
|
problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文件 {0} 已存在,建议更名后重试。
|
||||||
|
@ -22,3 +23,4 @@ problemDetail.plugin.missingManifest=缺少 plugin.yaml 配置文件或配置文
|
||||||
|
|
||||||
problemDetail.theme.version.unsatisfied.requires=主题要求一个最小的系统版本为 {0}, 但当前版本为 {1}。
|
problemDetail.theme.version.unsatisfied.requires=主题要求一个最小的系统版本为 {0}, 但当前版本为 {1}。
|
||||||
problemDetail.theme.install.missingManifest=缺少 theme.yaml 配置文件或配置文件不符合规范。
|
problemDetail.theme.install.missingManifest=缺少 theme.yaml 配置文件或配置文件不符合规范。
|
||||||
|
problemDetail.internalServerError=服务器内部发生错误,请稍候再试。
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Error Response
|
problemDetail.title.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Error Response
|
||||||
|
problemDetail.internalServerError=Something went wrong, please try again later.
|
||||||
|
|
||||||
problemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Message argument is {0}.
|
problemDetail.run.halo.app.infra.exception.handlers.I18nExceptionTest$ErrorResponseException=Message argument is {0}.
|
||||||
error.somethingWentWrong=Something went wrong, argument is {0}.
|
error.somethingWentWrong=Something went wrong, argument is {0}.
|
||||||
|
problemDetail.title.internalServerError=Internal Server Error
|
Loading…
Reference in New Issue