Micronaut-core: Error Handling (`@Error`) for HttpClientResponseException does not provide response.body (body is null / empty)

Created on 11 Nov 2019  Â·  24Comments  Â·  Source: micronaut-projects/micronaut-core

Running on micronaut 1.2.6 and Kotlin 1.3.50.

Consider the following snippet:

@Client("\${formValidatorService.host}")
interface FormValidatorServiceClient {

    @Post("/validate")
    fun validate(@Body validationRequest: Single<FormSubmission>): Single<HttpResponse<ValidationResponseValid>>

}

@Controller("/forms")
open class FormSubmissionController(private val formValidatorServiceClient: FormValidatorServiceClient) : FormSubmissionOperations {

    override fun validate(submission: Single<FormSubmission>): Single<HttpResponse<ValidationResponseValid>> {
        return formValidatorServiceClient.validate(submission).map {
            HttpResponse.ok(it.body()!!)
        }
    }

    @Error
    fun formSubmissionException(request: HttpRequest<*>, exception: HttpClientResponseException): Single<HttpResponse<JsonError>> {
        println(exception.response.body())
        return Single.just(
                HttpResponse.status<JsonError>(HttpStatus.BAD_REQUEST, "Form Validation Failure")
                        .body(JsonError(exception.response.body().toString()))
        )
    }
}


@Validated
interface FormSubmissionOperations {

    @Post(value = "/validate", consumes = [MediaType.APPLICATION_JSON], produces = [MediaType.APPLICATION_JSON])
    fun validate(@Body submission: Single<FormSubmission>): Single<HttpResponse<ValidationResponseValid>>
}

When /validate is run the @Error is triggered by a 400 response returned by the @Client, but the body is always null.

Using TRACE on the HTTP Client, I can confirm that valid JSON is being returned in the response of the HTTP Client's request.

Also using response.getBody(BodyType), returns Optional.empty

Issue seems very similar to https://github.com/micronaut-projects/micronaut-core/issues/416

notabug

All 24 comments

So if the body is empty for the @Error handler I think that could possibly be considered a bug. Thoughts @jameskleeh ?

@graemerocher how does usage of @Error reconcile with your notes about using the onError..() methods when using reactive? (re #416) ?

@graemerocher I think the body should be available since the error handler should be invoked as part of the reactive flow

that is my thought too

Any thoughts on a workaround in the mean time?

After more testing it seems that the body is always returned as null for the HttpClientResponseException.

If you remove the error function and use .onErrorResumeNext{..} the response.body still returns null

I believe I have produced a collection of two sample apps that reproduce the behavior. See the README.md

https://github.com/willbuck/micronaut-core-issue-2324

@StephenOTT it does appear the response body is mapped to the exception.response.convertedBodies list. See the example above, that may be a workaround

So in speaking with @jameskleeh it seems what you are probably wanting is exception.response.getBody(JsonError::class.java). That will get you the returned body of the client call.

Note that exception.response.body() is meant only as a shorthand for a successful response.

What does a successful response in a exception entail? Are you saying that .body() is to be used only after getBody(..)?

The .body() method is designed to return the body based on the provided body type. Given the body type is different in the error condition, it returns empty.

So you are saying that JsonError is the initial type of the body in a @Error local error handling function that is targeting the HttpClientResponseException, and you convert it from JsonError into another type?

When defining a @Client, the default error type is JsonError. You can change that with @Client(errorType =. That will control how the body is decoded in the error condition. In either case, the .body() method is only designed to return the body of a successful response, which is controlled by the return type of the method.

Sorry but just trying to make sense of this: when you say successful response you mean in the scenario on a non-error.

So to generically handle errors on Clients (because you cannot add the error types on a per method basis), you would have to create a generic class that is essentially a map that takes in any Json fields present in the response and set the client error type to this generic class. And then convert from that generic type to the real expected type?

That is one way to go about it, however only really necessary for blocking operations. If you are processing the exception in the error handler of a reactive response, the body is available to be converted directly. exception.response.getBody(SomeType). The reason it isn't available to do so in blocking operations is because the body must be released and we wouldn't have any idea when that should be done if the client is no longer part of the flow.

Okay so do we consider using @Error handlers to be blocking functions? If following a reactive flow, the recommendation would be to not use @Error handlers, and rather use the onErrorReturnNext(). ?

If the return type of the client is a reactive flow then the error method will be called as part of the flow so the body should be available to be converted to any type that is appropriate

Yes I understand we can use the reactive error handling, but is the micronaut @Error handlers considered a blocking call? Aka when using reactive flows, and you don't want a blocking call, you must not use @Error handlers ?

No. Like I said the @error method is invoked as part of the reactive flow
of the return type is reactive.

On Fri, Dec 6, 2019 at 5:39 PM Stephen Russett notifications@github.com
wrote:

Yes I understand we can use the reactive error handling, but is the
micronaut @Error handlers considered a blocking call? Aka when using
reactive flows, and you don't want a blocking call, you must not use
@Error handlers ?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/micronaut-projects/micronaut-core/issues/2324?email_source=notifications&email_token=AAMCVLP4WRRU2GAYDDAGBJLQXLIBHA5CNFSM4JLX4FQKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEGFR5GA#issuecomment-562765464,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAMCVLLS2POP7JLQTRFP3SDQXLIBHANCNFSM4JLX4FQA
.

Can you provide an example of this?

@StephenOTT can you clarify what kind of example you're looking for?

I was able to get response in reactive mode with below example

  .onErrorMap { error ->
                            if (error is HttpClientResponseException) {
                                val ex = error.response.getBody(MyException::class.java)
                                //do handle response
                            }
                          }

Can someone share whether any workaround or fix was released on how to handle the null body in the http service declarative calls?

@readalong can you elaborate / provide a small example to demonstrate what you mean? Does exception.response.getBody(JsonError::class.java) do what you're trying to do?

If some poor soul is still trying to figure out what to do in this one, the way it worked for me was:

Stream

handle the exception in the stream and make sure to parse the body (with the error)
don't throw or rethrow the exception that's is set to happen
.doOnError(throwable -> {

                if (throwable instanceof HttpClientResponseException) {
                    HttpClientResponseException ee = (HttpClientResponseException) throwable;

                    //must be called to process the body

                    var error_ = ee.getResponse().getBody(MyErrorDto.class);

                }

Error handler

create an handler for that error

@Error
public HttpResponse<MyResponseResponse> error(HttpClientResponseException ex) {
    var error_ = ex.getResponse().getBody(MyErrorDto.class).orElse(new MyErrorDto());

    return HttpResponse.<MyResponseResponse>status(HttpStatus.BAD_REQUEST).body(error_);
}

If you remove the doOnError() cycle the body value will be dumped, try out by yourself

Hope it helps.

Was this page helpful?
0 / 5 - 0 ratings