Modify response body retrofit 2.2 interceptor

2020-06-26 10:23发布

问题:

I'm developing an app using Retrofit 2 to request to API. This API is in ASP.NET and it is zipping with GZip and encoding to Base64, like the code below:

private static string Compress(string conteudo)
{
    Encoding encoding = Encoding.UTF8;
    byte[] raw = encoding.GetBytes(conteudo);

    using (var memory = new MemoryStream())
    {
        using (GZipStream gzip = new GZipStream(memory, CompressionMode.Compress, true))
        {
            gzip.Write(raw, 0, raw.Length);
        }
        return Convert.ToBase64String(memory.ToArray());
    }
}

private static string Decompress(string conteudo)
{
    Encoding encoding = Encoding.UTF8;
    var gzip = Convert.FromBase64String(conteudo);

    using (GZipStream stream = new GZipStream(new MemoryStream(gzip), CompressionMode.Decompress))
    {
        int size = gzip.Length;
        byte[] buffer = new byte[size];
        using (MemoryStream memory = new MemoryStream())
        {
            int count = 0;
            do
            {
                count = stream.Read(buffer, 0, size);
                if (count > 0)
                {
                    memory.Write(buffer, 0, count);
                }
            }
            while (count > 0);
            return encoding.GetString(memory.ToArray());
        }
    }
}

Now, what I need to do in my Android app is get the response from Retrofit, decode from Base64 and unzip it. I tried to do it using Interceptor, but I got no success.

This is the return that I received from the service H4sIAAAAAAAEACspKk0FAI1M/P0EAAAA, decoding and unzipping the response, we have true.

Does anybody know how to do it?

回答1:

It's easy. The code below uses Google Guava in order to decode Base64 character streams and Google Gson to deserialize JSON content.

Consider the following test service interface:

interface IService {

    @GET("/")
    Call<String> get();

}

Now you can implement your interceptor response input stream transformer base using the template method design pattern:

abstract class AbstractTransformingDecodingInterceptor
        implements Interceptor {

    protected abstract InputStream transformInputStream(InputStream inputStream)
            throws IOException;

    @Override
    @SuppressWarnings("resource")
    public final Response intercept(final Chain chain)
            throws IOException {
        final Request request = chain.request();
        final Response response = chain.proceed(request);
        final ResponseBody body = response.body();
        return response.newBuilder()
                .body(ResponseBody.create(
                        body.contentType(),
                        body.contentLength(),
                        Okio.buffer(Okio.source(transformInputStream(body.byteStream())))
                ))
                .build();
    }

}

This implementation should also detect content MIME types in order not to do wrong transformations, but you can implement it yourself easily. So here are also two transforming interceptors for both Base64 and GZip:

final class Base64DecodingInterceptor
        extends AbstractTransformingDecodingInterceptor {

    private static final Interceptor base64DecodingInterceptor = new Base64DecodingInterceptor();

    private Base64DecodingInterceptor() {
    }

    static Interceptor getBase64DecodingInterceptor() {
        return base64DecodingInterceptor;
    }

    @Override
    protected InputStream transformInputStream(final InputStream inputStream) {
        return BaseEncoding.base64().decodingStream(new InputStreamReader(inputStream));
    }

}
final class GzipDecodingInterceptor
        extends AbstractTransformingDecodingInterceptor {

    private static final Interceptor gzipDecodingInterceptor = new GzipDecodingInterceptor();

    private GzipDecodingInterceptor() {
    }

    static Interceptor getGzipDecodingInterceptor() {
        return gzipDecodingInterceptor;
    }

    @Override
    protected InputStream transformInputStream(final InputStream inputStream)
            throws IOException {
        return new GZIPInputStream(inputStream);
    }

}

And test it:

private static final OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .addInterceptor(getGzipDecodingInterceptor())
        .addInterceptor(getBase64DecodingInterceptor())
        .addInterceptor(getFakeContentInterceptor())
        .build();

private static final Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://whatever")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

private static final IService service = retrofit.create(IService.class);

