Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to automatically deserialize non-OK JSON response using WebClient and RestClient #4382

Closed
ikhoon opened this issue Aug 4, 2022 · 8 comments · Fixed by #5002

Comments

@ikhoon
Copy link
Contributor

ikhoon commented Aug 4, 2022

WebClient or RestClient is able to deserialize a JSON response into an object using:

WebClient client = WebClient.of(...);
ComletableFuture<ResponseEntity<MyItems>> response = 
  client.prepare()
        .get("/api/items/...")
        .asJson(MyItems.class)
        .execute();

The client tries to convert the JSON response into an object when the class of response status is a success.

private static <T> ResponseEntity<T> newJsonResponseEntity(AggregatedHttpResponse response,
JsonDecoder<T> decoder) {
if (!response.status().isSuccess()) {
throw newInvalidHttpResponseException(response);
}

I believe that most REST clients expect a 2xx status for normal situations.
However, when it comes to tests, it would be inconvenient. Users might want to test an error response.

So it could be useful to add some variant methods of .asJson(...) of WebClient and .execute(...) of RestClient

ComletableFuture<ResponseEntity<MyError>> response = 
  client.prepare()
        .get("/api/items/...?bad=parameter")
        .asJson(MyError.class, HttpStatus.INTERNAL_SERVER_ERROR)
        .execute();

In addition to HttpStatus, we can also add Predicate<HttpStatus> and HttpStatusClass as a parameter.

@ahnyujin
Copy link
Contributor

Hi @ikhoon, I understand the intent of this issue is to deserialize error response for the tests.
But I wonder why status is given as parameter for .asJson(MyError.class, HttpStatus.INTERNAL_SERVER_ERROR).
Is it to throw an error in the same way as before if the given status does not match?

@ikhoon
Copy link
Contributor Author

ikhoon commented Aug 11, 2022

But I wonder why status is given as parameter for .asJson(MyError.class, HttpStatus.INTERNAL_SERVER_ERROR).

The response format between a normal response and an error response may be different in most cases.
The deserialized type of response is decided when a client sends a request.
HttpStatus.isSuccess() is used whether the expected response data format is received.
If a specific HttpStatus or predicate is given, we may override the default predicate.

Is it to throw an error in the same way as before if the given status does not match?

We need to update the error message below. If a Predicate is set, we can skip the (expect: ... ) part.

" (expect: the success class (2xx). response: " + response, null);

@ikhoon
Copy link
Contributor Author

ikhoon commented Aug 11, 2022

For gentle error messages, we can internally create HttpStatusPredicate.

class HttpStatusPredicate<T> extends Predicate<T> {

    // Use the following fields to generate error messages
    @Nullable
    private HttpStatus status;
    @Nullable
    private HttpStatusClass statusClass;
    ...
}

// WebClientRequestPreparation
public <T> ... asJson(Class<? extends T> clazz, HttpStatus status) {
    return asJson(clazz, new HttpStatusPredicate(status));
}

public <T> ... asJson(Class<? extends T> clazz, Predicate<HttpStatus> predicate) {
    ...
}

@ahnyujin
Copy link
Contributor

ahnyujin commented Aug 13, 2022

Thanks a lot🙇 The part I was wondering about has been solved.
I'll try it!

@my4-dev
Copy link
Contributor

my4-dev commented Aug 31, 2022

Hi @ikhoon! I'm trying to this issue.
But, I can't understand how HttpStatusPredicate which you show upper would be used.
Please let me know at detail.

@ikhoon
Copy link
Contributor Author

ikhoon commented Sep 1, 2022

We can implement HttpStatusPredicate like the following.

final class HttpStatusPredicate extends Predicate<HttpStatus> {

    @Nullable
    private HttpStatus status;
    @Nullable
    private HttpStatusClass statusClass;
     
    ...    

    @Override
    boolean test(HttpStatus status) {
        if (this.status != null) {
            return this.status.equals(status);
        }
        assert statusClass != null;
        return status.codeClass() == statusClass;
    }

    @Nullable
    HttpStatus status() {
        return status;
    }

    @Nullable
    HttpStatusClass statusClass() {
        return statusClass;
    }
}

The custom HttpStatusPredicate will carry the desired status information,
and we can refactor AggregatedResponseAs to use HttpStatusPredicate for a simple condition.

