1. 概述
在本教程中,我們將學習 GraphQL 中的錯誤處理選項。我們將探討 GraphQL 規範中關於錯誤響應的規定。 此外,我們還將使用 Spring Boot 開發一個 GraphQL 錯誤處理的示例。
2. 根據 GraphQL 規範,響應
根據 GraphQL 規範,收到的每個請求都必須返回一個規範化的響應。該規範化的響應包含來自相應成功或失敗請求操作的 map 對象,即數據或錯誤。響應還可能包含部分成功結果數據和字段錯誤。
響應 map 的關鍵組件是 errors、data 和 extensions。
響應中的 errors 部分描述了請求操作期間發生的任何失敗。如果未發生錯誤,則 errors 組件不應出現在響應中。在下一部分,我們將研究規範中描述的不同類型的錯誤。
響應中的 data 部分描述了成功執行請求操作的結果。如果操作是一個查詢,則此組件是一個查詢根操作類型的對象。另一方面,如果操作是一個 mutation,則此組件是一個 mutation 根操作類型的對象。
如果由於缺少信息、驗證錯誤或語法錯誤,請求操作在執行之前就失敗了,則 data 組件不應出現在響應中。如果操作在執行不成功的結果時失敗,則 data 組件必須是 null。
響應 map 還可以包含一個額外的組件,稱為 extensions,它是一個 map 對象。該組件允許實現者在響應中提供其他自定義內容,而無需受到任何額外的格式限制。
如果 data 組件不在響應中,則 errors 組件必須存在並且至少包含一個錯誤。它應指示失敗的原因。
以下是一個 GraphQL 錯誤示例:
mutation {
addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE",
location: {zipcode: "75024", city: "Dallas", state: "TX"}) {
vin
year
make
model
trim
}
}當唯一約束被違反時,錯誤響應將如下所示:
{
"data": null,
"errors": [
{
"errorType": "DataFetchingException",
"locations": [
{
"line": 2,
"column": 5,
"sourceName": null
}
],
"message": "Failed to add vehicle. Vehicle with vin NDXT155NDFTV59834 already present.",
"path": [
"addVehicle"
],
"extensions": {
"vin": "NDXT155NDFTV59834"
}
}
]
}3. 錯誤響應組件與 GraphQL 規範
響應中 errors 部分是一個非空列表,其中每個元素都是一個映射。
3.1. 請求錯誤
正如其名稱所示,請求錯誤可能在操作執行之前發生,如果請求本身存在問題,就會發生。這可能由於請求數據解析失敗、請求文檔驗證、不支持的操作或無效的請求值而導致。
當發生請求錯誤時,這表明執行尚未開始,這意味着響應中的 數據 部分不得出現在響應中。換句話説,響應僅包含 錯誤 部分。
下面是一個演示無效輸入語法情況的示例:
query {
searchByVin(vin: "error) {
vin
year
make
model
trim
}
}以下是翻譯後的內容:
以下是語法錯誤(此處為缺少引號)的請求錯誤響應:
{
"data": null,
"errors": [
{
"message": "Invalid Syntax",
"locations": [
{
"line": 5,
"column": 8,
"sourceName": null
}
],
"errorType": "InvalidSyntax",
"path": null,
"extensions": null
}
]
}3.2. 字段錯誤
字段錯誤,顧名思義,可能由於值未能強制轉換為預期類型或在特定字段的值解析過程中發生內部錯誤而產生。 這意味着字段錯誤發生在請求執行期間。
在發生字段錯誤的情況下,請求執行的所請求的操作繼續進行並返回一個部分結果,這意味着 數據 部分必須包含所有字段錯誤,這些錯誤位於 錯誤 部分。
讓我們來看另一個例子:
query {
searchAll {
vin
year
make
model
trim
}
}本次,我們已包含車輛的車身類型(trim)字段,該字段根據我們的GraphQL schema應為非空值。
然而,其中一輛車輛的信息具有車身類型(trim)值為null的值,因此我們只返回部分數據——車身類型(trim)值為非null的車輛,以及錯誤信息:
{
"data": {
"searchAll": [
null,
{
"vin": "JTKKU4B41C1023346",
"year": 2012,
"make": "Toyota",
"model": "Scion",
"trim": "Xd"
},
{
"vin": "1G1JC1444PZ215071",
"year": 2000,
"make": "Chevrolet",
"model": "CAVALIER VL",
"trim": "RS"
}
]
},
"errors": [
{
"message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
"path": [
"searchAll",
0,
"trim"
],
"errorType": "DataFetchingException",
"locations": null,
"extensions": null
}
]
}3.3. 錯誤響應格式
正如我們之前所見,響應中的錯誤是由一個或多個錯誤組成的。每個錯誤必須包含一個message鍵,描述失敗原因,以便客户端開發者可以進行必要的修正,從而避免錯誤。
每個錯誤還可能包含一個名為locations的鍵,它是一個列表,指向與錯誤相關的請求的 GraphQL 文檔中的行號。每個位置是一個包含鍵:line 和 column 的映射,分別提供關聯元素的行號和起始列號。
另一個可能出現在錯誤中的鍵是path。它提供了一個從根元素到響應中特定元素(存在錯誤)的路徑值。path的值可以是表示字段名稱或索引的字符串,如果字段值是列表,則表示錯誤元素索引。如果錯誤與具有別名名稱的字段相關聯,則path的值應為別名名稱。
3.4. 處理字段錯誤
無論是可空字段還是非可空字段,我們都應將其處理為返回 <em>null</em> 的字段,並且錯誤必須添加到 <em>errors</em> 列表中。
對於可空字段,響應中該字段的值將是 <em>null</em>,但 <em>errors</em> 列表必須包含描述失敗原因和其他信息,如前面所述。
另一方面,父級字段處理非可空字段錯誤。如果父級字段是非可空,則錯誤處理將傳播到我們到達可空父級或根元素為止。
同樣,如果列表字段包含非可空類型,並且其中一個或多個列表元素返回 <em>null</em>,則整個列表將解析為 <em>null</em>。此外,如果包含列表字段的父級字段是非可空,則錯誤處理將傳播到我們到達可空父級或根元素為止。
無論出於何原因,如果在解析過程中,同一個字段上引發了多個錯誤,那麼對於該字段,我們必須只將一個字段錯誤添加到 <em>errors</em> 列表中。
4. Spring Boot GraphQL 庫
我們的 Spring Boot 應用程序示例使用 <em><a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-graphql">spring-boot-starter-graphql</a></em> 模塊,該模塊引入了所需的 GraphQL 依賴項。
我們還使用 <a href="https://mvnrepository.com/artifact/org.springframework.graphql/spring-graphql-test"><em>spring-graphql-test</em></a > 模塊進行相關測試:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>5. Spring Boot GraphQL 錯誤處理
本節主要介紹在 Spring Boot 應用本身中處理 GraphQL 錯誤的方法。 我們不會涉及 GraphQL Java 和 GraphQL Spring Boot 應用的開發。
在我們的 Spring Boot 應用示例中,我們將根據位置或車輛識別號碼 (Vehicle Identification Number, VIN) 對車輛進行 mutate 或查詢。 我們將通過這個示例展示不同的錯誤處理實現方式。
在以下子節中,我們將看到 Spring Boot 模塊如何處理異常或錯誤。
5.1. GraphQL 響應與標準異常
通常,在 REST 應用中,我們通過擴展 RuntimeException 或 Throwable 創建自定義運行時異常類:
public class InvalidInputException extends RuntimeException {
public InvalidInputException(String message) {
super(message);
}
}通過這種方法,我們可以看到 GraphQL 引擎返回的響應如下:
{
"errors": [
{
"message": "INTERNAL_ERROR for 2c69042a-e7e6-c0c7-03cf-6026b1bbe559",
"locations": [
{
"line": 2,
"column": 5
}
],
"path": [
"searchByLocation"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": null
}在上述錯誤響應中,我們可以看到它不包含任何錯誤詳情。
默認情況下,任何在請求處理過程中發生的異常都由 ExceptionResolversExceptionHandler 類處理,該類實現了 GraphQL API 中的 DataFetcherExceptionHandler 接口。它允許應用程序註冊一個或多個 DataFetcherExceptionResolver 組件。
這些解析器將按順序調用,直到其中一個能夠處理並解決異常,將其轉換為 GraphQLError。如果沒有任何解析器能夠處理異常,則異常將被分類為 INTERNAL_ERROR。它還包含執行 ID 和通用錯誤消息,如上所示。
5.2. 使用自定義異常處理的 GraphQL 響應
現在讓我們看看在實現自定義異常處理後響應會是什麼樣子。
首先,我們還有一個自定義異常:
public class VehicleNotFoundException extends RuntimeException {
public VehicleNotFoundException(String message) {
super(message);
}
}`
DataFetcherExceptionResolver 提供異步接口。然而,在大多數情況下,只需擴展 DataFetcherExceptionResolverAdapter 並覆蓋其 resolveToSingleError 或 resolveToMultipleErrors 方法,即可實現同步錯誤解析。
現在,讓我們實現這個組件,並返回 NOT_FOUND 分類以及異常消息,而不是使用通用錯誤。
`@Component
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof VehicleNotFoundException) {
return GraphqlErrorBuilder.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.path(env.getExecutionStepInfo().getPath())
.location(env.getField().getSourceLocation())
.build();
} else {
return null;
}
}
}在這裏,我們創建了一個 GraphQLError,並添加了適當的分類和其他錯誤詳情,以生成更具用的響應,位於 JSON 響應的 errors 部分:
{
"errors": [
{
"message": "Vehicle with vin: 123 not found.",
"locations": [
{
"line": 2,
"column": 5
}
],
"path": [
"searchByVin"
],
"extensions": {
"classification": "NOT_FOUND"
}
}
],
"data": {
"searchByVin": null
}
}該錯誤處理機制的一個重要細節是,未決異常將在 ERROR 級別進行記錄,同時還會附帶與客户端發送的錯誤相關的 executionId。 已解決的異常,如上所示,將在日誌中以 DEBUG 級別進行記錄。
6. 結論
在本教程中,我們學習了不同類型的 GraphQL 錯誤。我們還研究瞭如何根據規範格式化 GraphQL 錯誤。隨後,我們在 Spring Boot 應用程序中實現了錯誤處理。