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

Improve documentation for custom exception requirements for RestClient #31783

Closed
ciscoo opened this issue Dec 7, 2023 · 0 comments
Closed

Improve documentation for custom exception requirements for RestClient #31783

ciscoo opened this issue Dec 7, 2023 · 0 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: documentation A documentation task
Milestone

Comments

@ciscoo
Copy link

ciscoo commented Dec 7, 2023

Affects: 6.1.1


It was not immediately clear to me, that custom exceptions cannot be any of the following types:

  • UncheckedIOException
  • IOException
  • HttpMessageNotReadableException

If for example you define an exception as:

public class MyException extends IOException { }

// or

public class MyException extends UncheckedIOException { }

And attempt to throw it in a status handler:

RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
try {
    restClient.get()
        .uri("/products/{id}", 99999999)
        .retrieve()
        .onStatus(HttpStatusCode::isError, (req, resp) -> {
            throw new MyException();
        })
        .body(Product.class);
} catch (MyException ex) {
    // does not reach here
}

Does not work.

Instead a RestClientException is thrown because the (3) exceptions I listed above are explicitly caught here and as a result, to access your custom exception, you have to unwrap it:

RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
try {
    restClient.get()
        .uri("/products/{id}", 99999999)
        .retrieve()
        .onStatus(HttpStatusCode::isError, (req, resp) -> {
            throw new MyException();
        })
        .body(Product.class);
} catch (RestClientException ex) {
    MyException myEx = (MyException) ex.getRootCause();
}

The same also applies when using defaultStatusHandler when building the RestClient as well.

It would be nice to mention somewhere in the documentation and Javadoc that custom exceptions cannot be either of the (3) types above nor extend those types in some way otherwise a RestClientException will be thrown, not your custom exception.

Minimal Spring Boot example
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import java.io.IOException;
import java.util.Objects;

@SpringBootApplication
public class DemoApplication {

    private static final Logger logger = LoggerFactory.getLogger(DemoApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @EventListener(ApplicationReadyEvent.class)
    public void verifyTodoService(ApplicationReadyEvent event) {
        var todoService = event.getApplicationContext().getBean(TodoService.class);
        try {
            todoService.getProduct(99999999);
        } catch (MyException ex) {
            logger.error("verifyTodoService :: this should happen", ex);
        } catch (RestClientException ex) {
            logger.error("verifyTodoService :: this should not happen");
            Objects.requireNonNull((MyException) ex.getRootCause());
            logger.error("unwrapped custom exception successfully");
        }
    }

    @EventListener(ApplicationReadyEvent.class)
    public void verifyRestClient(ApplicationReadyEvent event) {
        RestClient restClient = RestClient.builder().baseUrl("https://dummyjson.com").build();
        try {
            restClient.get()
                    .uri("/products/{id}", 99999999)
                    .retrieve()
                    .onStatus(HttpStatusCode::isError, (req, resp) -> {
                        throw new MyException();
                    })
                    .body(Product.class);
        } catch (Exception ex) {
            logger.error("verifyRestClient listener :: Caught exception is MyException? {}", MyException.class.isAssignableFrom(ex.getClass()));
        }
    }

    @Bean
    public TodoService todoService(RestClient.Builder restClientBuilder) {
        RestClient restClient = restClientBuilder
                .baseUrl("https://dummyjson.com")
                .defaultStatusHandler(HttpStatusCode::isError, (req, res) -> {
                    throw new MyException();
                })
                .build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

        return factory.createClient(TodoService.class);
    }

    class MyException extends IOException {

    }

    record Product(String id, String title, String description) {
    }


    interface TodoService {

        @GetExchange("/products/{id}")
        Product getProduct(@PathVariable int id) throws MyException;

    }

}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 7, 2023
@poutsma poutsma self-assigned this Dec 8, 2023
@poutsma poutsma added this to the 6.1.2 milestone Dec 11, 2023
@poutsma poutsma added type: documentation A documentation task in: web Issues in web modules (web, webmvc, webflux, websocket) and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: documentation A documentation task
Projects
None yet
Development

No branches or pull requests

3 participants