Skip to content

Keep the buffered response writer usable across responseReset() - #1033

Merged
tandraschko merged 1 commit into
apache:mainfrom
BalusC:fix-response-reset-with-buffered-writer
Jul 29, 2026
Merged

Keep the buffered response writer usable across responseReset()#1033
tandraschko merged 1 commit into
apache:mainfrom
BalusC:fix-response-reset-with-buffered-writer

Conversation

@BalusC

@BalusC BalusC commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

ServletExternalContextImpl.getResponseOutputWriter() caches a BufferedWriter since 0e84278. responseReset() and responseSendError() drop that field, but a ResponseWriter created earlier in the render phase keeps writing into the instance it obtained back then. Everything rendered after the reset — typically an error page — therefore lands in a buffer nobody drains, and the client gets HTTP 200 with an empty body.

This is not limited to third party exception handlers: AjaxExceptionHandlerImpl (line 205) and ErrorPageWriter (line 427) follow the same reset-then-reuse-the-cached-writer pattern.

There is a second break from the same commit: release() flushes the BufferedWriter, which flushes the container writer and thereby commits the response. A render exception on a non-ajax request can then no longer be reset and forwarded to the error page.

Fix

ResettableBufferedWriter replaces the plain BufferedWriter:

  • it keeps its identity across responseReset() / responseSendError(), which now discard the buffered chars instead of dropping the writer;
  • it resolves the container writer per drain instead of holding on to it, so it survives HttpServletResponse.reset();
  • flush() drains into the container writer but does not flush it — committing stays the container's decision, or that of an explicit responseFlushBuffer().

The buffering and its performance benefit are unchanged: same 8 KB buffer, same coalescing of the many small HtmlResponseWriterImpl writes.

Verified

  • New ServletExternalContextImplTest#testResponseResetDiscardsBufferedOutputButKeepsWriterUsable. On current main it fails with expected: <error page> but was: <aborted responseerror page>, which also shows aborted output being re-committed.
  • Full mvn clean verify on the reactor is green.
  • Real world reproduction: OmniFaces FullAjaxExceptionHandlerIT on Tomcat 11 + MyFaces 5.0.0-SNAPSHOT goes from 7 errors to 14/14 passing.

🤖 Generated with Claude Code (Opus 5)

@tandraschko

Copy link
Copy Markdown
Member

@BalusC checkstyle failed
thanks for the PR

if (length >= BUFFER_SIZE)
{
drain();
response.getWriter().write(string, offset, length);
if (length >= BUFFER_SIZE)
{
drain();
response.getWriter().write(string, offset, length);
if (length >= BUFFER_SIZE)
{
drain();
response.getWriter().write(string, offset, length);
ServletExternalContextImpl.getResponseOutputWriter() caches a BufferedWriter. responseReset() and responseSendError() dropped that field, but a ResponseWriter created earlier in the render phase keeps writing into the instance it obtained back then, so everything rendered after the reset — typically an error page — landed in a buffer nobody drains and the client got HTTP 200 with an empty body. AjaxExceptionHandlerImpl and ErrorPageWriter follow the same reset-then-reuse pattern and were affected as well.

release() additionally flushed the BufferedWriter, which flushed the container writer and thereby committed the response, so a render exception on a non-ajax request could no longer be reset and forwarded to the error page.

Buffer through a ResettableBufferedWriter instead: it keeps its identity across a reset, discards the buffered chars there, resolves the container writer per drain, and drains without flushing the container writer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BalusC
BalusC force-pushed the fix-response-reset-with-buffered-writer branch from 55c9a57 to 36c0b21 Compare July 29, 2026 14:38
@BalusC

BalusC commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

checkstyle failed

Fixed now.

@melloware melloware added the bug Something isn't working label Jul 29, 2026
@tandraschko
tandraschko merged commit f964a26 into apache:main Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants