GZIP request payload Compression in Apigee Java Callout

I am implementing a Java Callout in Apigee to GZIP-compress an XML payload before sending it to a backend endpoint.

The input XML is stored in the variable inputPayload. My Java Callout successfully runs, output is stored in request.content and javacallout is attached as last policy before going to target, so in Apigee Trace shows this:

  • request.content → shown in Trace as [B@709ec03e.

Apigee Trace does not show the raw binary request

My backend server rejects the request, reporting the body is not in valid gzip format.

I am unsure whether Apigee is actually sending the correct gzipped binary, or if it is still passing string instead of true compressed bytes.

Content-type header is set to application/zip

Please guide me how to resolve this.

3 Likes

Hi Rimsha_Aizaz,

While it is possible to pass binary payloads through Apigee, for binary payloads to work, the payload cannot be touched by Apigee by any variable reference. Therefore it is not possible to take XML and convert it to a binary format from within Apigee.

One approach may be to leverage Cloud Run to create a multi-step flow. It could look like Consumer → primary endpoint (on Apigee) → passes XML to Cloud Run → which compresses the payload → calling secondary endpoint (on Apigee) passing through → target. A single proxy can support both endpoints and both consumer and target management will still be handled by Apigee. Cloud Run then provides both the conversion and the clean payload needed for the target.

Cheers,

1 Like

Hi @paul-wright

Thanks for your response.

So there isn’t any solution handling it through java using the Java Callout?

I have been trying to do it with the jar like below:

Backend wants the request as an input stream

public class GzipRequestSigner implements Execution {

@Override
public ExecutionResult execute(MessageContext mc, ExecutionContext ec) {
    try {
        // Read input payload (your signed XML)
        String signedPayload = (String) mc.getVariable("signedPayload");
        if (signedPayload == null) {
            signedPayload = "";
        }

        // GZIP compress the payload into byte[]
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPOutputStream gzipOut = new GZIPOutputStream(baos)) {
            gzipOut.write(signedPayload.getBytes(StandardCharsets.UTF_8));
        }
        byte[] gzipped = baos.toByteArray();

        // Assign gzipped bytes directly to request.content
        mc.setVariable("request.content", gzipped);
        mc.setVariable("message.isBinary", true);
        mc.setVariable("request.header.Content-Type", "application/zip");
        mc.setVariable("request.header.Content-Encoding", null);

        // Let Apigee send chunked
        mc.setVariable("request.header.Transfer-Encoding", "chunked");
        mc.setVariable("request.header.Content-Length", null);

        // Debug info for Trace
        mc.setVariable("debug.original.length", signedPayload.getBytes(StandardCharsets.UTF_8).length);
        mc.setVariable("debug.gzipped.length", gzipped.length);

        return ExecutionResult.SUCCESS;

    } catch (Exception e) {
        mc.setVariable("GzipError", e.toString());
        return ExecutionResult.ABORT;
    }
}

}

Really appreciate your support.

1 Like

I believe you do not need a Java Callout for this . Apigee should do that for you automatically if you provide the proper header. You need an AssignMessage policy, attached in the Request flow of the target:

<AssignMessage name="AM-Set-Compression">
    <Set>
        <Headers>
            <Header name="Content-Encoding">gzip</Header>
        </Headers>
    </Set>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

If Apigee receives an uncompressed payload, or if you have set an uncompressed XML payload in Apigee, then, by setting this header you are telling Apigee to compress with gzip before proxying the request to the upstream system.

I guess that is expected with the content is a byte array.

What you are doing should work but it’s the long way around.
Is it possible your backend is looking at the content-encoding header and rejecting based on not seeing ‘gzip’ in that header? The other possibility is that you haven’t encoded the gzipped byte array properly.

I can’t help with diagnosing that; you’d need a wire trace or some way to verify the gunzip of the payload you produce.

But you can avoid all that by just using the built-in auto-compression.

1 Like

Hi @dchiesa1 ,

The backend does not just accept the request with header parameter Content-Encoding. According to the backend implementation the request should be passed as stream of input compressed with ‘gzip’ and header to be passed as application/zip.

I really appreciate your response and have tried passing the payload as is with only the header as gzip but did not work.

