当HTTP状态代码不足时:处理Web API错误报告

RESTful Web API设计的一个领域(经常被忽视)是如何报告与业务或应用程序有关的错误和问题。 首先要想到HTTP状态代码的正确用法,尽管它非常方便,但通常它的信息量还不够。 让我们以400错误请求为例。 是的,它清楚地表明请求是有问题的,但是究竟出了什么问题呢?

RESTful的体系结构风格并不决定在这种情况下应该做什么,因此每个人都在发明自己的风格,约定和规范。 它可能像将错误消息包含到响应中一样简单,也可能像复制/粘贴长堆栈跟踪记录一样是短视的(对于Java或.NET,仅举几例)。 并不缺乏想法,但是幸运的是,我们至少有一些以RFC 7807形式提供的指南:HTTP API的问题详细信息 。 尽管它不是官方规范,而是草案(仍然),但它概述了有关当前问题的良好通用原则,这就是我们在本文中要讨论的内容。

简而言之, RFC 7807:HTTP API的问题详细信息仅提出了错误或问题表示形式( JSONXML格式),其中可能至少包括以下详细信息:

  • type –标识问题类型的URI参考
  • 标题 –问题类型的简短易读摘要
  • statusHTTP状态代码
  • 详细信息 –针对此问题的发生的易于理解的解释
  • 实例 –标识问题特定发生的URI参考

更重要的是,问题类型定义可以使用其他成员扩展问题详细信息对象,从而为上述成员做出贡献。 如您所见,从实现角度看,它看起来非常简单。 更好的是,感谢
Zalando ,我们已经有了
RFC 7807:HTTP API实现的问题详细信息 对于Java (和 特别是Spring Web )。 所以……让我们尝试一下!

我们将使用最新的技术堆栈, Spring BootApache CXF ,流行的Web服务框架和JAX-RS 2.1实现来构建我们虚构的People Management Web API。 为了简单起见,仅公开了两个端点:注册和按人员标识符查找。

Web API错误

撇开开发现实世界服务时可能遇到的大量问题和业务约束,即使使用此简单的API,也可能会出错。 我们要解决的第一个问题是,如果您要寻找的人尚未注册怎么办? 看起来很适合404 Not Found ,对吗? 确实,让我们从第一个问题PersonNotFoundProblem开始

public class PersonNotFoundProblem extends AbstractThrowableProblem {
    private static final long serialVersionUID = 7662154827584418806L;
    private static final URI TYPE = URI.create("http://localhost:21020/problems/person-not-found");
    
    public PersonNotFoundProblem(final String id, final URI instance) {
        super(TYPE, "Person is not found", Status.NOT_FOUND, 
            "Person with identifier '" + id + "' is not found", instance, 
                null, Map.of("id", id));
    }
}

它非常类似于典型的Java异常,并且确实是一个,因为AbstractThrowableProblemRuntimeException的子类。 这样,我们可以从JAX-RS API中抛出它。

@Produces({ MediaType.APPLICATION_JSON, "application/problem+json" })
@GET
@Path("{id}")
public Person findById(@PathParam("id") String id) {
    return service
        .findById(id)
        .orElseThrow(() -> new PersonNotFoundProblem(id, uriInfo.getRequestUri()));
}

如果我们运行服务器并尝试获取提供任何标识符的人员,则将返回问题详细信息响应(因为未预先填充数据集),例如:

$ curl "http://localhost:21020/api/people/1" -H  "Accept: */*" 

HTTP/1.1 404
Content-Type: application/problem+json

{
    "type" : "http://localhost:21020/problems/person-not-found",
    "title" : "Person is not found",
    "status" : 404,
    "detail" : "Person with identifier '1' is not found",
    "instance" : "http://localhost:21020/api/people/1",
    "id" : "1"
}

