网关修改响应码,拯救业务不规范设计

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
注册配置 MSE Nacos/ZooKeeper,118元/月
性能测试 PTS,5000VUM额度
简介: 项目中的后端接口普遍使用200响应码,无论是否出错,导致OpenFeign和第三方应用处理困难。问题在于后端开发者对HTTP基础知识理解不足,未统一处理异常时的响应码。客户端依赖响应体的`code`字段而非HTTP状态码判断请求结果。为解决这个问题,网关可扮演关键角色:

为什么需要修改?

公众号:《后端随笔》,记录后端相关文章

可能是因为在项目开始前,并没有制定标准的规范,而且开发人员对Http基本知识了解。服务端无论有没有出现异常,又或者是权限不足,一律将Http的响应码设置为200,导致无法正常使用OpenFeign以及无法适配第三方应用(依赖Http响应码)。

后端开发人员对Http基本知识存在欠缺

我在进入公司时,调试接口时发现很多的接口响应码都是200,无论处理这个请求时,有没有抛出异常,Http响应码都是200。我在看代码时,很多的处理逻辑和下面差不多。

// Controller 目前不讨论返回Json字符串对不对
@PostMapping("/addMarsNoticeToGM")
public String addMarsNoticeToGM(HttpServletRequest request, @RequestBody Notice notice) {
   
    return noticeService.postNotice(notice);
}

// Service
@Override
public String postNotice(Notice notice) {
   
    int row = noticeMapper.insert(notice);
    if (row != 0) {
   
        return ResultEnum.useResultEnum(ResultEnum.REQUESTSUCCESS);
    }
    // 将结果转换成json字符
    return ResultEnum.useResultEnum(ResultEnum.PROGRAMERROR);
}

在项目中,并没有对异常进行统一的处理,没有在发生异常时,设置Http响应码。客户端那边判断请求是否成功,是通过对响应体中的code字段进行(该code不是Http中的响应码),如果code不等于200,那么客户端就认为请求被成功的处理了。

客户端开发

公司一直是做单机游戏,很多人对Http基本上没有任何了解,比如不知道请求头有什么用,不同的Http响应码代表的意义。貌似在处理请求响应时,只对成功的请求进行处理,如果将Http响应码设置成非200- 299,可能会导致客户端无法使用。而且客户端已经迭代了几个版本,响应码问题只能由后端兼容。

后端使用OpenFeign进行通信

后端使用的是Spring Cloud Alibaba,各个服务之间是使用OpenFeign进行通信(之前后端开发人员没有使用过)。使用OpenFeign进行服务间调用时,如果被调用者未能成功处理请求并且Http响应码为200,那么会导致此调用未能进入OpenFeign的Fallback中。因为OpenFeign认为这个调用是成功的,这样会导致调用者需要额外的代码来对响应体进行判断和处理,这样是不对并且不推荐的。

// feign.AsyncResponseHandler#handleResponse
void handleResponse(CompletableFuture<Object> resultFuture,
                      String configKey,
                      Response response,
                      Type returnType,
                      long elapsedTime) {
   
    // copied fairly liberally from SynchronousMethodHandler
    boolean shouldClose = true;

    try {
   
          // 如果Http响应码是200~299,则认为该调用是成功的
        if (response.status() >= 200 && response.status() < 300) {
   
        if (isVoidType(returnType)) {
   
          resultFuture.complete(null);
        } else {
   
          final Object result = decode(response, returnType);
          shouldClose = closeAfterDecode;
          resultFuture.complete(result);
        }
        // 开启dismiss404并且Http响应码是404时,正常处理
      } else if (dismiss404 && response.status() == 404 && !isVoidType(returnType)) {
   
        final Object result = decode(response, returnType);
        shouldClose = closeAfterDecode;
        resultFuture.complete(result);
      } else {
   
        // 否则其它的Http响应码都当作异常处理
        resultFuture.completeExceptionally(errorDecoder.decode(configKey, response));
      }
    } catch (final IOException e) {
   
      if (logLevel != Level.NONE) {
   
        logger.logIOException(configKey, logLevel, e, elapsedTime);
      }
      resultFuture.completeExceptionally(errorReading(response.request(), response, e));
    } catch (final Exception e) {
   
      resultFuture.completeExceptionally(e);
    } finally {
   
      if (shouldClose) {
   
        ensureClosed(response.body());
      }
    }
  }

从上面的源码中可以看到,OpenFeign判断此次调用是否成功是便是通过Http的响应码来判断的。如果响应码没有在200~299范围内(404需要根据条件开启),OpenFeign会抛出异常。

如果设置了熔断(以Sentinel为例),那么该异常会被com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler#invoke捕获,从而调用我们自己的FallbackFactory进行异常处理。

