A proxy endpoint that handles both JSON
& x-www-form-urlencoded
A few weeks back, I got a request to expose a ‘proxy’ endpoint. It had to accept an arbitrary JSON payload with only 2 mandatory fields: email
and fromEmail
. This endpoint had to run some logic based on those 2 emails, remove one field (fromEmail
) and pass forward the request to a third-party.
The rest of the JSON fields are not of interest and are only to be forwarded.
Therefore, I decided to write this article specifically because of the “tricks” I had to pull that are presented below. Full code can be found on GitHub.
Usual suspects
Let’s start with the usual suspects, an endpoint, a DTO with constraint validation annotations on the expected fields and the rest of the request body in a map (which is later forwarded). Logic is encapsulated in ReferralService
and we use RestTemplate
to forward the request. Tests are there to validate the behavior so we are good to go.
Now let’s add the same support for x-www-form-urlencoded
requests.
Add x-www-form-urlencoded
support
1. Adding tests
We start by adding a new test class. Spring doesn’t have a RequestMatcher
for x-www-form-urlencoded
so I had to build one (opened a PR).
2. Update controller
Time to update the controller. We can change the existing endpoint to consumex-www-form-urlencoded
along with JSON
but this won’t work because @RequestBody
uses HttpMessageConverter
to convert request payload to POJO (Plain Old Java Object) and does not know how to handle form-encoded parameters (1).
We could change@RequestBody
to @ModelAttribute
to accommodate the form-encoded request conversion but it will break the JSON conversion. In addition, only the explicitly declared fields (email
& fromEmail
) from ReferralRegister
will be populated because@ModelAttribute
needs a 1 to 1 mapping between form data and POJO fields. The easy way out is just to create another handler method for form data.
2.1 Form data
Using @ModelAttribute
on the new endpoint still does not fit our needs because we don’t know all the form fields and, as mentioned earlier, only the explicitly declared fields will be populated (2).
2.2 Request parameters as function arguments
To handle the above issue, we list the expected fields in the parameters list of the handler function. The request still gets validated because we are using @Email
and @NotNull
annotations and we added @Validated
on controller
This approach breaks encapsulation as we need to change in two places (ReferralRegister#sanitized
& ReferralController#formUrlEncodedHandler
) if, for example, we need to translate the fromEmail
to its associated user id and pass it along to the third-party.
3. Custom HttpMessageConverter
Let’s fix this by creating a custom HttpMessageConverter
that will convert form parameters to our POJO.
ReferralConverter
must be declared as a bean for Spring to pick it up, so we add it in Config
inner class from Main
. We also add the converter to the RestTemplate
used for forwarding the request so it uses the same converter for transforming the POJO back to form encoded parameters (see ReferralConverter#writeInternal
).
Now we can get rid of the ReferralController#formUrlEncodedHandler
method and use JSON ReferralController#handler
for both JSON
& x-www-form-urlencoded
requests! Constraint annotations will continue to validate the payload and we now have all the logic in one place. If anything related to read/write the payload needs to change, we only need to update the converter.
Conclusion
I am not sure why but I was living under the impression that Spring had an out of the box mechanism for such use cases. Seems like it is only giving you the tools to do so and you have to handle it yourself.
If you encountered a similar issue or you have a better approach to this one, drop me a message. I’m keen to see how others tackled this.
(1) You can test it by checking out git tag MediaTypeNotSupported and run the tests
(2) You can test it by checking out git tag ExplicitFields and run the tests