You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RestTemplate处理application/octet-stream响应报错求助

问题描述

调用的API返回二进制响应(Content-Type为application/octet-stream),Controller需返回ResponseEntity<byte[]>给前端。最初用泛型ResponseEntity<V>接收响应时出现解析错误,改成ResponseEntity<byte[]>后仍遇到JSON解析错误,提示意外字符'%'(编码37),无法解析为合法JSON值。


初始方法代码

@Override
public <V> ResponseEntity<V> getBillingDocPDFFromGCP(String url, HttpMethod method, HttpServletRequest request, String accessToken, Class<V> responseType, Map<String, String> params, BaseStoreModel baseStore, String appDomain) {
    if (StringUtils.isEmpty(url)) {
        throw new ConfigurationException(HttpStatus.NOT_ACCEPTABLE + " : Missing API Uri property in configuration");
    }
    LOG.info("getGCPBillingDocPDF API URL...{}", url);
    MultiValueMap<String, String> headers = getServiceHeadersForGCPPDF(baseStore, accessToken);
    final HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
    final RestTemplate restTemplate = new RestTemplate();
    List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
    messageConverters.add(converter);
    restTemplate.setMessageConverters(messageConverters);

    try {
        return (ResponseEntity<V>) restTemplate.exchange(url, method, requestEntity, responseType);

    } catch (final HttpClientErrorException httpClientException) {
        LOG.error(UNAUTHORIZED_ERROR + " : " + httpClientException.getMessage(), httpClientException);
        throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED, httpClientException.getMessage());
    } catch (final RestClientException restClientException) {
        LOG.error(REST_CLIENT_ERROR, restClientException);
        throw new RestClientException(REST_CLIENT_REPORTED_ERROR + " : " + restClientException.getMessage(), restClientException);
    }
}

public MultiValueMap<String, String> getServiceHeadersForGCPPDF(final BaseStoreModel baseStore, final String aceToken) {
    final HttpHeaders headers = new HttpHeaders();
    final String siteId = getCurrentSiteId(baseStore);
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    headers.setContentType(MediaType.parseMediaType("application/octet-stream"));
    headers.add("X-API-Key", value);
    return headers;
}

初始错误信息

Rest Client Error org.springframework.web.client.RestClientException: Error while extracting response for type [class java.lang.Object] and content type [application/octet-stream]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected character ('%' (code 37)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is


修改后的方法代码

@Override
public <V> ResponseEntity<byte[]> getBillingDocPDFFromGCP(String url, HttpMethod method, HttpServletRequest request, String accessToken, Class<V> responseType, Map<String, String> params, BaseStoreModel baseStore, String appDomain) {
    if (StringUtils.isEmpty(url)) {
        throw new ConfigurationException(HttpStatus.NOT_ACCEPTABLE + " : Missing API Uri property in configuration");
    }
    LOG.info("getGCPBillingDocPDF API URL...{}", url);
    MultiValueMap<String, String> headers = getServiceHeadersForGCPPDF(baseStore, accessToken);
    final HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
    final RestTemplate restTemplate = new RestTemplate();
    List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
    messageConverters.add(converter);
    restTemplate.setMessageConverters(messageConverters);

    try {
        return  restTemplate.exchange(url, method, requestEntity, byte[].class);

    } catch (final HttpClientErrorException httpClientException) {
        LOG.error(UNAUTHORIZED_ERROR + " : " + httpClientException.getMessage(), httpClientException);
        throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED, httpClientException.getMessage());
    } catch (final RestClientException restClientException) {
        LOG.error(REST_CLIENT_ERROR, restClientException);
        throw new RestClientException(REST_CLIENT_REPORTED_ERROR + " : " + restClientException.getMessage(), restClientException);
    }
}

修改后的错误信息(翻译后)

导致错误的原因:org.springframework.web.client.RestClientException: 提取类型为[class [B]、内容类型为[application/octet-stream]的响应时出错;嵌套异常为org.springframework.http.converter.HttpMessageNotReadableException: JSON解析错误:意外字符('%'(编码37)):应为合法值(JSON字符串、数字、数组、对象或'true'/'false'/'null'标记);嵌套异常为com.fasterxml.jackson.core.JsonParseException: 意外字符('%'(编码37)):应为合法值(JSON字符串、数字、数组、对象或'true'/'false'/'null'标记)


解决方案

问题根源是你给RestTemplate仅配置了MappingJackson2HttpMessageConverter,且设置它支持所有媒体类型(MediaType.ALL),导致二进制响应也被Jackson当作JSON解析——而PDF文件头的%字符不符合JSON格式,因此抛出错误。

解决步骤

  1. 恢复字节数组转换器:ByteArrayHttpMessageConverter是RestTemplate默认自带的二进制处理转换器,你之前手动设置messageConverters时将其覆盖,需重新添加。
  2. 调整转换器顺序:把ByteArrayHttpMessageConverter放在Jackson转换器前面,让二进制响应优先被正确处理。
  3. 修正请求头:将Accept头改为application/octet-stream,明确告知服务器你期望接收二进制数据,而非JSON。

修改后代码示例

@Override
public ResponseEntity<byte[]> getBillingDocPDFFromGCP(String url, HttpMethod method, HttpServletRequest request, String accessToken, Class<?> responseType, Map<String, String> params, BaseStoreModel baseStore, String appDomain) {
    if (StringUtils.isEmpty(url)) {
        throw new ConfigurationException(HttpStatus.NOT_ACCEPTABLE + " : Missing API Uri property in configuration");
    }
    LOG.info("getGCPBillingDocPDF API URL...{}", url);
    MultiValueMap<String, String> headers = getServiceHeadersForGCPPDF(baseStore, accessToken);
    final HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
    final RestTemplate restTemplate = new RestTemplate();
    
    // 重新配置消息转换器
    List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    // 优先添加字节数组转换器,处理二进制响应
    messageConverters.add(new ByteArrayHttpMessageConverter());
    // 添加Jackson转换器,仅处理JSON响应
    MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
    jacksonConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
    messageConverters.add(jacksonConverter);
    
    restTemplate.setMessageConverters(messageConverters);

    try {
        return restTemplate.exchange(url, method, requestEntity, byte[].class);

    } catch (final HttpClientErrorException httpClientException) {
        LOG.error(UNAUTHORIZED_ERROR + " : " + httpClientException.getMessage(), httpClientException);
        throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED, httpClientException.getMessage());
    } catch (final RestClientException restClientException) {
        LOG.error(REST_CLIENT_ERROR, restClientException);
        throw new RestClientException(REST_CLIENT_REPORTED_ERROR + " : " + restClientException.getMessage(), restClientException);
    }
}

public MultiValueMap<String, String> getServiceHeadersForGCPPDF(final BaseStoreModel baseStore, final String aceToken) {
    final HttpHeaders headers = new HttpHeaders();
    final String siteId = getCurrentSiteId(baseStore);
    // 修正Accept头为二进制流类型
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM));
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    headers.add("X-API-Key", value); // 注意替换为实际API密钥变量
    return headers;
}

额外说明

  • 移除了方法上多余的泛型<V>,因返回类型已固定为ResponseEntity<byte[]>,泛型参数不再需要。
  • 确保ByteArrayHttpMessageConverter优先处理application/octet-stream类型响应,避免Jackson错误解析二进制数据。

内容的提问来源于stack exchange,提问作者Karthik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 05:10:21