所以如果项目使用了OpenFeign类似的框架进行服务间调用,被调用者如果未能成功处理请求(异常或执行业务逻辑失败),正确设置Http响应码是必须的。

需要解决的问题

基于上述的描述,目前需要解决的问题有:

  1. 不影响客户端,客户端接口还是需要将Http响应码设置为200(无论是否抛出异常)
  2. 使OpenFeign能够正常工作
  3. 第三方应用接口的响应码不能进行修改

网关

网关作为业务系统的入口和出口,不仅仅作为简单的门户使用,还充当着很多的角色。在Spring Cloud Gateway中,网关可以对请求进行修改,比如操作Cookie、Header、请求体等。

因为所有的请求都需要经过网关发送回客户端,所以上面三个问题我们都可以通过网关进行解决,这三个问题的解决方案为:

OpenFeign

为了能够正常使用OpenFeign,我们首先需要知道,通过OpenFeign调用其它服务时,正常来说是不经过网关的。OpenFeign会从注册中心中根据serviceId获取被调用这的IP和端口,最终是通过IP:Port/Uri去访问其它服务,不会经过网关。

所以,我们可以让各个服务正确的设置Http响应码,如果在处理请求时抛出了异常,就设置Http响应码为500。这样便能使OpenFeign能够正常工作。

客户端和第三方应用接口

目前项目的接口主要分为管理后台接口、游戏客户端接口、第三方应用接口(团队文档系统等)。因为游戏客户端的接口,不管请求是否成功,Http响应码都必须是200。而管理后台和第三方应用都能正确处理Http响应码,第三方应用对接口要求严格,比如权限不足就必须返回403。

我最终是默认将所有请求的Http响应码都修改为200,增加配置项,可以手动的配置哪些接口不需要对响应码进行修改。这样便可以兼顾游戏客户端,管理后台,第三方应用。

而且因为OpenFeign的调用不会经过网关,所以在网关中将Http响应码设置为200并不会影响到OpenFengin的正常使用。

代码

在Spring Cloud Gateway中修改响应码很简单,只需要创建一个ModifyResponseStatusFilter,实现GlobalFilter接口就可以了,该接口采用的是责任链模式。

但是需要注意修改的时机,如果响应已经写回给客户端了,那么在此之后,便不能对响应码进行修改。

在Gateway中,会存在很多的GloablFilter。我们可以实现Ordered接口,通过合理的返回org.springframework.core.Ordered#getOrder值,便可以控制自己的Filter在哪个Filter之前或之后执行。getOrder()的返回值越小,GolbalFilter#filter方法便会越早执行并且响应会越晚经过。

源码分析

设置响应码的方法为exchange.getResponse().setStatusCode(),setStatusCode方法源码如下:

private final AtomicReference<State> state = new AtomicReference<>(State.NEW);

@Override
public boolean setStatusCode(@Nullable HttpStatus status) {
   
    if (this.state.get() == State.COMMITTED) {
   
        return false;
    }
    else {
   
        this.statusCode = (status != null ? status.value() : null);
        return true;
    }
}

protected Mono<Void> doCommit(@Nullable Supplier<? extends Mono<Void>> writeAction) {
   
    Flux<Void> allActions = Flux.empty();
    if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
   
        ....
    }
    ....

    return allActions.then();
}

通过源码可以知道,调用setStatusCode修改Http响应码,必须在status的值变为State.COMMITTED之前设置才有效。status是使用doCommit方法进行修改的,而doCommit是在NettyWriteResponseFilter这个过滤器中被调用,其源码为:

public class NettyWriteResponseFilter implements GlobalFilter, Ordered {
   
    @Override
    public int getOrder() {
   
        return -1;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   
        return chain.filter(exchange)
                .doOnError(throwable -> cleanup(exchange))
                .then(Mono.defer(() -> {
   
                    ....
                    return (isStreamingMediaType(contentType)
                            // writeAndFlushWith和writeWith都会调用doCommit,具体看AbstractServerHttpResponse
                            ? response.writeAndFlushWith(body.map(Flux::just))
                            : response.writeWith(body));
                })).doOnCancel(() -> cleanup(exchange));
    }

}

从源码中可以看到,NettyWriteResponseFilter的getOrder方法的返回值为-1,也就是说,我们如果要在自己的ModifyResponseStatusFilter中成功的调用exchange.getResponse().setStatusCode()修改Http响应码,就必须保证ModifyResponseStatusFilter在NettyWriteResponseFilter之前执行,所以只需要保证ModifyResponseStatusFilter#getOrder() > NettyWriteResponseFilter#getOrder()值就可以了。

最终代码

public class XcyeGatewayProperties {
   

    /**
     * 网关会自动将所有返回的响应码转换为200,如果存在不需要转换的请求头,请在此排除,支持ant模式
     */
    private List<String> excludeAdapterResponseStatusUriList;
}
public class ModifyResponseStatusFilter implements GlobalFilter, Ordered {
   

