Spring Boot 3.1.3升级至3.1.4后Gzip解压内容截断问题求助
Spring Boot 3.1.4+升级后Gzip解压内容截断问题排查与解决建议
问题概述
将Spring Boot从3.1.3升级到3.1.4及更高版本后,API在解压Gzip压缩数据时出现内容截断问题。调试发现:StringHttpMessageConverter.readInternal调用时,请求头中的Content-Length为压缩后的377字节,但解压后实际内容应为1076字节,导致请求体被截断。问题根源是Spring Framework的一项内部变更,该变更让消息转换器更严格依赖Content-Length值来读取请求内容。
相关代码
调用方Gzip压缩代码
private fun getGzip(body: ByteArray?): ByteArray { ByteArrayOutputStream().use { byteStream -> GZIPOutputStream(byteStream).use { zipStream -> zipStream.write(body!!) zipStream.finish() return byteStream.toByteArray() } } }
应用内Gzip解压过滤器代码
@Component @Order(0) open class GzipDecompressFilter : OncePerRequestFilter() { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { var httpServletRequest = request val contentEncoding = httpServletRequest.getHeader("Content-Encoding") if (contentEncoding != null && contentEncoding.indexOf("gzip") > -1) { try { val decompressStream: InputStream = StreamHelper.decompressStream(httpServletRequest.inputStream) httpServletRequest = object : HttpServletRequestWrapper(httpServletRequest) { override fun getInputStream(): ServletInputStream { return DecompressServletInputStream(decompressStream) } override fun getReader(): BufferedReader { return BufferedReader(InputStreamReader(decompressStream)) } // 新增:重写内容长度相关方法,返回-1表示未知长度 override fun getContentLength(): Int { return -1 } override fun getContentLengthLong(): Long { return -1 } } } catch (_: IOException) { } } chain.doFilter(httpServletRequest, response) } } open class DecompressServletInputStream(private val inputStream: InputStream) : ServletInputStream() { private var finished = false override fun read(): Int { val data = inputStream.read() if (data == -1) { finished = true } return data } override fun isFinished(): Boolean { return finished } override fun isReady(): Boolean { return true } override fun setReadListener(listener: ReadListener?) { throw UnsupportedOperationException() } } object StreamHelper { private const val GZIP_ID1: Byte = 0x1f private const val GZIP_ID2 = 0x8b.toByte() fun decompressStream(input: InputStream?): InputStream { val pushbackInput = PushbackInputStream(input, 2) val signature = ByteArray(2) // 补充处理读取不足2字节的情况 val readBytes = pushbackInput.read(signature) if (readBytes == 2) { pushbackInput.unread(signature) return if (signature[0] == GZIP_ID1 && signature[1] == GZIP_ID2) { GZIPInputStream(pushbackInput) } else pushbackInput } else { pushbackInput.unread(signature, 0, readBytes) return pushbackInput } } }
控制器简化代码
@CrossOrigin @RestController @RequestMapping( value = ["/api"], method = [RequestMethod.POST], consumes = [MediaType.APPLICATION_JSON_VALUE], produces = ["application/json;charset=UTF-8"] ) class ServiceController { @Autowired private lateinit var serviceBi: ServiceBi @Autowired private lateinit var objectMapper: ObjectMapper @RequestMapping(value = ["/endpoint"]) @ApiResponse(content = [Content(schema = Schema(hidden = true))]) fun invokeEndpoint(@RequestBody req: Request): Response { try { // 修正笔误:使用req而非request return serviceBi.endpoint(req)!! } catch (e: Exception) { throw e } } }
测试代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ServiceControllerTest { @LocalServerPort private val port = 8989 @Autowired private lateinit var restTemplate: TestRestTemplate @Test fun testEndpoint() { val response = restTemplate.exchange( "http://localhost:$port/api/endpoint", HttpMethod.POST, HttpEntity(getGzip(request().toByteArray()), makeHttpHeaders()), String::class.java ) } private fun makeHttpHeaders(): HttpHeaders { val httpHeaders = HttpHeaders() httpHeaders.contentType = MediaType.APPLICATION_JSON httpHeaders.add("Content-Encoding", "gzip") return httpHeaders } private fun getGzip(body: ByteArray?): ByteArray { ByteArrayOutputStream().use { byteStream -> GZIPOutputStream(byteStream).use { zipStream -> zipStream.write(body!!) zipStream.finish() return byteStream.toByteArray() } } } private fun request(): String = "long json" }
核心解决思路
- 重写内容长度方法:自定义的
HttpServletRequestWrapper必须重写getContentLength()和getContentLengthLong(),返回-1告知Spring框架内容长度未知,使其通过流式读取完整的解压后内容,而非依赖原始的压缩后Content-Length值。 - 修复流结束判断:
DecompressServletInputStream的isFinished()方法需正确跟踪流的读取状态,避免框架误以为流未结束或提前终止读取。 - 完善流校验逻辑:在
StreamHelper中补充处理读取Gzip签名不足2字节的边界情况,避免潜在的IO异常。
内容的提问来源于stack exchange,提问作者user12124207
相关产品推荐
相关产品推荐

