When implementing request encryption that includes payload data, the conventional approach involves converting the request body to a string format before encryption. Here's a typical pattern:
String payloadString = ModelOptionsUtils.toJsonString(payload);
String hashedRequestPayload = sha256Hex(payloadString);
// Add hashedRequestPayload to headers and include in request
ResponseEntity<String> response = this.restClient.post()
.uri("/")
.headers(headers -> {
headers.addAll(jsonContentHeaders);
})
.body(chatRequest)
.retrieve()
.toEntity(String.class);
This approach has significant drawbacks. The conversion step ModelOptionsUtils.toJsonString(payload) may not produce identical output to what the RestClient actually serializes, leading to encryption mismatches. When inconsistencies occur, debugging becomes extremely difficult since the encrypted value never matches the actual request body being sent.
Using Interceptors for Consistent Encryption
The solution is to perform encryption within an interceptor, ensuring we process the exact payload that gets transmitted.
RestClient provides better flexibility and control compared to traditional HTTP utilities. Its interceptor mechanism allows seamless manipulation of request and response logic. Here's how to integrate an authentication interceptor:
ApiAuthHttpRequestInterceptor apiAuthInterceptor = new ApiAuthHttpRequestInterceptor();
this.restClient = RestClient.builder(baseUrl)
.defaultHeaders(jsonContentHeaders)
.defaultStatusHandler(responseErrorHandler)
.requestInterceptor(apiAuthInterceptor)
.build();
The interceptor implementation acceses the raw request body directly:
public class ApiAuthHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String hashedPayload = sha256Hex(new String(body));
request.getHeaders().putAll(authHeaders);
ClientHttpResponse response = execution.execute(request, body);
return response;
}
}
This approach eliminates the conversion step entirely. Since we receive the raw byte array that will actually be sent over the wire, the encryption is guaranteed to match the transmitted payload.
How It Works
The underlying mechanism iterates through registered interceptors sequentially, executing each in order before the request is dispatched. This recursive chain ensures all interceptors process the request in sequence. After all interceptors complete their processing, the final request proceeds to the server.
By intercepting at the byte level, we bypass any potential serialization inconsistencies between our conversion logic and the framework's internal serialization, resulting in reliable and predictable encryption.