Do we need to enable the streaming in properties or set the compression algorithm in properties?

I am pasting the chunk of backend code gzip implementation for any reference.

  httpsURLConnection = (HttpsURLConnection) uRL.openConnection();

            httpsURLConnection.setHostnameVerifier(myhostnameverifier);

            httpsURLConnection.setDoOutput(true);

            httpsURLConnection.setDoInput(true);

            httpsURLConnection.setRequestMethod("POST");

            // httpsURLConnection.getResponseCode()

            OutputStream out;

            if (msgType.trim().equalsIgnoreCase("Payroll")) {

                httpsURLConnection.setRequestProperty("Content-Type", "application/zip");

                out = httpsURLConnection.getOutputStream();

                GZIPOutputStream gzos = new GZIPOutputStream(out);

                System.out.println(gzos);

                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(gzos));

                bufferedWriter.write(new String(qstring, "UTF-8"));

                bufferedWriter.close();

                System.out.println("Zip File created and send to the server");

            } else {

                httpsURLConnection.setRequestProperty("Content-Type", "text/xml");

                out = httpsURLConnection.getOutputStream();

                out.write(qstring, 0, qstring.length);

                out.flush();

            }

Hi Rimsha_Aizaz,

Apigee can pass binary payloads, but the payload must remain untouched—so converting XML to binary within Apigee isn’t possible. A practical approach is using Cloud Run in a multi-step flow: the consumer sends XML to Apigee, which forwards it to Cloud Run for compression or conversion, then Cloud Run calls a secondary Apigee endpoint to pass the processed binary payload to the target. This way, a single proxy manages both endpoints while Apigee handles consumer and target management, and Cloud Run ensures the payload is properly transformed.

1 Like

Resolving GZIP Compression Issues in Apigee Java Callout

The [B@709ec03e output indicates you have a byte array, which is good, but there are several common issues that cause gzip failures in Apigee. Here’s how to resolve them:

Common Issues & Solutions

1. Incorrect Content-Type Header

Content-Type: application/gzip  (NOT application/zip)
  • Use application/gzip or application/x-gzip for gzipped content

  • application/zip is for ZIP archives (different format)

2. Message Assignment in Java Callout

Your Java callout must properly write the compressed bytes. Here’s the correct implementation:

java

import com.apigee.flow.execution.ExecutionContext;
import com.apigee.flow.execution.ExecutionResult;
import com.apigee.flow.execution.spi.Execution;
import com.apigee.flow.message.MessageContext;
import java.io.ByteArrayOutputStream;
import java.util.zip.GZIPOutputStream;

public class GzipCompressor implements Execution {
    
    public ExecutionResult execute(MessageContext messageContext, 
                                   ExecutionContext executionContext) {
        try {
            // Read input XML
            String inputPayload = messageContext.getVariable("inputPayload");
            
            // Compress
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream);
            gzipStream.write(inputPayload.getBytes("UTF-8"));
            gzipStream.close(); // CRITICAL: Must close to finalize GZIP format
            
            byte[] compressedData = byteStream.toByteArray();
            
            // Set as message content
            messageContext.getMessage().setContent(compressedData);
            
            // Set headers
            messageContext.setVariable("request.header.Content-Type", "application/gzip");
            messageContext.setVariable("request.header.Content-Encoding", "gzip");
            
            return ExecutionResult.SUCCESS;
            
        } catch (Exception e) {
            messageContext.setVariable("gzip.error", e.getMessage());
            return ExecutionResult.ABORT;
        }
    }
}

3. Critical: Close the GZIPOutputStream

The most common mistake is not closing the GZIPOutputStream:

java

gzipStream.close(); // Writes GZIP trailer - MUST be called

Without closing, the GZIP format is incomplete and invalid.

4. Set Both Headers

xml

<AssignMessage name="SetGzipHeaders">
    <Set>
        <Headers>
            <Header name="Content-Type">application/gzip</Header>
            <Header name="Content-Encoding">gzip</Header>
        </Headers>
    </Set>
</AssignMessage>
```

### 5. **Policy Flow Order**
```
JavaCallout (compress) → AssignMessage (set headers) → TargetEndpoint

