Skip to content

Guard inner flush in ServerCall.writeResponseBody for Jetty 12 compatibility#1508

Open
nico-mayora wants to merge 1 commit into
restlet:2.6from
nico-mayora:2.6
Open

Guard inner flush in ServerCall.writeResponseBody for Jetty 12 compatibility#1508
nico-mayora wants to merge 1 commit into
restlet:2.6from
nico-mayora:2.6

Conversation

@nico-mayora
Copy link
Copy Markdown

@nico-mayora nico-mayora commented May 19, 2026

The aim

Stop spurious SEVERE log entries and bogus 500 retries on every Restlet response served via the Jetty 12 connector. The wire response is already correct, only logging and the retry path misbehave.

The solution

Guard the inner responseEntityStream.flush() in ServerCall.writeResponseBody with a try/catch that logs at FINE, mirroring the pattern already used by sendResponse()'s finally block two methods up.

org.eclipse.jetty.io.content.BufferedContentSink in Jetty 12 strictly enforces the Content.Sink contract: once a write with last=true has occurred, any subsequent write or flush completes the callback with IOException("complete"). Under Jetty 9 / 11 the same flush was silently tolerated.

That IOException propagates from writeResponseBody up to ServerAdapter.commit, where it's caught and logged at SEVERE as "An exception occurred writing the response entity" and triggers a retry against a response whose body bytes are already on the wire.

The outer flush+close in sendResponse()'s finally block already wraps the same operation:

  } catch (IOException ioe) {
      getLogger().log(Level.FINE,
              "The stream was probably already closed by the connector. "
                      + "Probably OK, low message priority.", ioe);
  }

This patch mirrors that exact treatment on the inner flush:

protected void writeResponseBody(Representation entity, OutputStream responseEntityStream) throws IOException {
    // Send the entity to the client
    if (responseEntityStream != null) {
        entity.write(responseEntityStream);
        try {
            responseEntityStream.flush();
        } catch (IOException ioe) {
            getLogger().log(Level.FINE, "Unable to flush the entity stream.", ioe);
        }
    }
}

Reproduction

  • Jetty 12.1.9 + org.restlet.ext.jetty 2.6.0.
  • Mount any ServerResource returning a simple representation, e.g.
public class Hello extends ServerResource {
    @Get
    public Representation hello() {
        return new StringRepresentation("hi");
    }
}
  • curl it. Without this patch, the response body is hi (correct) but the server logs:
SEVERE: An exception occurred writing the response entity
java.io.IOException: complete
    at org.eclipse.jetty.io.content.BufferedContentSink._lastWritten(...)
    at org.eclipse.jetty.io.content.BufferedContentSink.flush(...)
    at org.restlet.engine.adapter.ServerCall.writeResponseBody(ServerCall.java:...)
    ...
  • With the patch: response unchanged, no SEVERE entry, no retry.

Check-list

  • PR size
    • Under 300 lines ✅
    • Can't be split without complicating the process even more
  • Tests
    • Added
    • Not applicable: exercising this path requires a live Jetty 12 server in the Restlet test harness; the analogous existing guard in sendResponse() is also untested, and this change is mechanically the same one method down
  • Doc
    • Added
    • Not applicable
  • Reviewer
    • Asked for a review
    • Added label DO NOT REVIEW

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants