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格式,因此抛出错误。
解决步骤
- 恢复字节数组转换器:
ByteArrayHttpMessageConverter是RestTemplate默认自带的二进制处理转换器,你之前手动设置messageConverters时将其覆盖,需重新添加。 - 调整转换器顺序:把
ByteArrayHttpMessageConverter放在Jackson转换器前面,让二进制响应优先被正确处理。 - 修正请求头:将
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

