Repository navigation
feat: keep a UI alive for its ongoing transfers and terminate them when the session is invalidated - #25431
Conversation
Adds failing tests for the behavior described in #25409: - a UI that is closed while a download for it is being served stays attached to the session until the download has completed, and is detached once it has; - the completion callback of UploadHandler.toTempFile is invoked even when the browser tab is closed in the middle of the upload; - invalidating the session terminates an ongoing download instead of letting it run to the end. All three currently fail: the closed UI is detached at the end of the request that noticed the close, which makes the upload callback fail with a UIDetachedException, and an ongoing transfer is unaffected by session invalidation.
An ElementRequestHandler implemented directly, without going through TransferUtil, can sit blocked waiting for its data source. Nothing terminates it today when the session is invalidated, so the test fails by running to completion.
A handler that writes to the VaadinResponse output stream instead of going through TransferUtil keeps writing after the session has been invalidated.
…ation Ongoing upload and download requests are now tracked per UI, which addresses two unmet expectations: A UI that is closed, e.g. by closing the browser tab, while a transfer for it is being served is kept attached to its session until that request has been served. Transfer progress listeners and the completion callbacks of handlers such as UploadHandler.toTempFile are bound to that UI, so they were previously ineffective or failed with a UIDetachedException. The UI is detached by the regular end-of-request cleanup of the request that serves the last transfer. Invalidating the session terminates the transfers that are ongoing for its UIs and logs a warning with the path, the owner component and the number of bytes transferred so far for each of them. Termination is enforced by wrapping the request and the response passed to the handler, so that every stream the framework hands out - the response output stream and writer, the request input stream and reader, and the streams of multipart parts - refuses to transfer more bytes and throws an IOException instead. This covers the built-in handlers as well as a handler that implements ElementRequestHandler directly, since all data that reaches the client passes through those streams. A handler that is blocked waiting for its own data source is not interrupted, but it can no longer deliver anything once it resumes. A transfer progress notification for a UI that has been detached is now skipped instead of throwing a UIDetachedException, since that only happens when the session has been invalidated and the transfer is thus being terminated anyway. That keeps the failure the application sees the one that actually terminated the transfer. Fixes #25409
The Sonar quality gate flagged the new code for low coverage and for duplication. The two input stream wrappers had the same reading logic, so the servlet one now reads through the plain one instead of repeating it. The defensive branches for an unparseable UI id and for a missing transfer owner are gone as well: the UI id always comes from the resource URI that the request was matched against, and the owner element is required by the transfer. ActiveTransferTest covers what the wrapped request and response hand out, and the servlet stream helpers that it shares with ActiveTransferLifecycleTest now live in a test utility.
The counter only ever appeared in the warning that is logged when a transfer is terminated, and it lumped together the bytes read from the request and the bytes written to the response. The streams now only check whether the transfer has been terminated, and the warning identifies the transfer by its path and owner.
Addresses review feedback: - the terminating streams extend FilterInputStream, FilterWriter and FilterReader instead of repeating the plain delegations; - a transfer describes its owner through the same StateNode logic that other framework messages use, which also brings in the create and attach locations in development mode; - terminating the transfers of a UI is a UIInternals concern rather than a VaadinService one, and registering one uses Registration.addAndRemove; - the field holding the transfers documents why it is not protected by the session lock; - the resource path is parsed once per request; - a request or response that cannot be wrapped is now reported with a warning, once per JVM, instead of silently serving transfers that cannot be terminated; - UploadHandler documents the lifecycle of an ongoing upload the same way DownloadHandler documents a download.
Basing the terminating streams on FilterInputStream and FilterReader made skip bypass the termination check, since those classes pass it straight to the wrapped stream instead of reading through the methods that are checked. A terminated upload could thus still consume the request body through the stream of a multipart part. Terminating the transfers of a UI now also terminates all of them before describing any of them for the log, and describing a transfer tolerates a failure in rendering the owner component, so that application code cannot leave the remaining transfers running.
Addresses review feedback: - a transfer is described through the existing public StateNode.describe(), which keeps the description logic in one place and never runs application code, so neither the method nor its caller needs to guard against a failing toString(); - the streams no longer check whether the transfer has been terminated in methods that expose no data of their own, which are skipping and flushing; - an unwrappable request or response is reported once per type rather than once per JVM, and the message tells what will not happen if the session is invalidated rather than implying that it already has.
The tests that cover what happens to a UI and its session while a transfer is being served now live in the test class of the request handler that serves those requests, instead of in a test class of their own. They keep their own request, response and UI, since they need the servlet types and a UI that is added to the session, whereas the existing tests there use mocks and an uninitialized UI. Reaching the end-of-request session cleanup from another package is done through a mock service method, and the Mockito calls of the tests added by this branch use static imports.
The UI, request and response that one of these tests serves a transfer with are collected into a record that the setup helper returns, instead of living in fields that the rest of the class has no use for. The upload test keeps its content in memory rather than writing a temporary file, and the test for a handler that writes to the response directly now writes a single chunk after the invalidation, since the first write after it already has to fail.
|
@claude does this make sense? |
|
Claude finished @Artur-'s task in 1m 49s —— View job Does this make sense? — yes, the approach is soundI traced the full lifecycle ( Keeping a closed UI alive
Terminating on invalidation
Progress notifications
A few things worth a second look (none blocking; Legioth already approved)
Bottom line: the model — track per-UI, defer detach while a transfer is in flight, terminate the handed-out streams on invalidation, wrap servlet types so casts keep working and pass non-servlet through — is coherent and the locking is correct. It makes sense. 👍 |
|
Explain that a closed UI stays attached until its ongoing upload or download finishes, and that invalidating the session instead terminates that transfer, on the application lifecycle, uploads, and downloads pages. Documents vaadin/flow#25431 (`9d8ad814fa8c97d9ef20415e360695c875b3d28d`).
|
Pull request created: #6082
|
|
Documentation Bot: Draft documentation pull request for this change: vaadin/docsvaadin/docs#6082 Files updated:
It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.
|
) Documentation for vaadin/flow#25431 by `@totally-not-ai`[bot]. > [!NOTE] > The source pull request is merged, so this documentation describes the final > shape of the change. Please review it and mark it ready for review. **Change categories:** BEHAVIOR_CHANGE | File | Change | |------|--------| | `articles/flow/advanced/application-lifecycle.adoc` | Note that a UI serving an ongoing upload or download stays attached to its session until the transfer is handled, and that invalidating the session instead terminates any ongoing transfer. | | `articles/flow/advanced/upload-resources.adoc` | Explain that an ongoing upload keeps its UI attached across a close/tab-close so its callback still runs, but is terminated and discarded if the session is invalidated. | | `articles/flow/advanced/downloads.adoc` | Explain the same lifecycle for download progress listeners: they keep firing for a download whose UI has closed, but are notified through `onError` instead of `onComplete` if the session ends first. | Auto-generated by the Documentation Bot — review before merging. > Generated by [Documentation Bot](https://github.com/vaadin/flow/actions/runs/35347816994) for #25431 · agent · 103.8 AIC · ⌖ 6.17 AIC · ⊞ 11.2K · [◷](https://github.com/search?q=repo%3Avaadin%2Fdocs+%22gh-aw-workflow-id%3A+doc-bot%22&type=pullrequests) > - [x] expires <!-- gh-aw-expires: 2026-10-18T13:09:36.882Z --> on Oct 18, 2026, 1:09 PM UTC <!-- gh-aw-agentic-workflow: Documentation Bot, engine: claude, model: agent, id: 35347816994, workflow_id: doc-bot, run: https://github.com/vaadin/flow/actions/runs/35347816994 --> <!-- gh-aw-expires-type: pull-request --> <!-- gh-aw-workflow-id: doc-bot --> <!-- gh-aw-workflow-call-id: vaadin/flow/doc-bot -->
|
This ticket/PR has been released with Vaadin 25.4.0-alpha1. |