请注意应用程序/问题+ json媒体类型的用法以及响应中包含的其他属性ID 。 尽管有许多可以改进的地方,但是可以说它比仅裸露的404 (或由EntityNotFoundException引起的500 )要好。 另外,如果需要进一步的说明,可以参考此类问题背后的文档部分(在我们的情况下为http:// localhost:21020 / problems / person-not-found )。

因此,在例外之后设计问题只是一种选择。 您可能经常(出于非常正当的理由)会避免将业务逻辑与无关的细节耦合在一起。 在这种情况下,返回问题详细信息作为JAX-RS资源的响应有效负载是完全有效的。 例如,注册过程可能会引发NonUniqueEmailException,因此我们的Web API层可以将其转换为适当的问题详细信息。

@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON, "application/problem+json" })
@POST
public Response register(@Valid final CreatePerson payload) {
    try {
        final Person person = service.register(payload.getEmail(), 
            payload.getFirstName(), payload.getLastName());
            
        return Response
            .created(uriInfo.getRequestUriBuilder().path(person.getId()).build())
            .entity(person)
            .build();

    } catch (final NonUniqueEmailException ex) {
        return Response
            .status(Response.Status.BAD_REQUEST)
            .type("application/problem+json")
            .entity(Problem
                .builder()
                .withType(URI.create("http://localhost:21020/problems/non-unique-email"))
                .withInstance(uriInfo.getRequestUri())
                .withStatus(Status.BAD_REQUEST)
                .withTitle("The email address is not unique")
                .withDetail(ex.getMessage())
                .with("email", payload.getEmail())
                .build())
            .build();
        }
    }

要触发此问题,只需运行服务器实例并尝试两次注册同一个人即可,就像我们在下面所做的那样。

$ curl -X POST "http://localhost:21020/api/people" \ 
     -H  "Accept: */*" -H "Content-Type: application/json" \
     -d '{"email":"john@smith.com", "firstName":"John", "lastName": "Smith"}'

HTTP/1.1 400                                                                              
Content-Type: application/problem+json                                                           
                                                                                                                                                                                   
{                                                                                         
    "type" : "http://localhost:21020/problems/non-unique-email",                            
    "title" : "The email address is not unique",                                            
    "status" : 400,                                                                         
    "detail" : "The email 'john@smith.com' is not unique and is already registered",        
    "instance" : "http://localhost:21020/api/people",                                       
    "email" : "john@smith.com"                                                              
}

太好了,因此我们的最后一个示例有些复杂,但同时可能是最现实的示例。 我们的Web API在很大程度上依赖Bean验证 ,以确保API使用者提供的输入有效。 我们如何将验证错误表示为问题的详细信息? 最直接的方法是提供专用的ExceptionMapper提供程序,它是JAX-RS规范的一部分。 让我们介绍一个。

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
    @Context private UriInfo uriInfo;
    
    @Override
    public Response toResponse(final ValidationException ex) {
        if (ex instanceof ConstraintViolationException) {
            final ConstraintViolationException constraint = (ConstraintViolationException) ex;
            
            final ThrowableProblem problem = Problem
                    .builder()
                    .withType(URI.create("http://localhost:21020/problems/invalid-parameters"))
                    .withTitle("One or more request parameters are not valid")
                    .withStatus(Status.BAD_REQUEST)
                    .withInstance(uriInfo.getRequestUri())
                    .with("invalid-parameters", constraint
                        .getConstraintViolations()
                        .stream()
                        .map(this::buildViolation)
                        .collect(Collectors.toList()))
                    .build();

            return Response
                .status(Response.Status.BAD_REQUEST)
                .type("application/problem+json")
                .entity(problem)
                .build();
        }
        
        return Response
            .status(Response.Status.INTERNAL_SERVER_ERROR)
            .type("application/problem+json")
            .entity(Problem
                .builder()
                .withTitle("The server is not able to process the request")
                .withType(URI.create("http://localhost:21020/problems/server-error"))
                .withInstance(uriInfo.getRequestUri())
                .withStatus(Status.INTERNAL_SERVER_ERROR)
                .withDetail(ex.getMessage())
                .build())
            .build();
    }

    protected Map<?, ?> buildViolation(ConstraintViolation<?> violation) {
        return Map.of(
                "bean", violation.getRootBeanClass().getName(),
                "property", violation.getPropertyPath().toString(),
                "reason", violation.getMessage(),
                "value", Objects.requireNonNullElse(violation.getInvalidValue(), "null")
            );
    }
}

