square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
43.14k stars 7.3k forks source link

Web Service API that Consumes JSON Requires Raw Response Body (original string content used to parse JSON) #3720

Closed kevinvandenbreemen closed 2 years ago

kevinvandenbreemen commented 2 years ago

What kind of issue is this?

Given:

Currently the code in OkHttpCall.parseResponse(rawResponse) looks like this:


  Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
    ResponseBody rawBody = rawResponse.body();

    // Remove the body's source (the only stateful object) so we can pass the response along.
    rawResponse =
        rawResponse
            .newBuilder()
            .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
            .build();

    int code = rawResponse.code();

Scenario: I have a piece of software that is executing calls against a web service and getting responses as JSON and deserializing these.

I need a way to capture the original JSON body (string content) for use by another software system.

Unfortunately by the time the response body has been parsed it is no longer possible to get the body as the raw response is now a NoContentResponseBody whose string body is unavailable. See OkHttpCall.parseResponse(rawResponse). My program is building apis that return Response in order to write simpler code without callbacks in the context of coroutines.

Would it be possible to define a ResponseBody type that just contains the original response body as a string for use later (rather than the current NoContentResponseBody that is generated in OkHttpCall.parseResponse(rawResponse))? Would it be possible to have a parameter that could be passed to Retrofit.Builder specifying we want this kind of behaviour in our calls?

Thank you

JakeWharton commented 2 years ago

You can mix in this behavior with a custom Converter.Factory and a union type that holds both the raw and deserialized body.

Here's a full example:

final class WithRawBody<T> {
  private final ResponseBody raw;
  private final T body;

  public WithRawBody(ResponseBody raw, T body) {
    this.raw = raw;
    this.body = body;
  }

  public ResponseBody raw() {
    return raw;
  }

  public T body() {
    return body;
  }
}

final class WithRawBodyConverterFactory extends Converter.Factory {
  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
    Retrofit retrofit) {
    if (getRawType(type) != WithRawBody.class) {
      return null;
    }
    if (!(type instanceof ParameterizedType)) {
      throw new IllegalArgumentException("WithRawBody must have generic type");
    }
    Type bodyType = getParameterUpperBound(0, ((ParameterizedType) type));

    Converter<ResponseBody, Object> bodyConverter =
      retrofit.responseBodyConverter(bodyType, annotations);

    return (Converter<ResponseBody, Object>) value -> {
      byte[] rawBody = value.source().peek().readByteArray();
      ResponseBody rawResponse = ResponseBody.create(value.contentType(), rawBody);
      Object body = bodyConverter.convert(value);
      return new WithRawBody<>(rawResponse, body);
    };
  }
}

interface ExampleService {
  @GET("user")
  Call<WithRawBody<User>> getUser();
}

class User {
  String name;

  @Override public String toString() {
    return "User[name=" + name + "]";
  }
}

final class WithRawBodyMain {
  public static void main(String[] args) throws IOException {
    MockWebServer server = new MockWebServer();

    Retrofit retrofit = new Retrofit.Builder()
      .baseUrl(server.url("/"))
      .addConverterFactory(new WithRawBodyConverterFactory())
      .addConverterFactory(GsonConverterFactory.create())
      .build();

    ExampleService service = retrofit.create(ExampleService.class);

    server.enqueue(new MockResponse().setBody("{\"name\":\"Jake\"}"));
    WithRawBody<User> user = service.getUser().execute().body();

    System.out.println(user.body());
    System.out.println(user.raw().string());

    server.shutdown();
  }
}

This prints:

User[name=Jake]
{"name":"Jake"}