quarkusio/quarkus · error · IllegalStateException

QuarkusMultipartResponseDecoder was destroyed already

Error message

QuarkusMultipartResponseDecoder was destroyed already

What it means

checkDestroyed() throws IllegalStateException when any public operation (offer, hasNext, next, getBodyHttpDatas, getBodyHttpData, isMultipart, etc.) is called after destroy() has run. The decoder is a stateful, closeable object and cannot be reused after being torn down.

Source

Thrown at independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartResponseDecoder.java:252

        currentStatus = MultiPartStatus.HEADERDELIMITER;

        try {
            if (this.response instanceof HttpContent) {
                // Offer automatically if the given request is als type of HttpContent
                // See #1089
                offer((HttpContent) this.response);
            } else {
                parseBody();
            }
        } catch (Throwable e) {
            destroy();
            PlatformDependent.throwException(e);
        }
    }

    private void checkDestroyed() {
        if (destroyed) {
            throw new IllegalStateException(QuarkusMultipartResponseDecoder.class.getSimpleName()
                    + " was destroyed already");
        }
    }

    /**
     * True if this request is a Multipart request
     *
     * @return True if this request is a Multipart request
     */
    public boolean isMultipart() {
        checkDestroyed();
        return true;
    }

    /**
     * Set the amount of bytes after which read bytes in the buffer should be discarded.
     * Setting this lower gives lower memory usage but with the overhead of more memory copies.
     * Use {@code 0} to disable it.

View on GitHub (pinned to e1c734241f)

Solutions

  1. Create a new QuarkusMultipartResponseDecoder per response; never reuse or cache instances
  2. Guard usage with isDestroyed() (or your own flag) before further calls
  3. Ensure lifecycle code does not call destroy() while more chunks may still arrive
  4. Restructure async handlers so the decoder reference is dropped once the response completes

Example fix

// before
this.decoder.offer(chunk); // decoder may already be destroyed
// after
if (!decoder.isDestroyed()) {
    decoder.offer(chunk);
} else {
    decoder = new QuarkusMultipartResponseDecoder(response, factory, charset);
    decoder.offer(chunk);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (decoder.isDestroyed()) { decoder = createNewDecoder(); }

Type guard

boolean usable(QuarkusMultipartResponseDecoder d) { return d != null && !d.isDestroyed(); }

Try / catch

try { decoder.offer(chunk); } catch (IllegalStateException e) { if (e.getMessage().contains("destroyed")) { decoder = createNewDecoder(); decoder.offer(chunk); } else throw e; }

Prevention

When it happens

Trigger: Calling offer()/hasNext()/next()/getBodyHttpData(s) on a decoder instance after destroy() was called — typically by continuing to use a decoder stored in a field after response completion, or on error paths that call destroy().

Common situations: Reusing a cached decoder across requests; async callbacks firing after the response was cleaned up; decoding an errored response that triggered destroy() internally (e.g. missing boundary) and then probing its state.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/4e323c013878a0b4. Report an issue: GitHub.