【Feign请求头丢失问题】no suitable HttpMessageConverter found for response type

省流助手

HttpMessageConverter 失败的原因是:在项目中使用了一个拦截器拦截请求,部分接口需要登陆才能访问,否则返回一个text/html格式的响应,导致远程服务解析响应失败。
登陆失败的原因是:Feign发起远程调用的时候会重新生成一个新的请求,带来的问题就是不会携带原来请求的cookie,导致调用需要登陆的远程接口时会失败。解决方法是配置一个Feign的拦截器,在发送请求的时候带上原请求的cookie。
本文主要内容是围绕这个问题展开的一系列知识点,包括但不限于:

  • http的content type
  • 微服务联调debug
  • 查看Feign日志
  • 登陆拦截器
  • Fegin丢头问题

问题分析定位

今天在联调两个微服务的时候发现远程接口总是返回以下报错:

Could not extract response: no suitable HttpMessageConverter found for response type [class top.dumbzarro.greensource.common.utils.R] and content type [text/html;charset=UTF-8]

意思是没有一个HttpMessageConverter 可以将 [text/html;charset=UTF-8]转化为[class top.dumbzarro.greensource.common.utils.R] 。
其中,R是项目中定义的一个通用的返回对象,所有接口都返回这个对象。

远程接口在ware服务,详细如下:

@FeignClient("greensource-member")
public interface MemberFeignService {
    @GetMapping("/memberreceiveaddress/info/{id}")
    R info(@PathVariable("id") Long id);
}

被调用接口在member服务,详细如下:

@RestController
@RequestMapping("memberreceiveaddress")
public class MemberReceiveAddressController {
	@Autowired
    private MemberReceiveAddressService memberReceiveAddressService;

    @GetMapping("/info/{id}")
    //@RequiresPermissions("member:memberreceiveaddress:info")
    public R info(@PathVariable("id") Long id){
		MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
		return R.ok().setData(memberReceiveAddress);
    }
}

比较疑惑的是,在联调这两个服务之前,已经调通了auth服务和member服务、auth服务和third-party服务,两个服务之间的Feign远程调用就没有问题。

网上对于no suitable HttpMessageConverter的解决方案就是添加一个自定义的转换器等等。但是隐约感觉这不是类型转换的问题,不然在没有额外配置的情况下,之前的服务不可能跑的通。

HTTP Content-type

Content-type是HTTP协议中的一个字段,Content-Type 标头告诉客户端实际返回的内容的内容类型。
常见的有:

  • text/html: HTML格式,浏览器在获取到这种文件时会自动调用html的解析器对文件进行渲染的处理。
  • text/plain:将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。
  • application/json: JSON数据格式,浏览器不会对其进行处理。

TODO Content-type springmvc fegin的默认content-type

印象里接口都是返回json数据,content-type是application/json,怎么会突然冒出个text/html呢。于是使用全局搜索查了一下。
在这里插入图片描述
突然想起在部分需要登陆的业务中都增加了一个拦截器,用于判断用户是否登陆,在判断用户没有登陆的时候会返回一个text/html的响应。详细代码如下。

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        if(uri.equals("/error")){
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误');</script>");
            return false;
        }

        boolean match = new AntPathMatcher().match("/member/**", uri);
        if (match) { // member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
            return true;
        }

        HttpSession session = request.getSession();

        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);

        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);

            return true;
        } else {
            //未登录,返回登录页面
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.dumbzarro.top/login.html'</script>");
            return false;
        }
    }

