Spring Boot實作RFC 7807 Problem Details for HTTP APIs範例如下。
通常對於API請求發生邏輯錯誤時除了HTTP狀態碼還會定義一些錯誤碼及敘述並放在回應的JSON中返回,而RFC 7807則定義了一個統一的錯誤回應JSON格式。
RFC 7807的JSON屬性稱為Problem Details Object Members,說明如下:
type
- 字串型態。導向說明問題類型文件的URI。title
- 字串型態。簡短的問題敘述摘要status
- 數值型態。HTTP狀態碼。detail
- 字串型態。問題原因說明。instance
- 字串型態。發生問題的URI
而以上屬性以外的額外屬性皆稱為extensions。
RFC 7807返回的JSON範例如下。除了以上屬性外,回應的Content-Type
為application/problem+json
。
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": [
"/account/12345",
"/account/67890"
]
}
上面範例中的balance
及accounts
屬性皆為Extension Members,而Client端應該忽略這些extension member屬性。
本篇在Spring Boot實作RFC 7807規格的API錯誤回應。(不確定是否正確)
範例
範例環境:
- Spring Boot 2.3.2
- Lombok
實作類別:
DemoController
DemoException
DemoExceptionHandler
DemoResponse
ErrorCode
Employee
DemoController
設定REST API endpoint GET | /employees/{name}
,依傳入的路徑參數name
尋找名稱開頭符合的Employee
資料。name
參數中不可包含數字,否則拋出DemoException
。
DemoController
package com.abc.demo.controller;
import com.abc.demo.controller.exception.DemoException;
import com.abc.demo.error.ErrorCode;
import com.abc.demo.model.Employee;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@RestController
public class DemoController {
@GetMapping("/employees/{name}")
public List<Employee> getEmployees(@PathVariable String name) {
if (Pattern.compile("\\d").matcher(name).find()) {
throw new DemoException(
ErrorCode.WRONG_URI_PARAMS_FORMAT,
"URI parameter {name} cannot contain digit",
"/employees/" + name,
null
);
}
return findEmployeesByName(name);
}
private List<Employee> findEmployeesByName(String name) {
List<Employee> employeeList = Arrays.asList(
new Employee(1, "John", 33),
new Employee(2, "Mary", 28),
new Employee(3, "Jason", 45)
);
return employeeList.stream()
.filter(e -> isStartWith(e.getName(), name))
.collect(Collectors.toList());
}
private boolean isStartWith(String name, String startStr) {
return Pattern.compile("^" + startStr).matcher(name).find();
}
}
DemoException
包含錯誤代碼ErrorCode
、錯誤原因說明detail
、錯誤發生的URIinstance
及底層的錯誤exception
。這些資訊會在負責捕捉錯誤的DemoExceptionHandler
作為回應的JSON資訊。
DemoException
package com.abc.demo.controller.exception;
import com.abc.demo.error.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class DemoException extends RuntimeException {
private final ErrorCode errorCode;
private final String detail;
private final String instance;
private final Exception exception;
}
DemoExceptionHandler
為@ControllerAdvice
類,負責捕捉並處理Controller外拋的DemoException
並返回RFC 7807 Problem Details Object的JSON內容及對應的HTTP狀態碼、Content-Type
設為application/problem+json
。
DemoExceptionHandler
package com.abc.demo.controller.exception.handler;
import com.abc.demo.controller.exception.DemoException;
import com.abc.demo.controller.response.DemoResponse;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class DemoExceptionHandler {
@ExceptionHandler({DemoException.class})
public final ResponseEntity<DemoResponse> handleDemoException(DemoException ex) {
DemoResponse demoResponse = new DemoResponse (
ex.getErrorCode().getType(),
ex.getErrorCode().getTitle(),
ex.getErrorCode().getHttpStatus().value(),
ex.getDetail(),
ex.getInstance()
);
return ResponseEntity
.status(ex.getErrorCode().getHttpStatus())
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body(demoResponse);
}
}
DemoResponse
作為符合RFC 7807 Problem Details Object的格式。
DemoResponse
package com.abc.demo.controller.response;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class DemoResponse {
private String type;
private String title;
private int status;
private String detail;
private String instance;
}
ErrorCode
錯誤代碼列舉,包含了對應的HTTP狀態httpstatus
、錯誤摘要title
及說明文件URI位址資訊type
。
ErrorCode
package com.abc.demo.error;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@AllArgsConstructor
@Getter
public enum ErrorCode {
WRONG_URI_PARAMS_FORMAT(
HttpStatus.BAD_REQUEST,
"Wrong URI Parameters Format",
"http://localhost:8080/errortype.html#wrong-uri-params");
private final HttpStatus httpStatus;
private final String title;
private final String type;
}
Employee
為API呼叫正常時返回的資料。
Employee
package com.abc.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Employee {
private long id;
private String name;
private int age;
}
errortype.html
為錯誤類型說明文件,為RFC 7807 Problem Details Object的type
的URI指向的文件。
errortype.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error Tyeps</title>
</head>
<body>
<ul>
<li id="wrong-uri-params">Wrong URI Parameters Format: URI parameters format is wrong.</li>
</ul>
</body>
</html>
測試
以cURL送出curl -i -X GET "http://localhost:8080/employees/J"
回應整理如下。
curl -i -X GET "http://localhost:8080/employees/J"
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 30 Aug 2021 12:20:07 GMT
[
{
"id":1,
"name":"John",
"age":33},
{
"id":3,
"name":"Jason",
"age":45
}
]
以cURL送出curl -i -X GET "http://localhost:8080/employees/123"
回應整理如下。
$ curl -i -X GET "http://localhost:8080/employees/123"
HTTP/1.1 400
Content-Type: application/problem+json
Transfer-Encoding: chunked
Date: Mon, 30 Aug 2021 12:21:41 GMT
Connection: close
{
"type":"http://localhost:8080/errortype.html#wrong-uri-params",
"title":"Wrong URI Parameters Format",
"status":400,
"detail":"URI parameter {name} cannot contain digit",
"instance":"/employees/123"
}
參考github。
沒有留言:
張貼留言