    @Resource
    private XcyeGatewayProperties xcyeGatewayProperties;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
   
            String methodName = exchange.getRequest().getMethodValue();
            URI uri = exchange.getRequest().getURI();
            String path = methodName + ":" + uri.getPort() + ":" + uri.getPath();
            HttpStatus statusCode = exchange.getResponse().getStatusCode();
            String uriPath = uri.getPath();
            List<String> excludeAdapterResponseStatusUriList = xcyeGatewayProperties.getExcludeAdapterResponseStatusUriList();
            if (excludeAdapterResponseStatusUriList != null && !excludeAdapterResponseStatusUriList.isEmpty()) {
   
                for (String excludeUri : excludeAdapterResponseStatusUriList) {
   
                    if (!excludeUri.startsWith("/")) {
   
                        excludeUri = "/" + excludeUri;
                    }

                    if (pathMatcher.matches(excludeUri, uriPath)) {
   
                        return;
                    }
                }
            }
            boolean status = exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
            log.info("请求 {} 返回的原始响应状态码为: {}, ModifyResponseStatusFilter修改响应状态码为200的状态为 {}", path, statusCode.value(), status);
        }));
    }

    @Override
    public int getOrder() {
   
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
    }
}

最后

对请求和响应进行修改,是网关最基础的功能。上面在网关中修改Http响应码我个人是不推荐,客户端和服务端之间的Http通信以及响应体结构等,我觉得一开始就应该规定好,并且要符合标准,否则的话,随着项目的迭代,后续都不知道会存在什么问题,而且应该越早改正。

目录
相关文章
|
6月前
|
Prometheus 网络协议 JavaScript
api 网关 kong 数据库记录请求响应报文
Kong的tcp-log-with-body插件是一个高效的工具,它能够转发Kong处理的请求和响应。这个插件非常适用于需要详细记录API请求和响应信息的情景,尤其是在调试和排查问题时。
181 0
api 网关 kong 数据库记录请求响应报文
|
前端开发 Java 应用服务中间件
Gateway网关使用不规范,同事加班泪两行~
Gateway网关使用不规范,同事加班泪两行~
Gateway网关使用不规范,同事加班泪两行~
|
1月前
|
缓存 网络协议 API
【Azure 环境】请求经过应用程序网关,当响应内容大时遇见504超时报错
应用程序网关的响应缓冲区可以收集后端服务器发送的全部或部分响应数据包,然后再将它们发送给客户端。 默认在应用程序网关上启用响应缓冲,这对于适应缓慢的客户端很有用。
|
3月前
|
Java Sentinel Spring
网关修改响应码,拯救业务不规范设计
本文探讨了在一个未遵循HTTP标准规范的项目中遇到的问题及解决方案。
|
4月前
|
监控 负载均衡 Java
深入理解Spring Cloud中的服务网关
深入理解Spring Cloud中的服务网关
|
1月前
|
安全 5G 网络性能优化
|
2月前
|
监控 负载均衡 安全
微服务(五)-服务网关zuul(一)
微服务(五)-服务网关zuul(一)
|
3月前
|
运维 Kubernetes 安全
利用服务网格实现全链路mTLS(一):在入口网关上提供mTLS服务
阿里云服务网格(Service Mesh,简称ASM)提供了一个全托管式的服务网格平台,兼容Istio开源服务网格,用于简化服务治理,包括流量管理和拆分、安全认证及网格可观测性,有效减轻开发运维负担。ASM支持通过mTLS提供服务,要求客户端提供证书以增强安全性。本文介绍如何在ASM入口网关上配置mTLS服务并通过授权策略实现特定用户的访问限制。首先需部署ASM实例和ACK集群,并开启sidecar自动注入。接着,在集群中部署入口网关和httpbin应用,并生成mTLS通信所需的根证书、服务器证书及客户端证书。最后,配置网关上的mTLS监听并设置授权策略,以限制特定客户端对特定路径的访问。
130 2
|
11天前
|
负载均衡 Java 应用服务中间件
Gateway服务网关
Gateway服务网关
24 1
Gateway服务网关
|
1月前
|
前端开发 Java API
vertx学习总结5之回调函数及其限制,如网关/边缘服务示例所示未来和承诺——链接异步操作的简单模型响应式扩展——一个更强大的模型,特别适合组合异步事件流Kotlin协程
本文是Vert.x学习系列的第五部分,讨论了回调函数的限制、Future和Promise在异步操作中的应用、响应式扩展以及Kotlin协程,并通过示例代码展示了如何在Vert.x中使用这些异步编程模式。
47 5
vertx学习总结5之回调函数及其限制,如网关/边缘服务示例所示未来和承诺——链接异步操作的简单模型响应式扩展——一个更强大的模型,特别适合组合异步事件流Kotlin协程