Or set headers directly in Java callout as shown above.

Verification Steps

Test with curl on backend directly:

bash

# Create test gzipped file
echo '<test>data</test>' | gzip > test.gz

# Send to your backend
curl -X POST https://your-backend.com/endpoint \
  -H "Content-Type: application/gzip" \
  -H "Content-Encoding: gzip" \
  --data-binary @test.gz -v

If this works, your backend accepts gzip correctly.

Add Debug Policy After Java Callout

xml

<AssignMessage name="DebugCompressed">
    <AssignVariable>
        <Name>debug.contentLength</Name>
        <Value>{request.content.length()}</Value>
    </AssignVariable>
    <AssignVariable>
        <Name>debug.contentType</Name>
        <Value>{request.header.Content-Type}</Value>
    </AssignVariable>
</AssignMessage>

Check in Trace that debug.contentLength shows a reasonable compressed size.

Validate GZIP Format in Java Callout

Add validation before sending:

java

// After compression, validate it's readable
try (GZIPInputStream testStream = new GZIPInputStream(
        new ByteArrayInputStream(compressedData))) {
    // If this doesn't throw exception, GZIP is valid
    testStream.read();
} catch (Exception e) {
    throw new Exception("Generated invalid GZIP: " + e.getMessage());
}

Complete Working Example

java

import com.apigee.flow.execution.*;
import com.apigee.flow.execution.spi.Execution;
import com.apigee.flow.message.MessageContext;
import java.io.*;
import java.util.zip.GZIPOutputStream;

public class GzipCompressor implements Execution {
    
    public ExecutionResult execute(MessageContext msgCtx, ExecutionContext execCtx) {
        try {
            // Get input
            String input = msgCtx.getVariable("inputPayload");
            if (input == null || input.isEmpty()) {
                msgCtx.setVariable("gzip.error", "inputPayload is empty");
                return ExecutionResult.ABORT;
            }
            
            // Compress
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
                gzos.write(input.getBytes("UTF-8"));
            } // Auto-closes and finalizes GZIP
            
            byte[] compressed = baos.toByteArray();
            
            // Set content and headers
            msgCtx.getMessage().setContent(compressed);
            msgCtx.setVariable("request.header.Content-Type", "application/gzip");
            msgCtx.setVariable("request.header.Content-Encoding", "gzip");
            msgCtx.setVariable("request.header.Content-Length", 
                             String.valueOf(compressed.length));
            
            // Debug info
            msgCtx.setVariable("gzip.originalSize", input.length());
            msgCtx.setVariable("gzip.compressedSize", compressed.length);
            
            return ExecutionResult.SUCCESS;
            
        } catch (Exception e) {
            msgCtx.setVariable("gzip.error", e.toString());
            return ExecutionResult.ABORT;
        }
    }
}

Key Takeaways

  1. Always close GZIPOutputStream - use try-with-resources

  2. Use application/gzip not application/zip

  3. Set Content-Encoding: gzip header

  4. Call messageContext.getMessage().setContent(bytes)

  5. Validate backend expects gzip in body (not Content-Encoding transparent decompression)

The [B@709ec03e in Trace is normal for binary content. Focus on ensuring the Java callout properly closes the GZIP stream and sets correct headers.

2 Likes

Thanks @Victor_Camacho for your time and the detailed response.

I would try to resolve first without using Java callout and just setting up headers for gzip. If that did not work I would definitely go with your solution.

Really appreciate it.

Hi @dchiesa1

I am sending a request from Apigee with the following headers:

Content-Type: application/zip  
Content-Encoding: gzip  

The backend returns the response as text/xml (as expected).

The issue is that when the response reaches Apigee, the Content-Type automatically changes to application/zip, and the response body appears as unreadable (garbled) binary data in Trace.

I have already tried setting the Content-Type in the response to text/xml; charset=UTF-8 and added Accept: text/xml in the request, but the issue persists.

It seems Apigee is overriding the backend’s response headers once the request includes Content-Encoding: gzip. Could you please suggest how to make Apigee forward or preserve the backend’s actual text/xml content type instead of forcing it to application/zip?

The backend team confirms they are returning plain XML, not zipped data.