Crack the Header Code: GZIP Compression and HTTP Filters with Quarkus
Learn how to inspect, manipulate, and optimize HTTP traffic in your Java REST APIs using Quarkus and custom filters.
HTTP headers are the quiet diplomats of every web exchange. They tell the client what’s coming, describe the server’s mood, and even ask the browser to behave a certain way. In Quarkus, headers aren’t just metadata, they’re tools you can inspect, inject, and manipulate to make your APIs smarter and faster.
In this hands-on tutorial, we’ll go a little deeper than just hello-world endpoints and look at payload size and network efficiency. We’ll start by inspecting request headers, add custom ones to responses, then implement both built-in and custom GZIP compression using JAX-RS filters.
Set Up the Project
Let's start by creating a new Quarkus project. Open your terminal and run the following command:
mvn io.quarkus:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=http-header-tutorial \
-DclassName="org.acme.HeaderResource" \
-Dpath="/hello" \
-Dextensions="rest-jackson"
cd http-header-tutorial
This command generates a simple Quarkus project with Quarkus Rest and a JAX-RS endpoint. If you want to just see the source, take a look at the Github repository.
A Glimpse into HTTP Headers
HTTP headers are key-value pairs of strings sent with every HTTP request and response. They provide metadata about the message, such as:
Content-Type
: The media type of the resource (e.g.,application/json
,text/html
).Content-Length
: The size of the response body in bytes.Cache-Control
: Directives for caching mechanisms in both clients and proxies.Authorization
: Credentials for authenticating the client with the server.Accept-Encoding
: Informs the server which content encoding (compression algorithm) the client can understand.
Inspecting and Adding HTTP Headers
Edit src/main/java/org/acme/HeaderResource.java
to inspect request headers and inject a custom response header.
package org.acme;
import java.util.stream.Collectors;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.HttpHeaders;
@Path("/hello")
public class HeaderResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response hello(@Context HttpHeaders headers) {
String allHeaders = headers.getRequestHeaders().entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue())
.collect(Collectors.joining("\n"));
return Response.ok("Hello from RESTEasy Reactive!\n\n" + "Your request headers:\n" + allHeaders)
.header("X-Custom-Header", "Hello from the server!")
.build();
}
}
In this code:
We inject the
HttpHeaders
context to get access to the request headers.We read all the headers and return them in the response body.
We add a custom header,
X-Custom-Header
, to our response.
Run the application in development mode:
./mvnw quarkus:dev
Then in another terminal:
curl -i http://localhost:8080/hello
The -i
flag includes the response headers in the output. You should see something like this:
curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 114
X-Custom-Header: Hello from the server!
Hello from RESTEasy Reactive!
Your request headers:
Accept: [*/*]
User-Agent: [curl/8.7.1]
Host: [localhost:8080]
As you can see, our custom header is present, and we've successfully read the incoming request headers.
Simulate a Large Response
Now, let's simulate a large response. We'll create a new endpoint that returns a significant amount of JSON data.
Add the following method to your HeaderResource.java
:
@GET
@Path("/large-payload")
@Produces(MediaType.APPLICATION_JSON)
public Response getLargePayload() {
String largeJson = "{\"data\":[" +
"{\"id\":1,\"name\":\"A very long name to make the payload larger\"},"
.repeat(1000)
+
"{\"id\":1001,\"name\":\"Final entry\"}" +
"]}";
return Response.ok(largeJson).build();
}
No need to restart Quarkus. Just test it:
curl -i http://localhost:8080/hello/large-payload > large_response.json
ls -lh large_response.json
You'll see that the large_response.json
file is several kilobytes (62kb) in size. In a real-world scenario, with even larger payloads and numerous concurrent users, this can lead to increased bandwidth costs and slower response times. This is where GZIP compression comes to the rescue.
Enabling GZIP Compression (The Easy Way for the build in HTTP Server)
The body of an HTTP response is not compressed by default. You can enable the HTTP compression support by adding a property to the application.properties file like shown below or you can annotate your methode with @io.quarkus.vertx.http.Compressed.
Edit src/main/resources/application.properties
and enable compression:
quarkus.http.enable-compression=true
quarkus.http.compress-media-types=application/json
Quarkus will now automatically compress responses with known media types (text/*
, application/json
, etc.) if the client requests it.
Try it out:
curl -i -H "Accept-Encoding: gzip" http://localhost:8080/hello/large-payload
Look for this in the response:
Content-Encoding: gzip
The Content-Encoding: gzip
header indicates that the response body has been compressed. If you try to save the output to a file, you'll notice it's a binary file. To see the compressed size, you can pipe the output to wc -c
:
curl -s -H "Accept-Encoding: gzip" http://localhost:8080/hello/large-payload | wc -c
You’ll see a big difference compared to the uncompressed size. But what if you want more control?
Manual GZIP Compression with a JAX-RS Filter
While the configuration property is convenient, there are times when you need more control over the compression logic. For instance, you might want to:
Compress different content types with varying logic.
Implement a different compression algorithm.
Add custom logging or metrics around your compressed responses.
This is where JAX-RS filters shine. We'll create a ContainerResponseFilter
to implement GZIP compression manually. First disable the compression and remove the two lines from the application.properties.
Now create a response filter: src/main/java/org/acme/filter/GzipResponseFilter.java
package org.acme.filter;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
@Provider
public class GzipResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
String acceptEncoding = requestContext.getHeaderString("Accept-Encoding");
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
if (responseContext.getHeaders().containsKey("Content-Encoding")) return;
if (responseContext.hasEntity()) {
responseContext.getHeaders().putSingle("Content-Encoding", "gzip");
responseContext.setEntityStream(new GZIPOutputStreamWrapper(responseContext.getEntityStream()));
}
}
}
}
Then, the GZIPOutputStreamWrapper
:
src/main/java/org/acme/filter/GZIPOutputStreamWrapper.java
package org.acme.filter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.GZIPOutputStream;
public class GZIPOutputStreamWrapper extends GZIPOutputStream {
public GZIPOutputStreamWrapper(OutputStream out) throws IOException {
super(out);
}
}
The GzipResponseFilter
:
@Provider
: This annotation tells Quarkus that this class is a JAX-RS provider and should be registered automatically.ContainerResponseFilter
: This interface defines thefilter
method, which is executed for every response.filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
:We first check the
Accept-Encoding
header from the client's request to see if it supports GZIP.We also check if the response isn't already encoded.
If GZIP is supported and the response has a body (
hasEntity()
), we add theContent-Encoding: gzip
header.The core of the logic is wrapping the original response's
OutputStream
with aGZIPOutputStream
. This will compress the data on the fly as it's being written to the client.
Try again:
curl -i -H "Accept-Encoding: gzip" http://localhost:8080/hello/large-payload
You should see Content-Encoding: gzip
again—but this time, it’s your logic, not Quarkus’s built-in setting.
Why This Matters
You’ve now mastered HTTP headers in Quarkus. How to inspect them, how to modify responses, and how to use them to guide dynamic behaviors like compression. You’ve seen how to toggle between framework-supplied convenience and full control with custom filters.
Want to take it further? Add Brotli compression support. Create a metrics filter that logs compression efficiency. Or combine this with ETag or Cache-Control headers to deliver high-performance APIs with bandwidth awareness baked in.
The HTTP layer is no longer just a transport. It’s your optimization playground.