Affects: springframework at 6.0.13 is okay but update to 6.1.1 then error
springframework 6.0.13(boot 3.1.5) is okay but update to 6.1.1(boot 3.2.0) then error
I have been performing file uploads to s3 using the content of FilePart. It was working well, but after updating the Spring version, I started getting NoSuchFileException. It occurs correctly about 1 in 20 attempts. Therefore, the majority of the requests fail to upload the file.
The error message is as follows:
java.nio.file.NoSuchFileException: /var/folders/d_/d03cqx_x01n1jy_pgqdyklsw0000gn/T/spring-multipart-7499578255272102614/11133527665414509817.multipart
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:218)
at java.base/java.nio.file.Files.newByteChannel(Files.java:380)
at java.base/java.nio.file.Files.newByteChannel(Files.java:432)
at org.springframework.http.codec.multipart.DefaultParts$FileContent.lambda$content$0(DefaultParts.java:295)
at reactor.core.publisher.FluxUsing.subscribe(FluxUsing.java:75)
and this is my code(kotlin)
private class UploadStates(var bucket: String, var fileKey: String) {
var uploadId: String? = null
var partCounter = 0
var completedParts: MutableMap<Int, CompletedPart> = HashMap()
var buffered = 0
}
fun saveFile(fileKey: String, s3AsyncClient: S3AsyncClient, part: FilePart): Mono<String> {
val uploadState = UploadStates("aa", fileKey)
return Mono
.fromFuture(
s3AsyncClient
.createMultipartUpload(
CreateMultipartUploadRequest.builder()
.contentType((part.headers().contentType ?: MediaType.APPLICATION_OCTET_STREAM).toString())
.key(fileKey)
.bucket("aa")
.build()
)
)
.flatMapMany { response: CreateMultipartUploadResponse ->
checkResult(response)
uploadState.uploadId = response.uploadId()
part.content()
}
.bufferUntil { buffer: DataBuffer ->
uploadState.buffered += buffer.readableByteCount()
return@bufferUntil if (uploadState.buffered >= S3_BUFFER_SIZE) {
uploadState.buffered = 0
true
} else {
false
}
}
.map { buffers: List<DataBuffer> ->
concatBuffers(buffers)
}
.flatMap { buffer: ByteBuffer ->
uploadPart(s3AsyncClient, uploadState, buffer)
}
.reduce(
uploadState
) { state: UploadStates, completedPart: CompletedPart ->
state.completedParts[completedPart.partNumber()] = completedPart
state
}
.flatMap { state: UploadStates ->
completeUpload(s3AsyncClient, state)
}
.map { response: SdkResponse ->
checkResults(response)
uploadState.fileKey
}
.onErrorMap { e ->
throw RuntimeException(e.localizedMessage)
}
}
private fun uploadPart(s3AsyncClient: S3AsyncClient, uploadState: UploadStates, buffer: ByteBuffer): Mono<CompletedPart> {
val partNumber: Int = ++uploadState.partCounter
val request: CompletableFuture<UploadPartResponse> = s3AsyncClient.uploadPart(
UploadPartRequest.builder()
.bucket(uploadState.bucket)
.key(uploadState.fileKey)
.partNumber(partNumber)
.uploadId(uploadState.uploadId)
.contentLength(buffer.capacity().toLong())
.build(),
AsyncRequestBody.fromPublisher(Mono.just(buffer))
)
return Mono
.fromFuture(request)
.map { uploadPartResult ->
checkResults(uploadPartResult)
CompletedPart.builder()
.eTag(uploadPartResult.eTag())
.partNumber(partNumber)
.build()
}
}
private fun checkResults(result: SdkResponse) {
if (result.sdkHttpResponse() == null || !result.sdkHttpResponse().isSuccessful) {
throw UploadFailedException(response = result)
}
}
private fun completeUpload(s3AsyncClient: S3AsyncClient, state: UploadStates): Mono<CompleteMultipartUploadResponse> {
val multipartUpload: CompletedMultipartUpload = CompletedMultipartUpload.builder()
.parts(state.completedParts.values)
.build()
return Mono.fromFuture(
s3AsyncClient.completeMultipartUpload(
CompleteMultipartUploadRequest.builder()
.bucket(state.bucket)
.uploadId(state.uploadId)
.multipartUpload(multipartUpload)
.key(state.fileKey)
.build()
)
)
}
Firstly, I downgraded the version, so the problem is currently resolved, but I'm reaching out because I'm unable to upgrade the version.
Comment From: poutsma
It's hard to determine what's going on without having a complete picture. If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.
Comment From: fclemonschool
okay, this is sample but need aws error.zip
and postman json New Collection.postman_collection.json @poutsma
Comment From: poutsma
Due to https://github.com/spring-projects/spring-framework/issues/31567, we now clean up temp files after the HTTP exchange has been completed. What you are seeing is the result of that, meaning that you are attempting to use a FilePart
when the temp file backing it has already been deleted.
Looking at the sample, it is not surprising that the files are missing. In FileUtil.kt
, line 24, the Mono<String>
result from the saveFile
function is subscribed to. In a reactive environment, such as a WebFlux application, you never want to subscribe to a stream yourself, you typically propagate the stream to the higher layer. In the sample, this would mean returning Mono<String>
in FilePart.upload
and FileService
, and Mono<ResponseEntity<String>>
in TestController
.
There could be other issues in the sample, I only took a brief glance. At any rate, this is expected behavior given the sample code.