// In AggregatedResponseAs
private static final HttpStatusPredicate SUCCESS_PREDICATE = new HttpStatusPredicate(HttpStatusClass.SUCCESS);

private static <T> ResponseEntity<T> 
    newJsonResponseEntity(AggregatedHttpResponse response, 
                          JsonDecoder<T> decoder) {
     return newJsonResponseEntity(response, decoder, SUCCESS_PREDICATE);
}

private static <T> ResponseEntity<T> 
    newJsonResponseEntity(AggregatedHttpResponse response, 
                          JsonDecoder<T> decoder, HttpStatus status) {
     return newJsonResponseEntity(response, decoder, new HttpStatusPredicate(status));
}

private static <T> ResponseEntity<T> 
    newJsonResponseEntity(AggregatedHttpResponse response, 
                          JsonDecoder<T> decoder, Predicate<HttpStatus> status) {
    if (status instanceof HttpStatusPredicate) {
       ... // Extract either HttpStatus or HttpStatusClass to provide the expected status.
    }
}

@my4-dev
Copy link
Contributor

my4-dev commented Sep 4, 2022

Previous pull request #4411 has a little problem, so I submit pull request again as #4412 .
Please review pull request #4412 🙏

@ikhoon
Copy link
Contributor Author

ikhoon commented Sep 7, 2022

Thanks for the pull request. I will take a look at it soon.

@jrhee17 jrhee17 added this to the 1.26.0 milestone Aug 16, 2023
@ikhoon ikhoon modified the milestones: 1.26.0, 1.27.0 Oct 20, 2023
@ikhoon ikhoon modified the milestones: 1.27.0, 1.28.0 Jan 16, 2024
jrhee17 added a commit that referenced this issue Apr 11, 2024
Motivation:

Please refer to #4382 

Modifications:

- Add some methods to specify what type of response is allowed 
- Add `HttpStatusPredicate` and `HttpStatusClassPredicate` class to
deserialize non-OK JSON response for more shortcut methods.

Result:

- Closes #4382 
- Users can deserialize non-OK JSON response like the following

```java
// WebClient
WebClient.of().prepare()
     .as(ResponseAs.blocking()
                   .andThen(res -> "Unexpected server error", res -> res.status().isServerError())
                   .andThen(res -> "missing header", res -> !res.headers().contains("x-header"))
                   .orElse(AggregatedHttpObject::contentUtf8))
     .execute();

// RestClient
final ResponseEntity<MyResponse> res =
        RestClient.of(server.httpUri()).get("/")
                  .execute(ResponseAs.blocking()
                                     .<MyResponse>andThenJson(MyError.class, res -> res.status().isClientError())
                                     .andThenJson(EmptyMessage.class, res -> res.status().isInformational())
                                     .orElseJson(MyMessage.class));
```

---------

Co-authored-by: jrhee17 <[email protected]>
Co-authored-by: Hannam Rhee <[email protected]>
Co-authored-by: minu <[email protected]>
Co-authored-by: minwoox <[email protected]>
Dogacel pushed a commit to Dogacel/armeria that referenced this issue Jun 8, 2024
…#5002)

Motivation:

Please refer to line#4382 

Modifications:

- Add some methods to specify what type of response is allowed 
- Add `HttpStatusPredicate` and `HttpStatusClassPredicate` class to
deserialize non-OK JSON response for more shortcut methods.

Result:

- Closes line#4382 
- Users can deserialize non-OK JSON response like the following

```java
// WebClient
WebClient.of().prepare()
     .as(ResponseAs.blocking()
                   .andThen(res -> "Unexpected server error", res -> res.status().isServerError())
                   .andThen(res -> "missing header", res -> !res.headers().contains("x-header"))
                   .orElse(AggregatedHttpObject::contentUtf8))
     .execute();

// RestClient
final ResponseEntity<MyResponse> res =
        RestClient.of(server.httpUri()).get("/")
                  .execute(ResponseAs.blocking()
                                     .<MyResponse>andThenJson(MyError.class, res -> res.status().isClientError())
                                     .andThenJson(EmptyMessage.class, res -> res.status().isInformational())
                                     .orElseJson(MyMessage.class));
```

---------

Co-authored-by: jrhee17 <[email protected]>
Co-authored-by: Hannam Rhee <[email protected]>
Co-authored-by: minu <[email protected]>
Co-authored-by: minwoox <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment