I have a requirement to Base64
decode every JSON request payload that my Spring Boot service receives. The JSON payload would have been Base64
encoded at the client before posting using the HTTP POST
method. Further, I also need to Base64
encode the JSON response before presenting to the calling client application.
I am required to reduce boilerplate code by using handler interceptors.
I have already achieved the request/incoming leg of the operation by the use of interceptors but is yet to achieve this for the response leg.
I have posted the code snippets below. The code to intercept the response and base64 encode it is in the postHandle method of the interceptor class.
What am I doing wrong here?
Interceptor Class:
public class Base64ResponseEncodingInterceptor implements HandlerInterceptor {
private static final String DECODED_REQUEST_ATTRIB = "decodedRequest";
private static final String POST = "POST";
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView arg3) throws Exception {
try {
if (POST.equalsIgnoreCase(request.getMethod())) {
CharResponseWrapper res = new CharResponseWrapper(response);
res.getWriter();
byte[] encoded = Base64.encodeBase64(res.toString().getBytes());
byte[] encoded = Base64.encodeBase64(response.getHeader(ENCODED_RESPONSE_ATTRIB).getBytes());
response.getWriter().write(new String(encoded));
}
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
// preHandle and afterCompletion methods
// Omitted
}
The CharResponseWrapper
Class used above:
public class CharResponseWrapper extends HttpServletResponseWrapper {
protected CharArrayWriter charWriter;
protected PrintWriter writer;
protected boolean getOutputStreamCalled;
protected boolean getWriterCalled;
public CharResponseWrapper(HttpServletResponse response) {
super(response);
charWriter = new CharArrayWriter();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (getWriterCalled) {
throw new IllegalStateException("getWriter already called");
}
getOutputStreamCalled = true;
return super.getOutputStream();
}
@Override
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return writer;
}
if (getOutputStreamCalled) {
throw new IllegalStateException("getOutputStream already called");
}
getWriterCalled = true;
writer = new PrintWriter(charWriter);
return writer;
}
@Override
public String toString() {
String s = null;
if (writer != null) {
s = charWriter.toString();
}
return s;
}
}
JavaConfig Class where Interceptor is registered:
@Configuration
@EnableJpaRepositories(repositoryBaseClass = BaseRepositoryBean.class, basePackages = "")
@EntityScan(basePackages = { "com.companyname", "com.companyname.productname"})
public class RestConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Base64ResponseEncodingInterceptor());
}
}
The Controller Class, where the Interceptor is used (Only the working request leg is shown here):
@Autowired
HttpServletRequest request;
String decodedRequest = null;
@ModelAttribute("decodedRequest")
public void getDecodedParam(){
decodedRequest = (String) request.getAttribute("decodedRequest");
}
The code in the postHandle
method does not work. It is either the HttpServletResponse
is null
or I get an exception message:
getOutputStream already called
Update: Work around solution to reading the response directly in the ResponseBodyAdvice
In the Controller Class, I added the following:
@RestController
@RequestMapping("/api/ipmanager")
public class IPProfileRestController extends AbstractRestController {
@Autowired
HttpServletResponse response;
String encodedResponse = null;
@ModelAttribute("encodedResponse")
public void getEncodedResponse(){
response.setHeader("encodedResponse", StringUtils.EMPTY);
}
@RequestMapping(value = "/time", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.APPLICATION_JSON_VALUE })
public @ResponseBody String saveAccessClientTime(@RequestBody String ecodedRequest) {
// Some code here
String controllerResponse = prettyJson(iPProfileResponse);
response.setHeader("encodedResponse", controllerResponse);
return controllerResponse;
}
}
I have the following in the ResponseBodyAdvice
@ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice
Answer
As the Spring MVC documentation states:
the
postHandle
method ofHandlerInterceptor
is not always ideally
suited for use with@ResponseBody
andResponseEntity
methods. In such
cases anHttpMessageConverter
writes to and commits the response
beforepostHandle
is called which makes it impossible to change the
response, for example to add a header. Instead an application can
implementResponseBodyAdvice
and either declare it as an
@ControllerAdvice
bean or configure it directly on
RequestMappingHandlerAdapter
.
With that being said:
What am I doing wrong here?
Since the response has been already committed, you can't change it. In order to change the response you should register a ResponseBodyAdvice
and put your response encoding logic there:
@ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice
No comments:
Post a Comment