调用Feign接口请求头丢失问题处理

业务背景

A 服务调用 B 服务的 feign 接口报错,通过日志定位到如下代码,该接口会从请求头中获取用户信息进行校验,由于其中名为 partyId 的请求头为 null,导致 URLDecoder.decode 方法报空指针异常。

public static LoginUser getLoginUser() {
  LoginUser loginUser = new LoginUser();

  try {
    HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String partyId = URLDecoder.decode(httpServletRequest.getHeader("partyId"), "UTF-8");
    loginUser.setPartyId(partyId);
    } catch (UnsupportedEncodingException var7) {
    }

    return loginUser;
}

问题排查

由于该请求头非业务参数却有必填需求,猜测正常会由网关服务赋值,检查相关拦截器后发现该值默认为空字符串,但 B 服务获取时却为 null,显然从网关到 A 服务,再到 B 服务的传递过程中存在问题。检查 A 服务代码发现一段获取请求头后调用 feign 的 requestTemplate.head 进行赋值的代码。如下为简化代码。

/**
 * 包含的header集合
 */
Set<String> headers = new HashSet<String>() {{
    add("partyId");
}};

/**
 * 包含则返回true
 *
 * @param headerName
 * @return
 */
public boolean hasHeader(String headerName) {
    return headers.contains(headerName);
}

/**
 * 使用feign client发送请求时,传递header
 *
 * @return
 */
@Bean
public RequestInterceptor requestInterceptor() {
    return requestTemplate -> {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();

            if (headerNames != null) {
                String headerName;
                while (headerNames.hasMoreElements()) {
                    headerName = headerNames.nextElement();
                    String value = request.getHeader(headerName);
                    if (hasHeader(headerName)) {
                      requestTemplate.header(headerName, value);
                    }
                }
            }
            FeignHeaderUtil.remove();
        }
    };
}

通过在中间增加日志打印发现,A 服务获取到的请求头信息中 partyId 就已经为null。搜索网关服务请求头丢失相关问题后,找到如下博文(还有其他内容相同的,不清楚原创是谁)。获取header 请求参数_APP 莫名崩溃,开始以为是 Header 中 name 大小写的锅,最后发现原来是容器的错!...-CSDN博客

检查了三个服务的配置,发现网关使用的是 tomcat,A、B 服务则为 undertow,符合文中描述。

参考文中信息并监控各服务接口后得出以下结论:

1.tomcat 容器会把请求头key值全部转成小写,undertow 容器不会转成小写

2.tomcat 容器对请求头忽略大小写,undertow 容器不忽略大小写

3.value 为空字符串时会被 feign 的 requestTemplate.header 方法过滤

tomcat 会把驼峰格式的头信息转换成小写进行传递,而在接收时又会忽略大小写,因此使用tomcat 容器的服务间互相调用不会报错,但 undertow 容器会区分大小写,导致未能正常传递。通过打印日志发现 A 服务接收到的头信息中 partId 确实变为了 partyid。此处匹配的头信息不包含 partyid,因此驼峰与小写的头信息均未传递到 B 服务。

到此解决问题的思路就清晰了,A、B 服务都是使用 undertow 容器,只需把 partyid 的值重新赋给 partyId 传递过去即可。如果 B 服务使用的是tomcat容器,也可以把 partyid 加入到如上匹配规则中。改写代码如下。

@Bean
public RequestInterceptor requestInterceptor() {
    return requestTemplate -> {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();

            if (headerNames != null) {
                String headerName;
                while (headerNames.hasMoreElements()) {
                    headerName = headerNames.nextElement();
                    String value = request.getHeader(headerName);
                    if ("partyid".equals(headerName) && StringUtils.isBlank(value)) {
                        requestTemplate.header("partyId", "");
                    }
                    if (hasHeader(headerName)) {
                      requestTemplate.header(headerName, value);
                    }
                }
            }
            FeignHeaderUtil.remove();
        }
    };
}

然而调用接口后 B 服务仍未接收到该值,已进行赋值操作的情况下,可以推测出该请求头被过滤了,也就是后续的 requestTemplate.header 操作有问题,点进去一层层查看源码,忽略无关代码,最终找到如下语句。

// feign.RequestTemplate#appendHeader
private RequestTemplate appendHeader(String name, Iterable<String> values) {
    if (!values.iterator().hasNext()) {
        this.headers.remove(name);
        return this;
    } else if (name.equals("Content-Type")) {
        this.headers.remove(name);
        this.headers.put(name, HeaderTemplate.create(name, Collections.singletonList(values.iterator().next())));
        return this;
    } else {
        this.headers.compute(name, (headerName, headerTemplate) -> {
            return headerTemplate == null ? HeaderTemplate.create(headerName, values) : HeaderTemplate.append(headerTemplate, values);
        });
        return this;
    }
}

// feign.template.HeaderTemplate#append
public static HeaderTemplate append(HeaderTemplate headerTemplate, Iterable<String> values) {
    LinkedHashSet<String> headerValues = new LinkedHashSet(headerTemplate.getValues());
    headerValues.addAll((Collection)StreamSupport.stream(values.spliterator(), false).filter(Util::isNotBlank).collect(Collectors.toCollection(LinkedHashSet::new)));
    return create(headerTemplate.getName(), headerValues);
}

可以看到在存储头信息value值时,feign的源码通过jdk8的stream流筛选了所有不为空字符串的值,导致虽然设置了驼峰格式的请求头,原先为空字符串的值却仍为null。

解决方案

对所需传递的请求头进行赋值,由于调用的 B 服务接口并未对该值进行使用,将网关传递的空字符串改为任意非空字符串即可。如果 B 服务使用的是tomcat容器,仅传递 partyid 是同样适用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值