Skip to content

Commit

Permalink
Add deflate as a supported REST response encoding
Browse files Browse the repository at this point in the history
Some HTTP servers don't parse Accept-Encoding headers as they should
and just assume that deflate is commonly available.

okhttp3 has built-in support for transparently adding gzip as an
accepted encoding, and decoding the response body,
which is how gzip previously Just Worked.

This built-in encoding handler is conditional on the user not passing in
any Accept-Encoding headers, which is assumed to indicate that the
caller intends to somehow handle the encoded response body themself.

We can implement our own transparent decoding support
using the same mechanism that okhttp3 does internally
and add an Interceptor callback that adds headers before the request
and returns a new response with the encoding headers removed
and the body wrapped in decoding readers.

This is strictly more correct than the okhttp3 built-in decoder
because Content-Encoding can be a sequence of encodings applied in
order that have to be decoded in reverse order.
  • Loading branch information
fishface60 committed Oct 22, 2024
1 parent c5c1f62 commit 61307e0
Showing 1 changed file with 75 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.Inflater;
import net.rptools.maptool.client.AppPreferences;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.language.I18N;
Expand All @@ -33,11 +35,17 @@
import net.rptools.parser.VariableResolver;
import net.rptools.parser.function.AbstractFunction;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSource;
import okio.GzipSource;
import okio.InflaterSource;
import okio.Okio;

/**
* RESTful based functions REST.get, REST.post, REST.put, REST.patch, REST.delete
Expand Down Expand Up @@ -67,7 +75,7 @@ public static RESTfulFunctions getInstance() {
return instance;
}

private final OkHttpClient client = new OkHttpClient();
private final OkHttpClient client = buildClient();
private final Gson gson = new Gson();

@Override
Expand Down Expand Up @@ -236,6 +244,72 @@ private Object executeClientCall(String functionName, Request request, boolean f
}
}

private OkHttpClient buildClient() {
return new OkHttpClient.Builder()
.addInterceptor(
(Interceptor.Chain chain) -> {
var oldRequest = chain.request();

// If the macro has passed its own Accept-Encoding
// it's indicating it expects to somehow handle it itself.
if (oldRequest.header("Accept-Encoding") != null) {
return chain.proceed(oldRequest);
}

// Augment request saying we accept multiple content encodings
var newHeaders =
oldRequest
.headers()
.newBuilder()
.add("Accept-Encoding", "deflate")
.add("Accept-Encoding", "gzip")
.build();

var newRequest = oldRequest.newBuilder().headers(newHeaders).build();

var oldResponse = chain.proceed(newRequest);

// Replace the response's request with the original one
var responseBuilder = oldResponse.newBuilder().request(oldRequest);

// We might not have a body to decompress
var body = oldResponse.body();
if (body != null) {
BufferedSource source = body.source();
// The body may have been wrapped in an arbitrary encoding sequence
// and the server returns them in the order it encoded them
// so we wrap them with decoders in reverse order.
var encodings = oldResponse.headers().values("Content-Encoding");
Collections.reverse(encodings);
for (var encoding : encodings) {
if ("deflate".equalsIgnoreCase(encoding)) {
var inflater = new Inflater(true);
source = Okio.buffer(new InflaterSource(source, inflater));
} else if ("gzip".equalsIgnoreCase(encoding)) {
source = Okio.buffer(new GzipSource(source));
}
}

// Strip encoding and length headers as we've already handled them
var strippedHeaders =
oldResponse
.headers()
.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
var contentType = MediaType.parse(oldResponse.header("Content-Type"));
// Construct a new body with an inferred Content-Length
var newBody = ResponseBody.create(contentType, -1L, source);
responseBuilder.body(newBody);
}

return responseBuilder.build();
})
.build();
}

private Headers buildHeaders(Map<String, List<String>> headerMap) {
Headers.Builder headerBuilder = new Headers.Builder();

Expand Down

0 comments on commit 61307e0

Please sign in to comment.