Home Java Master Spring RestTemplate Logging with Interceptors
Intermediate 3 min · July 14, 2026

Master Spring RestTemplate Logging with Interceptors

Learn to log Spring RestTemplate requests and responses using interceptors.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and RestTemplate
  • Understanding of HTTP methods and status codes
  • Familiarity with SLF4J logging
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use ClientHttpRequestInterceptor to log requests and responses.
  • Override intercept() method to capture HTTP method, URI, headers, and body.
  • Wrap response body with BufferingClientHttpResponseWrapper to read it multiple times.
  • Avoid logging sensitive data like passwords or tokens.
  • Test interceptors with MockRestServiceServer or WireMock.
✦ Definition~90s read
What is Spring RestTemplate Request and Response Logging with Interceptors?

RestTemplate logging with interceptors is a technique to automatically log HTTP request and response details by implementing ClientHttpRequestInterceptor and registering it with the RestTemplate bean.

Imagine you're sending a letter via courier.
Plain-English First

Imagine you're sending a letter via courier. An interceptor is like a camera that takes a picture of the letter before it leaves and after it arrives, so you can see exactly what was sent and received. For a RestTemplate, interceptors log the HTTP request and response details for debugging or auditing.

Logging HTTP requests and responses is one of those things that seems trivial until you're debugging a production outage at 2 AM. I've been there — a fintech startup running Spring Boot 2.7 on EKS, where a third-party payment gateway started returning 400 errors, but our logs only showed "Bad Request". No request body, no headers, no response body. We were flying blind.

Spring's RestTemplate doesn't log request/response details by default. The RestTemplate class itself has no built-in logging mechanism for the actual HTTP payload. Sure, you can enable DEBUG logging for org.springframework.web.client, but that only logs URI and status code — not headers or body. That's like knowing a package was delivered but not what's inside.

Enter interceptors. By implementing ClientHttpRequestInterceptor, you can hook into the request/response lifecycle and log everything: method, URI, headers, and body. You can even wrap the response body to read it multiple times, which is crucial because by default the body stream is consumed once.

In this article, I'll show you how to build a robust logging interceptor, what the official docs don't tell you, and how to avoid the pitfalls I've seen teams fall into. By the end, you'll have a production-ready logging solution that will save you hours of debugging.

Why You Need a Custom Interceptor for Logging

Let's get one thing straight: RestTemplate's built-in logging is useless for debugging real-world issues. By default, it only logs the request URI and response status at DEBUG level. If you need to see headers or body, you're out of luck.

I've seen teams try to work around this by adding log statements before and after every restTemplate.exchange() call. That's a maintenance nightmare. You'll have dozens of duplicated log statements, and someone will inevitably forget to log the error response.

The correct approach is to use a ClientHttpRequestInterceptor. This interface lets you intercept the HTTP request before it's sent and the response after it's received. You can log everything in one place, and it automatically applies to all calls made through that RestTemplate instance.

Here's the hard truth: most teams don't discover this until they're in a production incident. Don't be that team. Add an interceptor from day one.

LoggingInterceptor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;

public class LoggingInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        logResponse(response);
        return response;
    }

    private void logRequest(HttpRequest request, byte[] body) {
        log.info("Request URI: {}", request.getURI());
        log.info("Request Method: {}", request.getMethod());
        log.info("Request Headers: {}", request.getHeaders());
        log.info("Request Body: {}", new String(body, StandardCharsets.UTF_8));
    }

    private void logResponse(ClientHttpResponse response) throws IOException {
        log.info("Response Status Code: {}", response.getStatusCode());
        log.info("Response Headers: {}", response.getHeaders());
        // WARNING: response.getBody() can be read only once!
        log.info("Response Body: {}", new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8));
    }
}
⚠ Critical: Response Body Stream Consumed Once
📊 Production Insight
I once debugged a case where the interceptor was logging the response body, but the application was getting null responses. The interceptor consumed the stream. Always wrap the response.
🎯 Key Takeaway
Use a custom ClientHttpRequestInterceptor to centralize logging of all RestTemplate calls. Avoid scattered log statements.

Building a Robust Logging Interceptor

