我有一个使用
spring-boot-starter-graphql
的简单 Spring Boot 项目。该项目有一个控制器接受一个参数。
@Controller
public class HelloNameController {
@QueryMapping
public String hello(@Argument String name) {
return "Hello " + name;
}
}
此参数是必需的。
Graphql 架构
type Query {
hello (name : String!) : String
}
当我在 Postman 中调用此 API 且不传递此参数时,应用程序会返回错误。我想覆盖此错误消息的消息,但我找不到方法来做到这一点。 在官方文档中,它说要实现
DataFetcherExceptionResolverAdapter
,我已将其实现为bean
@Configuration
public class GraphQLConfig {
@Bean
public DataFetcherExceptionResolver exceptionResolver() {
return DataFetcherExceptionResolverAdapter.from((ex, env) -> {
if (ex instanceof CoercingParseValueException) {
return GraphqlErrorBuilder.newError(env).message("CoercingParseValueException")
.errorType(ErrorType.ExecutionAborted).build();
}
if (ex instanceof CoercingSerializeException) {
return GraphqlErrorBuilder.newError(env).message("CoercingSerializeException")
.errorType(ErrorType.ExecutionAborted).build();
} else {
return null;
}
});
}
}
问题是错误永远不会达到这一点。如何捕获此类错误并覆盖该消息?
我在 GitHub 上问过类似的问题。 graphql-java 项目 (#2866) 和 spring-graphql 项目 (#415) 的响应类似。在撰写本文时总结这是不可能的。 然后我创建了一个“解决方法”:
首先,创建一个实现 GraphQLError 的自定义异常类。
import graphql.GraphQLError;
import graphql.language.SourceLocation;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.http.HttpStatus;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Getter
@NoArgsConstructor
public class BadRequestException extends RuntimeException implements GraphQLError {
private HttpStatus status = HttpStatus.BAD_REQUEST;
private String message = "Resource not found";
// Below code used for GraphQL only
private List<SourceLocation> locations;
public BadRequestException(String message, List<SourceLocation> locations) {
this.message = message;
this.locations = locations;
}
@Override
public Map<String, Object> getExtensions() {
Map<String, Object> customAttributes = new LinkedHashMap<>();
customAttributes.put("errorCode", this.status.value());
return customAttributes;
}
@Override
public List<SourceLocation> getLocations() {
return locations;
}
@Override
public ErrorType getErrorType() {
return ErrorType.BAD_REQUEST;
}
@Override
public Map<String, Object> toSpecification() {
return GraphQLError.super.toSpecification();
}
}
其次,创建一个实现WebGraphQlInterceptor的拦截器类,并注释为@Component,这样Spring就可以将其创建为bean。在此类内部实现捕获所需错误并将其转换为之前创建的异常类的逻辑
import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.validation.ValidationErrorType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.graphql.ResponseError;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Component
public class ErrorInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
return chain.next(request)
.map(response -> {
log.info("[ErrorInterceptor] Intercepting response... ");
List<GraphQLError> graphQLErrors = response.getErrors().stream()
.filter(error -> ErrorType.ValidationError.equals(error.getErrorType()))
.map(this::resolveException)
.collect(Collectors.toList());
if (!graphQLErrors.isEmpty()) {
log.info("[ErrorInterceptor] Found invalid syntax error! Overriding the message.");
return response.transform(builder -> builder.errors(graphQLErrors));
}
return response;
});
}
private GraphQLError resolveException(ResponseError responseError) {
ErrorClassification errorType = responseError.getErrorType();
if (ErrorType.ValidationError.equals(errorType)) {
String message = responseError.getMessage();
log.info("[ErrorInterceptor] Returning invalid field error ");
if (ValidationErrorType.NullValueForNonNullArgument.equals(
extractValidationErrorFromErrorMessage(responseError.getMessage()))) {
String errorMessage =
"Field " + StringUtils.substringBetween(message, "argument ", " @") + " cannot be null";
return new BadRequestException(errorMessage, responseError.getLocations());
}
}
log.info("[ErrorInterceptor] Returning unknown query validation error ");
return new BadRequestException("Unknown error", responseError.getLocations());
}
private ValidationErrorType extractValidationErrorFromErrorMessage(String message) {
return ValidationErrorType.valueOf(StringUtils.substringBetween(message, "type ", ":"));
}
}
这种方法的唯一问题是,所有需要的信息(例如错误类型、导致错误的字段等)都嵌入到本机错误消息中。因此,要提取所需的参数,我们必须解析字符串消息。
这是 kotlin 中完整的正确错误处理,包括验证错误处理。
这些错误由
GraphQL.execute()
引发,不算作“数据获取”错误 - 由 GraphQL Java 记录这里
Spring 记录了拦截器这里
enum class MyErrorType(val description: String) : ErrorClassification {
STUB_TYPE("This is a stub type");
fun create(message: String? = null, cause: Throwable? = null) = MyException(this, message, cause)
}
@GraphQlExceptionHandler // create this function for every concrete exception that occurred during fetching and then Throwable as the last fallback. Or create this one and has a switch case on exception type. Or provide a DataFetcherExceptionResolverAdapter bean.
fun handle(ex: Throwable, env: DataFetchingEnvironment): GraphQLError {
val errorBuilder = GraphqlErrorBuilder.newError(env)
.errorType(org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR)
... // set some error type (your own or pick one from spring's ErrorType) here so that below if doesn't catch it again. You should set it either way as it's client-facing.
}
@GraphQlExceptionHandler // example for MyException
fun handleMyError(ex: MyException, env: DataFetchingEnvironment): GraphQLError {
val errorBuilder = GraphqlErrorBuilder.newError(env)
.errorType(ex.type) // ex.type would be the MyErrorType from above
...
}
@Bean // anything not handled by the above will be caught here
fun requestErrorHandler() = WebGraphQlInterceptor { request: WebGraphQlRequest,
chain: WebGraphQlInterceptor.Chain
->
chain.next(request).map { response ->
if (response.isValid) return@map response
val errors = response.errors.stream()
.map { error ->
// https://www.graphql-java.com/documentation/v22/exceptions
if (error.errorType is graphql.ErrorType) log.error(
"Unexpected ${error.errorType} error in '${request.operationName}': $error\n${request.document}, vars: ${
mapper.writeValueAsString(request.variables)
}, id: ${request.id}")
GraphqlErrorBuilder.newError()
.errorType(ErrorType.INTERNAL_ERROR)
.message("internal error")
.build()
}
.toList()
response.transform { builder -> builder.errors(errors).build() }
}
}