jetty / jetty/jetty.project

ee8 AsyncResponse.resume() from non-Jetty thread causes IllegalStateException in HttpChannelState.unhandle()

Open
#14,731 7 comments 0 reactions 0 assignees View on GitHub
More Info Required Unable To Replicate
Dominant language
Java
Stars
4.1k
Forks
2k
Avg merge
3d 56m
Merged PRs (30d)
48

Description

**Jetty version(s)**
Broken: 12.1.6, 12.1.7
Working: 12.1.1 through 12.1.5

**Jetty Environment**
ee8

**HTTP version**
HTTP/1.1

**Java version/vendor**
OpenJDK 17

**OS type/version**
Linux

**Description**

After upgrading from 12.1.5 to 12.1.6, Jersey JAX-RS async responses resumed from a non-Jetty thread throw `IllegalStateException` in `HttpChannelState.unhandle()`. The response never reaches the client, resulting in a gateway timeout (504).

The pattern: a `ScheduledThreadPoolExecutor` thread calls `AsyncResponse.resume()`. Jersey writes the response and calls `AsyncContext.complete()`, which triggers `HttpChannelState.complete()` → `runInContext()` → `HttpChannel.handle()` → `HttpChannelState.unhandle()`. The `unhandle()` finds `_state=IDLE` instead of `HANDLING`.

Stack trace:

```
org.glassfish.jersey.server.ServerRuntime$Responder writeResponse
SEVERE: Error while closing the output stream in order to commit response.
java.lang.IllegalStateException: s=IDLE rs=COMPLETED os=COMPLETED is=IDLE awp=false se=false i=false al=0
at org.eclipse.jetty.ee8.nested.HttpChannelState.unhandle(HttpChannelState.java:409)
at org.eclipse.jetty.ee8.nested.HttpChannel.handle(HttpChannel.java:567)
at org.eclipse.jetty.ee8.nested.HttpChannel.run(HttpChannel.java:438)
at org.eclipse.jetty.server.handler.ContextHandler$ScopedContext.run(ContextHandler.java:1697)
at org.eclipse.jetty.ee8.nested.ContextHandler.handle(ContextHandler.java:941)
at org.eclipse.jetty.ee8.nested.HttpChannelState.runInContext(HttpChannelState.java:1050)
at org.eclipse.jetty.ee8.nested.HttpChannelState.complete(HttpChannelState.java:655)
at org.eclipse.jetty.ee8.nested.AsyncContextState.complete(AsyncContextState.java:60)
at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.complete(AsyncContextDelegateProviderImpl.java:102)
at org.glassfish.jersey.servlet.internal.ResponseWriter.commit(ResponseWriter.java:174)
at org.glassfish.jersey.server.ContainerResponse.close(ContainerResponse.java:404)
at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:721)
at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:380)
at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:370)
at org.glassfish.jersey.server.ServerRuntime$AsyncResponder$3.run(ServerRuntime.java:871)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
at org.glassfish.jersey.server.ServerRuntime$AsyncResponder.resume(ServerRuntime.java:903)
at org.glassfish.jersey.server.ServerRuntime$AsyncResponder.resume(ServerRuntime.java:859)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
```

**Analysis**

Bisected to 12.1.6. Works on 12.1.5, broken on 12.1.6 and 12.1.7.

The regression was introduced by #14370 (fix for #14327), which changed how the core `HttpChannelState` handles failure/abort during response writing.

The core `HttpChannelState` changes affect the ee8 async complete path: when `AsyncResponse.resume()` is called from a non-Jetty thread, the core layer now completes/aborts the response in a way that leaves the ee8 `HttpChannelState` in `s=IDLE rs=COMPLETED os=COMPLETED` before `unhandle()` is called, causing the `IllegalStateException`.

The ee8-nested sources (`HttpChannelState.java`, `HttpChannel.java`, `HttpOutput.java`) are unchanged between 12.1.5 and 12.1.6 — only logging format changes.

**How to reproduce?**

1. Use Jetty ee8 with Jersey (`@Suspended AsyncResponse`)
2. Suspend the request, process work on a `ScheduledThreadPoolExecutor`
3. Call `asyncResponse.resume(response)` from the executor thread
4. The response commit fails with `IllegalStateException`, client never receives the response

Works on 12.1.5, fails on 12.1.6+.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the core HttpChannelState changes from #14370 and comparing the ee8-nested HttpChannelState.java, HttpChannel.java, and HttpOutput.java between 12.1.5 and 12.1.6. Reproduce the Jersey ee8 case with AsyncResponse.resume() on a ScheduledThreadPoolExecutor thread. Done means the response reaches the client without the IllegalStateException or a 504 on 12.1.6 and later.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.