Jetty 12 API changes: How to replace HttpInput.Interceptor and HttpOutput.Interceptor

Summary

This postmortem analyzes the Jetty 12 API removal of HttpInput.Interceptor and HttpOutput.Interceptor, why these hooks disappeared, what breaks during migration, and how senior engineers replace this functionality using Jetty 12’s new Handler/Filter pipeline and Content.Source / Content.Sink abstractions.

Root Cause

The failure occurs because Jetty 12 removed the legacy Servlet‑container–centric interception APIs (HttpInput.Interceptor, HttpOutput.Interceptor) as part of its architectural shift toward:

  • Jakarta EE 10 namespace changes
  • A unified, non-blocking content pipeline
  • A redesigned I/O abstraction (Content.Source, Content.Sink)
  • A stricter separation between request processing and transport layers

Your old code depended on APIs that no longer exist, so the migration breaks at compile time.

Why This Happens in Real Systems

Large production systems hit this issue because:

  • Jetty 12 is a major architectural rewrite, not a drop‑in upgrade.
  • Interceptors were historically used for:
    • Request/response body filtering
    • Security scanning
    • Logging and auditing
    • Payload transformation
  • These responsibilities now belong to Handlers, Filters, or custom Content wrappers.

Real-World Impact

Teams migrating from Jetty 9–11 typically experience:

  • Broken request/response body manipulation
  • Inability to inject logic before/after body reads
  • Unexpected behavior when relying on old I/O semantics
  • Refactoring cost because the new API requires a different mental model

Example or Code (if necessary and relevant)

Below is a minimal example of how Jetty 12 replaces input interception using a custom Content.Source wrapper.

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.io.Content;
import java.nio.ByteBuffer;

public class FilteringSource implements Content.Source {
    private final Content.Source next;

    public FilteringSource(Content.Source next) {
        this.next = next;
    }

    @Override
    public Content.Chunk read() {
        Content.Chunk chunk = next.read();
        if (chunk == null || chunk.isLast()) {
            return chunk;
        }

        ByteBuffer buf = chunk.getByteBuffer();
        byte[] data = new byte[buf.remaining()];
        buf.get(data);

        // modify data here
        byte[] modified = myFilter(data);

        return Content.Chunk.from(ByteBuffer.wrap(modified), false);
    }
}

How Senior Engineers Fix It

Experienced engineers replace the old interceptors by using Jetty 12’s new content pipeline:

1. Wrap the request body using Content.Source

  • Implement a custom Content.Source
  • Replace the request’s source via:
    • A custom Handler
    • A Filter that installs a new HttpChannel.Listener

2. Wrap the response body using Content.Sink

  • Implement a custom Content.Sink
  • Attach it to the response before writing begins

3. Use Jetty 12’s Handler chain

Senior engineers typically introduce a custom Handler that:

  • Reads the existing Content.Source
  • Wraps it with a filtering implementation
  • Replaces it on the request before the servlet executes

4. Avoid touching Jetty internals

Jetty 12 intentionally hides low‑level I/O details. The supported extension points are:

  • Handlers
  • Filters
  • Content.Source / Content.Sink
  • HttpChannel.Listener

Why Juniors Miss It

Junior engineers often struggle because:

  • They expect Jetty 12 to be a drop‑in upgrade, not a redesign.
  • They search for the old interceptor classes instead of the new content pipeline.
  • They assume request/response body interception belongs inside the servlet layer.
  • They overlook Jetty’s architectural shift toward stream‑based, non-blocking content handling.

The key takeaway: Jetty 12 requires a new mental model—interception now happens through content sources/sinks and handlers, not servlet I/O hooks.

Leave a Comment