Spring Boot 3 RestClient发送对象请求失败,字符串请求正常
Spring Boot 3 RestClient POST请求分块编码导致403问题排查与解决
问题现象
使用Spring Boot 3的Http RestClient发送POST请求时,字符串格式的请求体可正常返回200,但发送AddAppRole对象时返回403 Forbidden。对比HTTP日志可见:
- 字符串请求:通过
Content-Length指定内容长度,无分块编码 - 对象请求:采用
Transfer-Encoding: chunked编码,请求体中出现7f、0这类分块标识,触发服务端异常拦截
原因分析
RestClient处理对象请求体时,默认以流式方式序列化对象,无法提前计算请求体总长度,因此自动启用分块编码。而目标服务端可能对分块编码的请求做了拦截校验,或是无法正确解析分块格式的请求体,最终返回403。
解决方法
1. 手动序列化对象为字符串后发送
借助Jackson将AddAppRole对象序列化为JSON字符串,再传入body()方法,让RestClient自动计算Content-Length,避免分块编码:
// 注入Spring管理的ObjectMapper @Autowired private ObjectMapper objectMapper; // 序列化对象为JSON字符串 String roleJson = objectMapper.writeValueAsString(addAppRole); ResponseEntity<ApiResponse> result = restClient .post() .uri("dev/v1/app-roles") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .body(roleJson) .retrieve() .toEntity(ApiResponse.class);
2. 全局配置RestClient禁用分块编码
通过自定义底层HttpClient的请求配置,全局禁用分块编码。注意此方式会影响所有请求,大请求体场景可能导致内存占用过高:
@Bean public RestClient restClient() { CloseableHttpAsyncClient httpClient = HttpAsyncClients.custom() .setDefaultRequestConfig(RequestConfig.custom() .setChunkedEnabled(false) // 禁用分块编码 .build()) .build(); ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); return RestClient.builder() .requestFactory(requestFactory) .build(); }
3. 提前计算请求体长度并手动设置
先将对象序列化为字节数组,计算长度后手动设置Content-Length请求头:
byte[] requestBodyBytes = objectMapper.writeValueAsBytes(addAppRole); ResponseEntity<ApiResponse> result = restClient .post() .uri("dev/v1/app-roles") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(requestBodyBytes.length)) .body(requestBodyBytes) .retrieve() .toEntity(ApiResponse.class);
原问题相关代码与日志
字符串请求成功代码
private final String roleBody = """ {"ApplicationID":"XXXX","RolesValues":["ROLE"],"retainPreviousRoles":"true"} """; ResponseEntity<ApiResponse> result = restClient .post() .uri("dev/v1/app-roles") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .body(roleBody) .retrieve() .toEntity(ApiResponse.class);
字符串请求日志
User-Agent: Apache-HttpClient/5.2.3 (Java/17.0.11) "POST /dev/v1/app-roles HTTP/1.1[\r][ ]" "Content-Type: application/json[\r][ ]" "Accept: application/json[\r][ ]" "Accept-Encoding: gzip, x-gzip, deflate[\r][ ]" "Content-Length: 128[\r][ ]" "Host: appregapi.bpglobal.com[\r][ ]" "Connection: keep-alive[\r][ ]" "User-Agent: Apache-HttpClient/5.2.3 (Java/17.0.11)[\r][ ]" "[\r][ ]" "{\"ApplicationID\":\"XXXX\",\"RolesValues\":[\"ROLE\"],\"retainPreviousRoles\":\"true\"}[ ]" "HTTP/1.1 200 OK[\r][ ]" "Transfer-Encoding: chunked[\r][ ]" "Content-Type: application/json; charset=utf-8[\r][ ]" "Content-Encoding: gzip[\r][ ]" "Vary: Accept-Encoding[\r][ ]" "Date: Sat, 08 Jun 2024 20:22:09 GMT[\r][ ]" "[\r][ ]" "44[\r][ ]"
对象请求失败代码
AddAppRole addAppRole = AddAppRole.builder() .applicationId("XXXX") .rolesValues(List.of("ROLE")) .retainPreviousRoles("true") .build(); ResponseEntity<ApiResponse> result = restClient .post() .uri("dev/v1/app-roles") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) .body(addAppRole) .retrieve() .toEntity(ApiResponse.class);
对象请求日志
User-Agent: Apache-HttpClient/5.2.3 (Java/17.0.11) "POST /dev/v1/app-roles HTTP/1.1[\r][ ]" "Content-Type: application/json[\r][ ]" "Accept: application/json[\r][ ]" "Accept-Encoding: gzip, x-gzip, deflate[\r][ ]" "Transfer-Encoding: chunked[\r][ ]" "Host: appregapi.bpglobal.com[\r][ ]" "Connection: keep-alive[\r][ ]" "User-Agent: Apache-HttpClient/5.2.3 (Java/17.0.11)[\r][ ]" "[\r][ ]" "7f[\r][ ]" "{\"ApplicationID\":\"XXXX\",\"RolesValues\":[\"ROLE\"],\"retainPreviousRoles\":\"true\"}[\r][ ]" "0[\r][ ]" "[\r][ ]" "HTTP/1.1 403 Forbidden[\r][ ]" "Transfer-Encoding: chunked[\r][ ]" "Content-Type: application/json; charset=utf-8[\r][ ]" "Date: Sat, 08 Jun 2024 20:23:35 GMT[\r][ ]" "[\r][ ]" "46[\r][ ]" "{[\r][ ]" HTTP/1.1 403 Forbidden
AddAppRole类定义
@Builder @JsonPropertyOrder(value = {"ApplicationID", "RolesValues", "retainPreviousRoles"}) public class AddAppRole { @JsonProperty("ApplicationID") private final String applicationId; @JsonProperty("RolesValues") private final List<String> rolesValues; private final String retainPreviousRoles; }
内容的提问来源于stack exchange,提问作者user25486013
相关产品推荐
相关产品推荐

