網頁

2021/8/30

Spring Boot 實作RFC 7807 Problem Details for HTTP APIs

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-Typeapplication/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"
  ]
}

上面範例中的balanceaccounts屬性皆為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


沒有留言:

張貼留言