Now let's build a production-ready interceptor that handles the stream consumption issue and adds sensible defaults. We'll use BufferingClientHttpResponseWrapper from Spring's AbstractClientHttpResponse or we can implement our own wrapper.

Spring actually provides org.springframework.http.client.BufferingClientHttpResponseWrapper but it's package-private. Instead, we can use org.springframework.web.util.ContentCachingResponseWrapper from Spring Web, but that's for servlet responses. For RestTemplate, the cleanest approach is to read the body into a byte array and then return a wrapper that serves from that array.

LoggingInterceptor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class LoggingInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        ClientHttpResponse wrappedResponse = new BufferingClientHttpResponseWrapper(response);
        logResponse(wrappedResponse);
        return wrappedResponse;
    }

    private void logRequest(HttpRequest request, byte[] body) {
        log.info("=== Request ===");
        log.info("URI: {}", request.getURI());
        log.info("Method: {}", request.getMethod());
        log.info("Headers: {}", request.getHeaders());
        if (body.length > 0) {
            log.info("Body: {}", new String(body, StandardCharsets.UTF_8));
        }
    }

    private void logResponse(ClientHttpResponse response) throws IOException {
        log.info("=== Response ===");
        log.info("Status: {}", response.getStatusCode());
        log.info("Headers: {}", response.getHeaders());
        byte[] bodyBytes = StreamUtils.copyToByteArray(response.getBody());
        if (bodyBytes.length > 0) {
            log.info("Body: {}", new String(bodyBytes, StandardCharsets.UTF_8));
        }
    }

    private static class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

        private final ClientHttpResponse delegate;
        private final byte[] body;

        public BufferingClientHttpResponseWrapper(ClientHttpResponse delegate) throws IOException {
            this.delegate = delegate;
            this.body = StreamUtils.copyToByteArray(delegate.getBody());
        }

        @Override
        public InputStream getBody() throws IOException {
            return new ByteArrayInputStream(body);
        }

        @Override
        public HttpHeaders getHeaders() {
            return delegate.getHeaders();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return delegate.getRawStatusCode();
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return delegate.getStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return delegate.getStatusText();
        }

        @Override
        public void close() {
            delegate.close();
        }
    }
}
🔥Better Approach: Use Commons IO or Spring's StreamUtils
📊 Production Insight
Spring 5.3+ has org.springframework.http.client.BufferingClientHttpResponseWrapper but it's package-private. You can use reflection, but it's fragile. The custom wrapper above is simple and works across versions.
🎯 Key Takeaway
Always wrap the response to buffer the body. Use a custom wrapper class to avoid consuming the stream prematurely.

What the Official Docs Won't Tell You

The official Spring documentation for ClientHttpRequestInterceptor is sparse. It tells you the interface exists and shows a trivial example that logs the URI. It doesn't warn you about the body stream consumption issue. It doesn't tell you that your interceptor might not be invoked if you're using AsyncRestTemplate (deprecated) or if you're using Spring Cloud Feign (which uses a different mechanism).

  1. Body stream is consumed once – I already beat this drum, but it's the number one issue. If you read the body in the interceptor, the application gets nothing. Always buffer.
  2. Interceptors are not inherited – If you create a new RestTemplate instance with new RestTemplate(), it doesn't have any interceptors. You must add them to every instance or use a single bean.
  3. Order matters – If you have multiple interceptors, the order they are added determines the execution order. The first interceptor added is the outermost. If you have an authentication interceptor that adds headers, your logging interceptor should be after it to see the final headers.
  4. Large payloads can crash your app – Logging a 10MB response body will fill your logs and memory. Always truncate or conditionally log based on size.
  5. Sensitive data masking – You must mask fields like password, token, secret in both request and response bodies. Use a regex or JSON path to redact them.
  6. Testing interceptors – You can't easily test an interceptor without an actual HTTP call. Use MockRestServiceServer from Spring Test to mock the server and verify the interceptor logs correctly.
LoggingInterceptor.java (truncation)JAVA
1
2
3
4
5
6
7
8
9
10
11
private static final int MAX_BODY_LOG_LENGTH = 1000;

