[local] Serve a file from one open handle so Content-Length matches the body - #35
Merged
Merged
Conversation
Hilbrand
approved these changes
Sep 21, 2026
Hilbrand
left a comment
Member
There was a problem hiding this comment.
If tested in actual use, and it still worked I approve.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This makes local file server reads report the precise content length of the file it is serving. Spring Boot serves the FileUrlResource non-atomically: first it sets the content length, then some microseconds later it writes the file. If the file is replaced during this window, the content length and the file do not match, and we get a 500 error. This happens very rarely, but it is a consistent flake. Only occurs for the local file server.
See gist for full history, cause, and reproduction: https://gist.github.com/JornC/fca15a1644f766b5942c065b2fb266af
See also item AER-4676 for a proper architectural approach to avoid this type of thing being able to happen altogether (i.e. not overwriting files).
LLM-generated technical summary
LocalFileController.getFilereturned a lazyFileUrlResource. Spring'sResourceHttpMessageConvertercallscontentLength()inaddDefaultHeadersto setContent-Length, and only afterwards opens the file inwriteContent. When the file is atomically renamed over between those two calls, Tomcat'sIdentityOutputFilterwrites the new file's bytes cut to the old length and drops the rest, so the caller receives200 OKwith a truncated body. Register hits this on every upload: the API writes a 27-byte pendingvalidation.json, the frontend polls it at once, and the validation thread overwrites it with the roughly 400-byte real result. The register API then fails to parse the 27-byte prefix and answers a bare 500 that nothing logs. The atomic-write fix in #30 removed the window in which the file was missing (the 404s) but not this one.The controller now opens the file once with
Files.newByteChannel, takes the length from that channel, streams from the same channel through anInputStreamResource, and setsContent-Lengthexplicitly. A handle opened before the rename keeps the old file, so the header and the body always describe the same version. Spring does not compute a length for anInputStreamResourceand leaves an explicit one alone, and it closes the stream after copying. A missing file still throws anIOExceptionand still becomes a 404.LocalFileControllerTest.testGetFileWhileOverwrittenreplaces the file with alternating 27-byte and 4 KB versions from a writer thread while performing 2000 GETs, and asserts thatContent-Lengthequals the served body and that the body is one complete version. Against the old controller it fails on the first iteration. In a local end-to-end run (one writer, eight readers, 20 seconds) the current main served 37 truncated bodies out of 12,400 reads; with this change, 0 out of 151,400.