Read Request body multiple times
Let's say you were doing an integration with a third-party system who needed to send data to your system via a webhook, and as part of the webhook verification, you needed to calculate a Hash based on a shared secret and compare this to a hash which the third party system would send to you in a custom header.
The standard way to do this is to implement a Filter in Spring and register it to intercept requests to the request path, and there do the webhook verification.
To illustrate this, let's create a new Project.
Head over to Spring Initializr to generate a new Project, add the dependencies for spring-web, validation, spring security
Let's add the PaymentPayload
So far, our application startups fine
But we're also hit with a 401 when we hit the /webhook endpoint. This is because we added Spring security to the Project. Let's add a SecurityConfig and permit requests to this endpoint for now, so that we can reach the controller
We also disable CSRF so we can proceed, This is not recommended in production.
We still anyway get a 403 😣. But then we see this log from Spring when we don't pass a request body.
When we do pass a request body, we get a warning from Spring from the same DefaultHandlerExceptionResolver for the MethodArgumentNotValidException. What's interesting is that in Spring 2.x, this would not return a 403. I also came across this stackoverflow question. But anyway, let's fix it.
Returning the right Response for Input Validation§
We add a ControllerAdvice, we don't even have to handle any exceptions, but we see that we already get a 400, when the request doesn't pass validation, this is exactly what we expect. Problem solved.
Expectations of the Filter§
We extend OncePerRequestFilter from Spring and state what the filter should do. Normally, with co-pilot or one of these tools, this should be enough to generate the snippet I expect. 😹. But let's write it.
The Filter§
We implement the filter like so:
Registering the Filter§
In order for the filter to be called when we hit the route, we have to register it.
Probably a good time to check that our app still works as expected. Well, looks like it does. We get the "Missing Security header" message and a 401 when we don't supply the header, this comes from our Filter. So all good so far. 👍🏽
Testing §
We'll do the following:
- Prepare the JSON request body
- Generate a SHA-512 hash of the request body in step 1.
- Base64 encode the result of step 2
- Pass result of step 3, the base64 encoded string in the x-webhook-hmac header of the request.
The JSON body
We make a POST request to our endpoint, passing the encoded hash in the header and get a 400, with a message "failed to read request".
Spring sees an empty Request body§
We get a 400 because Spring sees an empty request body at the point when we get to the Controller.
The documentation for ContentCachingRequestWrapper mention that cached content is to be read using getContentAsByteArray() after we have read the request body using .getInputStream() or .getReader(). What this means is that after we have read the request body in our filter, (which caches it), If we want to access the cached content, we have to use getContentAsByteArray().
Accessing cached Request body in Controller§
We change our Controller code to the following in order to access the cached request body. I find this to be verbose. We also lose the ability to validate the request body automatically. 😕 I expected more from this implementation.
Well, there's something we can do. We can actually write our own ContentCachingWrapper
CachedBodyHttpServletRequest§
We write a Custom HttpServletRequestWrapper, which will cache the request body and whenever getInputStream() or getReader() is called, the cached request body will be returned.
Our filter's doInternalFilter method becomes
We can then access the request body in our Controller using @RequestBody
You can access the full code here
Member discussion