private void logBody(byte[] body, String prefix) {
    if (body == null || body.length == 0) return;
    String bodyStr = new String(body, StandardCharsets.UTF_8);
    if (bodyStr.length() > MAX_BODY_LOG_LENGTH) {
        log.info("{} Body (truncated): {}...", prefix, bodyStr.substring(0, MAX_BODY_LOG_LENGTH));
    } else {
        log.info("{} Body: {}", prefix, bodyStr);
    }
}
⚠ Never Log Sensitive Data
📊 Production Insight
At a healthcare startup, we logged full patient data in debug logs. A junior dev pushed it to production. The logs were shipped to an external SIEM. We had a data breach. Don't be that team.
🎯 Key Takeaway
The official docs omit critical details about body consumption, interceptor ordering, and large payload handling. Always buffer, truncate, and mask.

Registering the Interceptor with RestTemplate

The builder approach is cleaner because it also lets you set timeouts, error handlers, and other settings in one place. Plus, RestTemplateBuilder is auto-configured by Spring Boot, so it inherits any global settings like MessageConverters.

If you have multiple RestTemplate beans (e.g., one for payment service, one for email service), you can add the interceptor selectively or use a composite interceptor.

RestTemplateConfig.java (builder)JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .interceptors(new LoggingInterceptor())
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .build();
    }
}
📊 Production Insight
I've seen teams use @Autowired on a RestTemplate field without realizing it's a different instance. Always define a @Bean and inject that.
🎯 Key Takeaway
Use RestTemplateBuilder to create RestTemplate beans and add interceptors. Avoid new RestTemplate() in production code.

Testing Your Logging Interceptor

You can also use WireMock for integration tests. WireMock runs an actual HTTP server, which is closer to production. But for unit testing the interceptor behavior, MockRestServiceServer is sufficient.

What about verifying the log output? You can use a @SpyBean on the logger or capture log output with OutputCapture from Spring Boot test. Here's a simple way:

LoggingInterceptorTest.java (log capture)JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.junit.jupiter.api.extension.ExtendWith;

@SpringBootTest
@ExtendWith(OutputCaptureExtension.class)
class LoggingInterceptorLogTest {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    void testLogOutput(CapturedOutput output) {
        MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
        mockServer.expect(requestTo("/api/test"))
                .andRespond(withSuccess("response body", MediaType.TEXT_PLAIN));

        restTemplate.getForObject("/api/test", String.class);

        assertThat(output).contains("Request URI: http://localhost/api/test");
        assertThat(output).contains("Response Body: response body");
    }
}
📊 Production Insight
Don't rely on manual testing. I've seen interceptors that work in dev but fail in production because of classpath issues or different log levels. Automate tests.
🎯 Key Takeaway
Test your interceptor with MockRestServiceServer and capture log output to verify logging behavior.

Advanced: Conditional Logging and Masking

This interceptor logs only when the response is an error (4xx or 5xx) by default, but you can configure it to log all requests. It also masks sensitive headers and body fields.

application.ymlYAML
1
2
3
4
5
6
logging:
  interceptor:
    log-only-errors: true
    max-body-length: 1000
    sensitive-headers: authorization, x-api-key
    sensitive-body-fields: password, token, secret
📊 Production Insight
At a fintech, we logged all requests including credit card numbers in the body. A simple regex mask saved us from PCI compliance violations.
🎯 Key Takeaway
Make your interceptor configurable: conditionally log, mask sensitive data, and truncate large bodies.
● Production incidentPOST-MORTEMseverity: high

The Silent 400: When Logs Lie

Symptom
Users reported failed payments, but server logs only showed "400 Bad Request" with no additional details. The third-party API returned an error message in the response body, but it wasn't logged.
Assumption
The developer assumed the third-party API was down or returning generic errors.
Root cause
RestTemplate by default does not log request/response bodies. The response body stream was consumed by the application, and the logging framework only captured the status line.
Fix
Implemented a custom ClientHttpRequestInterceptor that logs the full request and response, including body, using BufferingClientHttpResponseWrapper to allow multiple reads of the response body.
Key lesson
  • Always log full request and response payloads for external calls.
  • Use interceptors to centralize logging, not scattered log statements.
  • Be careful with body streaming — wrap responses to avoid premature consumption.
  • Mask sensitive data (passwords, tokens) in logs.
  • Test your logging interceptor with actual HTTP calls using WireMock.
