Custom error handler set using RestTemplateBuilder bean or using the configuration TestRestTemplateConfiguration from Spring Boot Reference Guide is overriden by TestRestTemplate.NoOpResponseErrorHandler.

Specifically, when using the following class:

class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
    // implementation
}

in the configuration of the builder:

ResponseErrorHandler customErrorHandler = new CustomResponseErrorHandler();
RestTemplateBuilder builder = builderProvider.getIfAvailable()
                                             .errorHandler(customErrorHandler);

The following line is false:

new TestRestTemplate(builder.build()).getRestTemplate().getErrorHandler().equals(customErrorHandler)

Comment From: wilkinsona

Closing in favour of the PR (#7266).

Comment From: dallasvaughan

Just a note about this: I discovered this issue when I called the method TestRestTemplate.withBasicAuth(String username, String password).

Inside that method is the call:

restTemplate.setErrorHandler(getRestTemplate().getErrorHandler());

...which sets the new RestTemplate's ErrorHandler with the current one. But then later in the method the TestRestTemplate constructor is called which then (re-)sets the just-set ResponseErrorHandler back to NoOpResponseErrorHandler.

This logic, along with the description in the javadoc for this method (Creates a new {@code TestRestTemplate} with the same configuration as this one), implies that the RestTemplate config should be the same as it was coming in, but it sets it to NoOpResponseErrorHandler regardless of the previous state.

Comment From: philwebb

Thanks @dallasvaughan, I've raised https://github.com/spring-projects/spring-boot/issues/7441 to fix this.

Comment From: bclozel

We've discussed this during a team call and we've come to the conclusion that the NoOpResponseErrorHandler is part of the core contract behind TestRestTemplate.

TestRestTemplate is about testing server behavior, looking at the HTTP responses coming out of the server no matter what the response status is. So developers should always test the responses with assertions and not test for thrown exceptions.

We understand that developers are tempted to use TestRestTemplate as a shortcut to get a RestTemplate already bound to the test server, but we don't think this use case is really the goal here. Instead, developers should get injected with a RestTemplateBuilder and get the @LocalServerPort information to build a client instance if they wish to test the behavior of RestTemplate against the application.

Comment From: tkruse

@bclozel : so how to make assertions on the result of put(), given that TestRestTemplate.put() just returns void? As an example, how to distinguish a 200 from a 404 error response in tests with that API?

Comment From: bclozel

@tkruse you can use the exchange method and specify the PUT method.

Comment From: cs94njw

TestRestTemplate doesn't actually offer what @bclozel describes. I am writing my unit tests, using TestRestTemplate with a ResponseType of "UUID". When I test BAD_REQUEST errors from my service, instead of my being able to inspect the ResponseEntity and check for status "401", TestRestTemplate throws an exception saying it can't decode the error message as a UUID. I don't understand why TestRestTemplate is even attempting to decode the ResponseType when a 4xx error is returned.

Comment From: bclozel

@cs94njw I don't understand how this is related. This looks like a different problem? Could you report that separately with a sample project (something we can git clone or download) we can take a look at?

Comment From: steinarb

If you return something other than whatever the TestRestTemplate tries to parse into (a UUID in the comment above, and a JSON object in my case), for instance a text/plain error message on a 4xx response, then what you get, is a stack trace from trying to parse the error instead of a meaningful stack trace containing the unparsable error message.

Googling for a fix got me here.

It would be OK for me if the test fails in this case, but I would have liked to see the entire error message instead of just the jackson parsing error

Here is my stack trace (the "Unable" jackson is unable to parse (heh!), is the first word in the text/plain body of a 4xx response. I would have liked to see what comes after "Unable" (in this case to see what I'm missing in the setup of the test to make the test go green)):

org.springframework.web.client.RestClientException: Error while extracting response for type [class com.mycompany.myapp.dto.frontend.application.CaseDto] and content type [application/json]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized token 'Unable': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'Unable': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (PushbackInputStream); line: 1, column: 8]

    at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:120)
    at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1037)
    at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1020)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:778)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:631)
    at org.springframework.boot.test.web.client.TestRestTemplate.exchange(TestRestTemplate.java:761)
    at com.mycompany.myapp.CaseHandlingComponentTest.caseTest(CaseHandlingComponentTest.kt:368)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized token 'Unable': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'Unable': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (PushbackInputStream); line: 1, column: 8]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:389)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:342)
    at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:105)
    ... 73 more
Caused by: com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'Unable': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (PushbackInputStream); line: 1, column: 8]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2337)
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:720)
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._reportInvalidToken(UTF8StreamJsonParser.java:3593)
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2688)
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:870)
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:762)
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4684)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4586)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3601)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:378)
    ... 75 more