Akka-Http文件上传:请求未完全读取就返回响应的问题排查
Hey Dave, let's unpack why you're seeing those "request not fully read" logs and how to ensure your uploaded files are completely written to disk before sending a response.
What You're Missing
The core misunderstanding here is how Akka HTTP ties request lifecycle to your stream processing:
- You assumed
uploadedFblocks until the sink finishes, but routing logic might be cutting it short: If you return a response (likecomplete(StatusCodes.OK)) without waiting for theIOResultfrom yourFileIOsink, Akka HTTP will immediately mark the request as handled and close the connection. YouruploadedFfuture might still be running in the background, but the request context is already gone—hence the "not fully read" logs. - Stream lifecycle is decoupled from routing by default: Akka Streams processes data asynchronously. If you don't explicitly link your sink's completion to the route's response, Akka HTTP won't wait for the file to finish writing. A common mistake looks like this:
Here, the response goes out right away, while the stream is still trying to read the request body and write to disk.path("upload") { post { fileUpload("file") { case (metadata, byteSource) => val uploadedF = byteSource.runWith(FileIO.toPath(Paths.get("/tmp/" + metadata.fileName))) complete(StatusCodes.OK) // Oops—returns immediately, doesn't wait for uploadedF } } }
How to Verify Full Request Read & Complete File Write
You need to bind your route's response to the completion of the IOResult future. Here are two solid approaches:
1. Wait for the Sink's IOResult Before Responding
Use Akka HTTP's onSuccess directive to wait for the sink to finish processing, then return a response based on the result. This ensures Akka HTTP keeps the request open until the file is fully written and the request body is fully read:
path("upload") { post { fileUpload("file") { case (metadata, byteSource) => val targetPath = Paths.get("/tmp/" + metadata.fileName) val uploadedF = byteSource.runWith(FileIO.toPath(targetPath)) onSuccess(uploadedF) { ioresult => if (ioresult.wasSuccessful) { complete(s"Success! Wrote ${ioresult.count} bytes to ${metadata.fileName}") } else { complete(StatusCodes.InternalServerError -> "File upload failed mid-write") } } } } }
Now, the route only completes when the sink's IOResult is available. Akka HTTP will hold the request context until then, so the entire request body is read and the file is safely written.
2. Add Integrity Checks (Bonus)
To be extra sure the file wasn't truncated, validate the written byte count against the expected request size (if provided via the Content-Length header):
path("upload") { post { extractRequest { req => fileUpload("file") { case (metadata, byteSource) => val expectedBytes = req.headers.collectFirst { case h: `Content-Length` => h.length } val targetPath = Paths.get("/tmp/" + metadata.fileName) val uploadedF = byteSource.runWith(FileIO.toPath(targetPath)) onSuccess(uploadedF) { ioresult => val isComplete = expectedBytes.forall(_ == ioresult.count) if (ioresult.wasSuccessful && isComplete) { complete(StatusCodes.OK -> "File uploaded fully and verified") } else { val errorMsg = if (!ioresult.wasSuccessful) "Write failed" else "File truncated" complete(StatusCodes.BadRequest -> s"Upload failed: $errorMsg") } } } } } }
The Key Idea
Akka HTTP routes are driven by futures—only when the RouteResult future (returned by your route logic) completes does the request get marked as done. By tying your complete call to the sink's IOResult future, you're telling Akka HTTP: "Don't finish this request until the file is fully written and the entire request body is read."
内容的提问来源于stack exchange,提问作者Dave Lugg