Production debug guideSymptom to Action4 entries
Symptom · 01
Logs show only URI and status code, no body
Fix
Implement a ClientHttpRequestInterceptor that logs request/response bodies. Use BufferingClientHttpResponseWrapper to read the response body multiple times.
Symptom · 02
Response body appears empty in logs but application reads it fine
Fix
The response body stream is consumed once. Use BufferingClientHttpResponseWrapper to cache the body in memory.
Symptom · 03
Sensitive data (passwords, tokens) visible in logs
Fix
Add a masking step in the interceptor to redact sensitive headers and body fields before logging.
Symptom · 04
Interceptor not invoked for certain requests
Fix
Ensure the interceptor is added to the RestTemplate instance used for those requests. Check if you're using a different RestTemplate bean.
★ Quick Debug Cheat SheetInstant actions for common RestTemplate logging issues
No request/response body in logs
Immediate action
Add a custom interceptor with BufferingClientHttpResponseWrapper
Commands
restTemplate.getInterceptors().add(new LoggingInterceptor());
Enable DEBUG logging for your interceptor class
Fix now
Implement LoggingInterceptor that logs body using IOUtils.toString(responseBody)
Sensitive data leaked in logs+
Immediate action
Add header/body masking in interceptor
Commands
Replace headers like 'Authorization' with '***'
Use regex to mask JSON fields like 'password'
Fix now
Add a method to sanitize log output before writing
Interceptor not triggering+
Immediate action
Verify RestTemplate instance and interceptor list
Commands
System.out.println(restTemplate.getInterceptors().size());
Check if RestTemplate is autowired vs new instance
Fix now
Ensure the same RestTemplate bean is used everywhere
FeatureRestTemplate InterceptorWebClient ExchangeFilter
Logging capabilityFull request/responseFull request/response
Body bufferingManual wrapper requiredBuilt-in buffering
Reactive supportNoYes
API complexitySimpleModerate
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
LoggingInterceptor.javapublic class LoggingInterceptor implements ClientHttpRequestInterceptor {Why You Need a Custom Interceptor for Logging
LoggingInterceptor.java (truncation)private static final int MAX_BODY_LOG_LENGTH = 1000;What the Official Docs Won't Tell You
RestTemplateConfig.java (builder)@ConfigurationRegistering the Interceptor with RestTemplate
LoggingInterceptorTest.java (log capture)@SpringBootTestTesting Your Logging Interceptor
application.ymllogging:Advanced

Key takeaways

1
Use ClientHttpRequestInterceptor to centralize logging of all RestTemplate calls.
2
Always buffer the response body to avoid stream consumption issues.
3
Truncate and mask sensitive data in logs to protect privacy and reduce log volume.
4
Test your interceptor with MockRestServiceServer to ensure it works correctly.
5
Register the interceptor via RestTemplateBuilder for clean configuration.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you log all HTTP requests and responses made by RestTemplate i...
Q02SENIOR
What is the problem with reading the response body in a RestTemplate int...
Q03SENIOR
How would you ensure sensitive data like passwords are not logged in you...
Q01 of 03SENIOR

How would you log all HTTP requests and responses made by RestTemplate in a Spring Boot application?

ANSWER
Implement ClientHttpRequestInterceptor and override intercept(). Log the request URI, method, headers, and body. For the response, use a buffered wrapper to read the body multiple times. Register the interceptor with the RestTemplate bean.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I use Spring's `CommonsClientHttpRequestInterceptor` for logging?
02
How do I log only errors with the interceptor?
03
What if I'm using `WebClient` instead of `RestTemplate`?
04
How do I test the interceptor without making real HTTP calls?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

3 min read · try the examples if you haven't

Previous
Basic Authentication with Spring RestTemplate: Interceptors and Headers
114 / 121 · Spring Boot
Next
Accessing HTTPS REST Services Using Spring RestTemplate with SSL