Summary
Uploads and downloads are now tracked per UI. A UI that is closed while a transfer for it is running stays attached to its session until that request is done, so progress listeners and completion callbacks still run. When the session is invalidated or times out, its ongoing transfers are stopped instead of running to the end.
Fixes #25409
What changed
Behavior change 1 — a closed UI is detached later than before. Affects every application that serves uploads or downloads. If the user closes the browser tab (or the UI is closed for any other reason) while a transfer for that UI is being served, the UI is no longer removed from the session by the end-of-request cleanup. It is kept until the request that serves the last transfer finishes, and that request's own cleanup detaches it. Before this, the callbacks bound to that UI (
TransferProgressListener,UploadHandler.toTempFileand friends) either did nothing or failed with aUIDetachedException.Behavior change 2 — an invalidated session now kills its transfers. Affects applications whose uploads or downloads can outlive the session, for example a long download that is still running when the session times out.
VaadinServicenow callsUIInternals.terminateActiveTransfers()for each UI of the session being destroyed. Every stream the framework hands to the request handler — the response output stream and writer, the request input stream and reader, and the streams of multipart parts — then refuses to transfer more bytes and throws anIOException. A download ends up truncated; a partly received upload is discarded and its callback is not run. A warning with the request path and the owner component is logged for each terminated transfer. An application that needs a transfer to survive can keep the session alive, or serve it from its own servlet.Behavior change 3 — a progress notification for a detached UI no longer fails the transfer.
TransferProgressAwareHandlernow catchesUIDetachedExceptionfromUI.accessand logs it at debug level. This only happens when the session is gone, and the transfer is being terminated anyway, so the application sees the real cause instead of a detached-UI error.How it works:
StreamRequestHandlercreates anActiveTransferper request, registers it on the UI while the session lock is held, and passes the handler a wrapped request and response. The wrappers are new subclasses ofVaadinServletRequest/VaadinServletResponse, so existing casts to the servlet types keep working. A request or response that is not servlet based is passed through unwrapped, with a one-time warning per type; for those, only transfers going throughTransferUtilstop.Javadoc on
UI.close(),UploadHandler,DownloadHandler,ElementRequestHandler.handleRequestandTransferProgressListenerwas updated to describe this lifecycle. No public API was removed or renamed.Use case
An app lets users upload a large CSV of invoices. The import runs in the completion callback, and the app shows the result in the UI. Users often close the tab once the progress bar looks full — before this change, the upload reached the server but the callback was skipped or threw, so the import silently never happened. Now the upload finishes and the import runs; only losing the whole session (logout or timeout) cancels it.
API Changes
com.vaadin.flow.internal.streams.ActiveTransfer
com.vaadin.flow.component.internal.UIInternals
com.vaadin.flow.internal.streams.TerminableStreamsUtilis new but package-private, so it adds no API surface.Test summary
TransferUtilthrowsIOExceptionand leaves the response truncatedElementRequestHandlerthat writes straight to the response stops the next write; only the bytes written before it arriveterminate(), the response output stream and writer, and the request input stream, reader and multipart part stream all refuse to transfer morewrite/delete, servlet streamisReady/isFinished/available/listeners/closeStreamRequestHandlerTest.ongoingDownload_uiClosed_uiStaysAttachedUntilDownloadHasCompleted→ 1StreamRequestHandlerTest.ongoingUpload_uiClosed_uploadCompletionCallbackIsInvoked→ 2StreamRequestHandlerTest.ongoingDownload_sessionInvalidated_downloadIsTerminated→ 3StreamRequestHandlerTest.ongoingDownload_customHandlerWritingToResponse_sessionInvalidatedTerminatesIt→ 4ActiveTransferTest.outputStream_terminated_refusesToWriteMore,.writer_terminated_refusesToWriteMore,.inputStream_terminated_refusesToReadMore,.reader_terminated_refusesToReadMore,.parts_terminated_partStreamRefusesToReadMore→ 5ActiveTransferTest.part_everythingButTheContent_delegated,.servletStreams_everythingButTheContent_delegated→ 6ActiveTransferTest.nonServletRequestAndResponse_leftUnwrapped→ 7ActiveTransferTest.description_containsPathAndOwner→ no essential row; it only pins the wording of the log messageDeliberately untested: the one-per-type warning for an unwrappable request or response (logging only), and the new Javadoc. The tests that were in
ActiveTransferLifecycleTestmoved intoStreamRequestHandlerTest; that file is deleted, not lost.