上面的代码片段区分了两种问题: ConstraintViolationException指示无效输入并映射到400 Bad Request ,而泛型ValidationException指示服务器端问题并映射到500 Internal Server Error 。 我们仅提取有关违规的基本详细信息,但是即使这样做,也可以大大改进错误报告功能。

$ curl -X POST "http://localhost:21020/api/people" \
    -H  "Accept: */*" -H "Content-Type: application/json" \
    -d '{"email":"john.smith", "firstName":"John"}' -i    

HTTP/1.1 400                                                                    
Content-Type: application/problem+json                                              
                                                                                
{                                                                               
    "type" : "http://localhost:21020/problems/invalid-parameters",                
    "title" : "One or more request parameters are not valid",                     
    "status" : 400,                                                               
    "instance" : "http://localhost:21020/api/people",                             
    "invalid-parameters" : [ 
        {
            "reason" : "must not be blank",                                             
            "value" : "null",                                                           
            "bean" : "com.example.problem.resource.PeopleResource",                     
            "property" : "register.payload.lastName"                                    
        }, 
        {                                                                          
            "reason" : "must be a well-formed email address",                           
            "value" : "john.smith",                                                     
            "bean" : "com.example.problem.resource.PeopleResource",                     
            "property" : "register.payload.email"                                       
        } 
    ]                                                                           
}

这次捆绑到invalid-parameters成员中的附加信息非常冗长:我们分别知道类( PeopleResource ),方法( register ),方法的参数( 有效负载 )和属性( lastNameemail )(所有这些都从属性路径)。

有意义的错误报告是现代RESTful Web API的基础之一。 通常这并不容易,但绝对值得付出努力。 消费者(通常只是其他开发人员)应该对哪里出了问题以及如何处理有一个清晰的了解。 RFC 7807:HTTP API的问题详细信息是朝正确方向迈出的一步, 问题问题弹簧网络之类的库在这里为您提供支持,请充分利用它们。

完整的源代码可在Github上找到

翻译自: https://www.javacodegeeks.com/2019/05/http-status-code-enough-tackling-web-apis-error-reporting.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RESTful API是一种设计风格,用于构建可伸缩的Web服务。它基于HTTP协议,并遵循一组规范来定义资源和操作。 下面是RESTful API接口规范的要点: 1. 使用有意义的URI:URI是标识资源的唯一标识符。URI应该简洁、清晰,并且描述资源的层次结构。例如,使用"/users"来表示用户资源。 2. 使用HTTP方法进行操作:HTTP定义了一组常见的操作方法,如GET、POST、PUT和DELETE。这些方法与资源的CRUD操作相对应:获取资源、创建资源、更新资源和删除资源。 3. 使用HTTP状态码:HTTP状态码提供了关于请求处理结果的信息。常见的状态码有200(成功)、201(已创建)、400(请求无效)、404(未找到)和500(服务器错误)。在API设计中,正确使用状态码可以提供清晰的响应。 4. 使用资源表示形式(Representation):资源表示形式是以某种格式(如JSON、XML)呈现资源的方式。RESTful API通常使用JSON作为默认的资源表示形式,但也可以支持其他格式。 5. 使用版本控制:当API发生变化,版本控制可以确保不会破坏现有客户端应用程序。可以通过在URI中添加版本号或使用自定义标头来实现版本控制。 6. 进行错误处理:对于错误情况,API应该返回适当的错误状态码和错误消息。可以使用HTTP状态码以及自定义错误代码来标识不同类型的错误。 7. 使用安全性措施:RESTful API应该使用适当的安全性措施,如身份验证、授权和加密,以保护数据和资源的安全性。 总结起来,RESTful API接口规范主要关注资源的唯一标识符、操作方法、状态码、资源表示形式、版本控制、错误处理和安全性措施。通过遵循这些规范,可以设计出易于理解、易于使用和易于扩展的Web API接口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值