public static void main(final String... args)
        throws IOException {
    final String body = service.get().execute().body();
    System.out.println(body);
}

Note that getFakeContentInterceptor returns a fake interceptor that always returns H4sIAAAAAAAEACspKk0FAI1M/P0EAAAA so that baseUrl does not even have a real URL. The output:

true



回答2:

Another way is to add Filter and modify the request / response objects with wrappers.

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    // We need to convert the ServletRequest to MultiReadRequest, so that we could intercept later
    MultiReadHttpServletRequest multiReadRequest =
        new MultiReadHttpServletRequest((HttpServletRequest) request);
    HttpServletResponseWrapper responseWrapper =
        new HttpServletResponseWrapper((HttpServletResponse) response);
    chain.doFilter(multiReadRequest, responseWrapper);
  }

You can add a proxy layer (jetty proxy servlet org.eclipse.jetty.proxy.ProxyServlet) and override methods : onResponseContent, addProxyHeaders

 public void handleProxyResponse(
      HttpServletRequest request,
      HttpServletResponse response,
      byte[] buffer,
      int offset,
      int length,
      Callback callback) {
    try {
      if (response.getStatus() == HttpStatus.OK_200
          && request.getRequestURI().startsWith(INTERCEPTED_END_POINT)) {
        String output;
        boolean acceptsGZipEncoding = acceptsGZipEncoding(request);
        if (acceptsGZipEncoding) {
          output = plainTextFromGz(buffer);
        } else {
          output = new String(buffer);
        }
        String proxyHost = getRequestUrlBase(request);
        try {
          // TODO: get this from config object
          output = output.replace(ProxyServer.REMOTE_HOST, proxyHost);
          byte[] outBuffer;
          if (acceptsGZipEncoding) {
            outBuffer = gzFromPlainText(output);
          } else {
            outBuffer = output.getBytes();
          }
          HttpServletResponseWrapper responseWrapper = (HttpServletResponseWrapper) response;
          responseWrapper.getResponse().reset();
          responseWrapper.getOutputStream().write(outBuffer);
        } catch (Exception e) {
          log.error(e.getMessage(), e);
          // Error in parsing json, writing original response
          response.getOutputStream().write(buffer, offset, length);
        }
      } else {
        response.getOutputStream().write(buffer, offset, length);
      }
      callback.succeeded();
    } catch (Throwable e) {
      callback.failed(e);
    }
  }

  private String getRequestUrlBase(HttpServletRequest request) {
    logHeaders(request);
    return request.getHeader(HttpHeader.HOST.name());
  }

  private String replaceBaseFromUrl(String url, String base) throws Exception {
    URI uri = new URI(url);
    StringBuffer sb = new StringBuffer("");
    if (base.startsWith("http")) {
      sb.append(base);
    } else {
      sb.append("http://").append(base);
    }
    sb.append(uri.getPath());
    if (!Strings.isNullOrEmpty(uri.getQuery())) {
      sb.append("?").append(uri.getQuery());
    }
    return sb.toString();
  }

  private boolean acceptsGZipEncoding(HttpServletRequest httpRequest) {
    String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
    return acceptEncoding != null && acceptEncoding.indexOf("gzip") != -1;
  }

  private byte[] gzFromPlainText(String plainText) {
    if (Strings.isNullOrEmpty(plainText)) {
      return null;
    }

    try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final GZIPOutputStream gzipOutput = new GZIPOutputStream(baos)) {
      gzipOutput.write(plainText.getBytes());
      gzipOutput.finish();
      return baos.toByteArray();
    } catch (IOException e) {
      log.error("Could not convert plain text to gz", e);
    }
    return plainText.getBytes();
  }

  private String plainTextFromGz(byte[] gz) {
    try (GZIPInputStream gzipIn = new GZIPInputStream(new ByteArrayInputStream(gz))) {
      return IOUtils.toString(gzipIn, Charset.defaultCharset());
    } catch (IOException e) {
      log.error("Could not write gz to plain text", e);
    }
    return new String(gz);
  }