A ClientHttpRequestInterceptor can record an outgoing RestTemplate request and its response in one place. Log the HTTP method, URI, status, and elapsed time for routine tracing. Reading and logging a response body needs extra care because the body is normally a stream that the application will read later.
This example uses Spring Boot 4.1.2. A shipping client calls another service for a shipment’s current status. We first log the exchange without exposing its payload, then enable body logging for a bounded diagnostic response.
1. Understand the Interceptor Call
When RestTemplate sends a request, it passes the HttpRequest and serialized request-body bytes to each registered ClientHttpRequestInterceptor. The interceptor calls ClientHttpRequestExecution.execute() to send the request and receive a ClientHttpResponse. The same interceptor can inspect the response before returning it to RestTemplate for message conversion.
The sequence matters: the interceptor must return the response so that RestTemplate can deserialize it into the type requested by the caller. Reading the response body inside the interceptor can consume its stream. We will leave the body untouched in the normal configuration and handle that separate case in Section 4.
2. Log Request and Response Metadata
Our shipping service returns a JSON response such as {"id":"S-42","state":"IN_TRANSIT"} for GET /shipments/S-42. The application needs the parsed status, while operations staff need to know which endpoint responded, whether it succeeded, and how long it took. The following interceptor records those details without logging request bodies, response bodies, or authentication headers.
public final class ExchangeLoggingInterceptor implements ClientHttpRequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(ExchangeLoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
long started = System.nanoTime();
try {
ClientHttpResponse response = execution.execute(request, body);
log.info("HTTP {} {} -> {} in {} ms", request.getMethod(), request.getURI(),
response.getStatusCode().value(),
Duration.ofNanos(System.nanoTime() - started).toMillis());
return response;
} catch (IOException ex) {
log.warn("HTTP {} {} failed after {} ms", request.getMethod(), request.getURI(),
Duration.ofNanos(System.nanoTime() - started).toMillis(), ex);
throw ex;
}
}
}
The timer starts before execute() and stops after the response headers are available. It therefore measures the HTTP exchange up to that point, rather than JSON deserialization or subsequent application work. If the HTTP call throws an IOException, the interceptor logs that failure and rethrows it so the caller can handle the error. An HTTP error response follows RestTemplate‘s normal error handling after the interceptor returns it.
Register the interceptor on the template used by the shipping client. Spring Boot provides a RestTemplateBuilder bean; its additionalInterceptors() method keeps the builder’s other settings while adding this interceptor. The RestTemplateBuilder configuration guide explains how to scope other settings to one client.
@Configuration(proxyBeanMethods = false)
public class ShipmentClientConfig {
@Bean
ShipmentStatusClient shipmentStatusClient(RestTemplateBuilder builder) {
RestTemplate template = builder
.baseUri("https://shipping.example")
.additionalInterceptors(new ExchangeLoggingInterceptor())
.build();
return new ShipmentStatusClient(template);
}
}
public record ShipmentStatus(String id, String state) {}
public final class ShipmentStatusClient {
private final RestTemplate restTemplate;
public ShipmentStatusClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public ShipmentStatus getStatus(String id) {
return restTemplate.getForObject("/shipments/{id}", ShipmentStatus.class, id);
}
}
The client supplies a relative path, and the builder’s baseUri() supplies the shipping service’s origin. A successful call produces a log line like HTTP GET https://shipping.example/shipments/S-42 -> 200 in 68 ms. The caller still receives ShipmentStatus("S-42", "IN_TRANSIT") because the interceptor has not read the response body.
3. Verify the Returned Status
A log line alone cannot prove that the application received the JSON. The repository test starts a local HTTP server that returns the shipment response. It calls ShipmentStatusClient.getStatus() and checks both the mapped record and the metadata log. A changing local port and elapsed time are not part of the assertion.
ShipmentStatus status = new ShipmentStatusClient(template).getStatus("S-42");
assertThat(status).isEqualTo(new ShipmentStatus("S-42", "IN_TRANSIT"));
assertThat(output).contains("HTTP GET http://localhost:",
"/shipments/S-42 -> 200");
assertThat(output).doesNotContain("IN_TRANSIT");
The last assertion confirms that the normal log records the exchange but omits the response payload. This is a useful default for an application that may send credentials or personal data to other services. URI paths and query strings can also contain sensitive values, so avoid logging them unmodified when the API places such data in its URL.
4. Inspect a Response Body When Needed
For a specific diagnostic task, a body-reading interceptor can reveal what the shipping service returned. The response body is normally read once. If the interceptor consumes it, the message converter may see an empty stream when it tries to create ShipmentStatus.
Wrap the request factory in BufferingClientHttpRequestFactory before enabling body inspection. It holds outgoing and incoming content in memory, allowing the interceptor and the message converter to read the response. This configuration uses a simple underlying request factory for clarity; adapt it if your application relies on a different HTTP client.
RestTemplate diagnosticTemplate = new RestTemplateBuilder()
.baseUri("https://shipping.example")
.requestFactory(() -> new BufferingClientHttpRequestFactory(
new SimpleClientHttpRequestFactory()))
.additionalInterceptors(new BodyLoggingInterceptor())
.build();
The interceptor reads the body only when DEBUG logging is enabled. It does not log headers or request bytes, and it should only be used for known non-sensitive responses.
public final class BodyLoggingInterceptor implements ClientHttpRequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(BodyLoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse response = execution.execute(request, body);
if (log.isDebugEnabled()) {
log.debug("Response body: {}",
new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8));
}
return response;
}
}
The second repository test enables DEBUG, logs {"id":"S-42","state":"IN_TRANSIT"}, and still receives the mapped ShipmentStatus. This confirms the buffering behavior for the test’s small response. Buffering retains payload bytes in memory, so do not enable this configuration indiscriminately for large, streaming, or sensitive responses. Log a safe summary or use targeted diagnostics instead.
5. Choose the Logging Level
For a quick look at basic client activity, logging.level.org.springframework.web.client.RestTemplate=DEBUG can show request and response metadata without writing an interceptor. It does not provide a general response-body log. Use a custom interceptor when you need a consistent application-specific exchange record or timing.
| Need | Approach | Main limit |
|---|---|---|
| Basic framework diagnostics | RestTemplate DEBUG logger | Does not show the complete response body |
| Routine exchange record | Metadata interceptor | Review URLs before logging sensitive path or query values |
| Temporary response inspection | Body interceptor with buffering | Extra memory use and payload exposure |
Spring Framework 7 deprecates RestTemplate in favor of RestClient. Existing RestTemplate code can still use this interceptor. For a new synchronous client, see the Spring REST client reference and choose RestClient unless you have a reason to keep the older API.
6. FAQs
6.1. Does an interceptor change the response sent to the caller?
The metadata interceptor does not read or replace the body, so RestTemplate receives the same response for conversion. A body-reading interceptor consumes the stream unless buffering or another repeatable-response mechanism is configured.
6.2. Can I log an Authorization header?
Avoid recording tokens, passwords, cookies, and similar credentials. If you need header diagnostics, select safe header names explicitly and redact sensitive values. The example deliberately logs no headers.
6.3. Does buffering make response logging safe for large downloads?
No. BufferingClientHttpRequestFactory keeps response content in memory. A large response can materially increase memory use, and the log can expose data. Prefer metadata or a bounded, sanitized summary for such calls.
7. Conclusion
Use a ClientHttpRequestInterceptor to record useful HTTP metadata while leaving the response body for RestTemplate to convert. Enable body logging only for bounded diagnostic responses, with buffering and a clear policy for sensitive data. For new synchronous HTTP clients, prefer RestClient.
Happy Learning!
Not able to find clientHttpRequestFactory and mappingJacksonHttpMessageConverter methods. Can you please explain them ?
Code is attached here.
Hoe to log only certain url’s using this approach
Don’t you think StreamUtils.copyToString(response.getBody(), Charset.defaultCharset())) is going to consume the data out of data stream and subsequent response.getBody() will not get the body ?
BufferingClientHttpRequestFactory allows us to read the response body multiple times for a single request.
Does it also intercept GET requests??
Yes, Why not?