在feign的请求的时候,被判定为没有登陆,所以返回了这个“text/html”格式的数据,而在远程接口处我们使用的是R进行接受,自然就无法成功解析然后就会出现报错。
正常来说这里应该返回的是一个application对象,由于这个项目是基于谷粒商城修改的,谷粒商城是前后端不分离了,而后续这个项目使用的是前后端分离的结构,所以这里将这个返回值做一个修改,即可解决这个报错了。
可参考如下代码修改

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        if(uri.equals("/error")){
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = response.getWriter();

            out.println(JSONObject.toJSONString(R.error()
                            .put("error","uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误"),
                    SerializerFeature.WriteMapNullValue,
                    SerializerFeature.WriteDateUseDateFormat));
            return false;
        }

        boolean match = new AntPathMatcher().match("/member/**", uri);
        if (match) { // member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
            return true;
        }

        HttpSession session = request.getSession();

        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);

        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);

            return true;
        } else {
            //未登录
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = response.getWriter();
            out.println(JSONObject.toJSONString(
                    R.error().put("error","用户未登录"),
                    SerializerFeature.WriteMapNullValue,
                    SerializerFeature.WriteDateUseDateFormat
                    )
            );
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

用户未登录

虽然不会报转换异常,但是还会返回“用户未登录”。
在这里插入图片描述
可以确保的是我在swagger已经登陆了,请求的时候带上了cookie了的,但是经过fegin之后就显示没有登陆,而仅仅是ware服务的这个接口报错,而auth和third-party都不会报错。

微服务联调

因为单独去测试member服务的时候都没有问题,于是就想看直接请求member服务和从ware服务器请求member的请求有什么不同,于是打算在两个服务都打断点看看。注意,如果你同一个服务有多个实例注册在nacos上,那么要在@FeignClient加入url的参数,去指定到本地的服务,否则请求可能会打到其他的机器上,导致没办法debug到当前的机器上。当然,如果只有一个实例,其实不用加也可以。示例如下:

//@FeignClient(value="greensource-member")
@FeignClient(value="greensource-member",url="localhost:7000")// 指定某台机器
public interface MemberFeignService {
    @GetMapping("/memberreceiveaddress/info/{id}")
    R info(@PathVariable("id") Long id);
}

这时候启动服务,开始debug,发现程序不会经过接口调用处经过,而是在member的登陆拦截器处被判定为没有登陆,直接返回到ware服务。
查看请求,发现此时没有session,没有登陆成功。
在这里插入图片描述

打开fegin 日志

我们配置一个FeginConfig,查看fegin的请求响应情况

@Configuration
public class FeignConfig {

    @Bean
    public feign.Logger logger() {
        return new Slf4jLogger();
    }
    @Bean
    public Logger.Level level() {
        return Logger.Level.FULL;
    }
}

在application.yml配置打印日志

logging:
  level:
    feign.Logger: debug

log4j定义了8个级别的log,优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。log4j默认的优先级为ERROR。Log4j建议只使用ERROR、WARN、INFO、DEBUG这四个级别(优先级从高到低)。如果将log level设置在某一个级别上,那么比此级别优先级高的log都能打印出来。

  1. ALL:最低等级的,用于打开所有日志记录。
  2. TRACE:很低的日志级别,一般不会使用。
  3. DEBUG:指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。
  4. INFO:消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志。
  5. WARN:表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示。
  6. ERROR:打印错误和异常信息,指出虽然发生错误事件,但仍然不影响系统的继续运行。
  7. FATAL:指出每个严重的错误事件将会导致应用程序的退出。重大错误,这种级别可以直接停止程序了。
  8. OFF:最高等级的,用于关闭所有日志记录。

可以看到我们的请求是没有设置cookie的
在这里插入图片描述
这就是fegin请求失败的根本原因,所以我们在ware配置fegin发送请求是带上cookie。

Feign丢失cookie问题

由于fegin每次请求都会自己发一个新的请求,而不会带上我们之前的请求的cookie,这时候我们就要手动配置一下。在之前设置debug的地方继续添加配置,注入一个拦截器到spring容器中,在Feign请求之前我们设置一下cookie

@Configuration
public class FeignConfig {

    @Bean
    public feign.Logger logger() {
        return new Slf4jLogger();
    }
    @Bean
    public Logger.Level level() {
        return Logger.Level.FULL;
    }

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    //2、同步请求头的数据(主要是cookie)
                    //把老请求的cookie值放到新请求上
                    String cookie = request.getHeader("Cookie");
                    template.header("Cookie", cookie);
                }
            }
        };
        return requestInterceptor;
    }
}

查看日志,发现请求已经成功带上了cookie
在这里插入图片描述
按道理来说两个请求应该是一个cookie和session的,但是这里却发现两个session不一致。
大概是登陆超时了,过期了,从新登陆一下就好了。
在这里插入图片描述
成功返回了消息。

为什么之前的微服务不会出现问题?

之前调通了auth-server和third-party 以及 auth-server 和 member,都没有出现类似的问题。
前者的原因是third-party没有登陆拦截器,因此auth-server 调用third-party的时候不会返回text/html的内容,因此能正常解析。既然没有登陆拦截器,那么有无cookie也不影响远程调用。
后者的原因是虽然member有登陆拦截器,但是因为auth-server请求的接口是放行的(详细见上面的代码),所以也不会返回text/html的返回值,因此也能正常解析。同时有因为接口不需要登陆认证的cookie,fegin请求头的cookie丢失了也不影响。

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值