quarkusio/quarkus · error · IllegalStateException
Already closed
Error message
Already closed
What it means
SseEventSinkImpl.send() throws IllegalStateException when the sink is already closed — either explicitly via close() or because the underlying server response connection was closed by the client. No further events can be written to that sink.
Source
Thrown at independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/SseEventSinkImpl.java:34
public static final byte[] EMPTY_BUFFER = new byte[0];
private ResteasyReactiveRequestContext context;
private SseBroadcasterImpl broadcaster;
private AtomicBoolean closed = new AtomicBoolean();
public SseEventSinkImpl(ResteasyReactiveRequestContext context) {
this.context = context;
}
@Override
public boolean isClosed() {
return context.serverResponse().closed() || closed.get();
}
@Override
public CompletionStage<?> send(OutboundSseEvent event) {
if (isClosed())
throw new IllegalStateException("Already closed");
// NOTE: we can't cast event to OutboundSseEventImpl because the TCK sends us its own subclass
return SseUtil.send(context, event, Collections.emptyList());
}
@Override
public void close() {
if (!closed.compareAndSet(false, true))
return;
ServerHttpResponse response = context.serverResponse();
if (!response.closed()) {
if (!response.headWritten()) {
// make sure we send the headers if we're closing this sink before the
// endpoint method is over
SseUtil.setHeaders(context, response);
}
response.end();
context.close();View on GitHub (pinned to e1c734241f)
Solutions
- Check sink.isClosed() before each send, and prune closed sinks from your registry.
- Catch IllegalStateException around send() and remove the sink from active tracking.
- Register an onClose handler on the broadcaster to drop dead sinks promptly.
- Use SseBroadcaster (which handles closed sinks internally) instead of manual per-sink sends.
Example fix
// before
sink.send(event); // IllegalStateException if client gone
// after
if (!sink.isClosed()) {
sink.send(event);
} else {
sinks.remove(sink);
} Defensive patterns
Strategy: type-guard
Validate before calling
if (sink.isClosed()) {
sinks.remove(sink);
return;
} Type guard
boolean canSend(SseEventSink s) {
return s instanceof SseEventSinkImpl impl && !impl.isClosed();
} Try / catch
try {
sink.send(event);
} catch (IllegalStateException e) {
sinks.remove(sink); // client disconnected
} Prevention
- Check isClosed() before every send
- Register onClose handlers to prune dead sinks
- Prefer SseBroadcaster over manual sends for multi-client fan-out
- Bound scheduled tasks to the connection lifetime
When it happens
Trigger: Calling sink.send(event) after sink.close(), after the client disconnected (context.serverResponse().closed()), or in a scheduled/async task that outlives the connection.
Common situations: Periodic jobs pushing to a stale sink; broadcasting to a list of sinks where some clients left; client browser tab closed while server keeps streaming; long GC pause causing connection termination.
Related errors
- Broadcaster has been closed
- Already registered on a broadcaster
- Can only work with Quarkus-REST instances: ${sseEventSink}
- Request already suspended
- retrieving all roles not supported when JAX-RS security cont
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/77f7ae4ef1c0b3cb.
Report an issue: GitHub.