Compilation error creating HttpHeaders using Collectors.toMap on the HttpServletRequest's headerNames Enumeration - spring

I'm trying to copy all existing headers from an HttpServletRequest to a Spring HttpHeaders object to use with RestTemplate. This can be done easily in a loop on enumeration, but I'm getting this error while using streams:
HttpHeaders headers = new HttpHeaders();
Enumeration<String> existingHeaders = request.getHeaderNames();
headers.putAll(
Collections.list(existingHeaders)
.stream()
.collect(
Collectors.toMap(Function.identity(),HttpServletRequest::getHeader))
);
I declared the variable Enumeration<String> for the stream to not consider elements as Object but I'm still getting this compilation error at collect():
The method toMap(Function<? super T,? extends K>, Function<? super T,? extends U>)
in the type Collectors is not applicable for the arguments
(Function<Object,Object>, HttpServletRequest::getHeader)

The reason your code does not compile is that its Collectors.toMap(...) call is producing a Map<String, String>, but headers.putAll(...) requires a Map<String, List<String>>. This can be fixed by changing the Collectors.toMap(...) call to produce a compatible map:
HttpHeaders headers = new HttpHeaders();
Enumeration<String> existingHeaders = request.getHeaderNames();
headers.putAll(Collections.list(existingHeaders)
.stream()
.collect(Collectors.toMap(Function.identity(),
name -> Collections.list(request.getHeaders(name)))));
Since the same HTTP header can have multiple values, HttpHeaders implements Map<String,List<String>> rather than Map<String,String>. Therefore, putAll(map) requires a map with List<String> values, not one with String values.

To get the HttpHeaders from an HttpServletRequest you can use the ServletServerHttpRequest class in spring-web. Since the HttpHeaders class is also in spring-web, you should already have this class available on your classpath.
private static HttpHeaders getHttpHeaders(HttpServletRequest request) {
return new ServletServerHttpRequest(request).getHeaders();
}
This answer does not address why you're getting error you're receiving, though it does address the underlying problem you're attempting to solve: getting an HttpHeaders from an existing HttpServletRequest.

Related

How to remove charset from Spring RestTemplate?

When I am posting form request, spring is adding charset like application/x-www-form-urlencoded; charset=UTF-8 which causing problem to consume restful service. How can I remove the charset from RestTemplate to so the content-type is exactly application/x-www-form-urlencoded?
FormHttpMessageConverter makes a lot of validations to make sure you are using a MediaType with a valid charset. I would try either subclassing it and registering a new converter (there are a lot of private methods though), or converting your MultiValueMap to String payload manually (StringHttpMessageConverter is a lot less restrictive about Media Types)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<String> entity = new HttpEntity<>("param1=value1", headers);
String result = restTemplate.postForObject( url, entity, String.class);
I had the same problem. I solved it by removing the charset from the contenttype in the header after it was added.
class MyFormHttpMessageConverter extends FormHttpMessageConverter {
#Override
public void write(final MultiValueMap<String, ?> map, final MediaType contentType, final HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
super.write(map, contentType, outputMessage);
HttpHeaders headers = outputMessage.getHeaders();
MediaType mediaType = headers.getContentType();
if(Objects.nonNull(mediaType) && MapUtils.isNotEmpty(mediaType.getParameters())){
Map<String, String> filteredParams = mediaType.getParameters()
.entrySet()
.stream()
.filter(entry -> !entry.getKey().equals("charset"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
headers.setContentType(new MediaType(mediaType, filteredParams));
}
}
}

Spring Boot MVC to allow any kind of content-type in controller

I have a RestController that multiple partners use to send XML requests. However this is a legacy system that it was passed on to me and the original implementation was done in a very loose way in PHP.
This has allowed to clients, that now they refuse to change, to send different content-types (application/xml, text/xml, application/x-www-form-urlencoded) and it has left me with the need to support many MediaTypes to avoid returning 415 MediaType Not Supported Errors.
I have used the following code in a configuration class to allow many media types.
#Bean
public MarshallingHttpMessageConverter marshallingMessageConverter() {
MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();
converter.setMarshaller(jaxbMarshaller());
converter.setUnmarshaller(jaxbMarshaller());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML,
MediaType.TEXT_XML, MediaType.TEXT_PLAIN, MediaType.APPLICATION_FORM_URLENCODED, MediaType.ALL));
return converter;
}
#Bean
public Jaxb2Marshaller jaxbMarshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(CouponIssuedStatusDTO.class, CouponIssuedFailedDTO.class,
CouponIssuedSuccessDTO.class, RedemptionSuccessResultDTO.class, RedemptionResultHeaderDTO.class,
RedemptionFailResultDTO.class, RedemptionResultBodyDTO.class, RedemptionDTO.class, Param.class,
ChannelDTO.class, RedeemRequest.class);
Map<String, Object> props = new HashMap<>();
props.put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setMarshallerProperties(props);
return marshaller;
}
The controller method is this:
#PostMapping(value = "/request", produces = { "application/xml;charset=UTF-8" }, consumes = MediaType.ALL_VALUE)
public ResponseEntity<RedemptionResultDTO> request(
#RequestHeader(name = "Content-Type", required = false) String contentType,
#RequestBody String redeemRequest) {
return requestCustom(contentType, redeemRequest);
}
This endpoint is hit by all clients. It is only one last client giving me trouble. They are sending content-type = application/x-www-form-urlencoded; charset=65001 (UTF-8)": 65001 (UTF-8)
Due to the way the charset is sent, Spring Boot refuses to return anything but 415. Not even MediaType.ALL seems to have any effect.
Is there a way to make Spring allow this to reach me ignoring the content-type? Creating a filter and changing the content type was not feasible since the HttpServletRequest is not allowing to mutate the content-type. I am out of ideas but I really think there has to be a way to allow custom content-types.
UPDATE
If I remove the #RequestBody then I don't get the error 415 but I have no way to get the request body since the HttpServletRequest reaches the Controller action empty.
You best case is to remove the consumes argument from the RequestMapping constructor. The moment you have it added, spring will try to parse it into known type MediaType.parseMediaType(request.getContentType()) & which tries to create a new MimeType(type, subtype, parameters) and thus throws exception due to invalid charset format being passed.
However, if you remove the consumes, and you wanna validate/restrict the incoming Content-Type to certain type, you can inject HttpServletRequest in your method as parameter, and then check the value of request.getHeader(HttpHeaders.CONTENT_TYPE).
You also have to remove the #RequestBody annotation so Spring doesn't attempt to parse the content-type in attempt to unmarshall the body. If you directly attempt to read the request.getInputStream() or request.getReader() here, you will see null as the stream has already been read by Spring. So to get access to input content, use spring's ContentCachingRequestWrapper inject using Filter and then you can later repeatedly read the content as it's cached & not reading from original stream.
I am including some code snippet here for reference, however to see executable example, you can refer my github repo. Its a spring-boot project with maven, once you launch it, you can send your post request to http://localhost:3007/badmedia & it will reflect you back in response request content-type & body. Hope this helps.
#RestController
public class BadMediaController {
#PostMapping("/badmedia")
#ResponseBody
public Object reflect(HttpServletRequest request) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.createObjectNode();
((ObjectNode) rootNode).put("contentType", request.getHeader(HttpHeaders.CONTENT_TYPE));
String body = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
body = URLDecoder.decode(body, StandardCharsets.UTF_8.name());
((ObjectNode) rootNode).put("body", body);
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
}
}
#Component
public class CacheRequestFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest cachedRequest
= new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
//invoke caching
cachedRequest.getParameterMap();
chain.doFilter(cachedRequest, servletResponse);
}
}

RestTemplate gives 400 Bad Request Error on a Get Request

When I try to make a get request with Spring's RestTemplate, it gives 400 BAD Request. I can call the same url from javascript successfully with the headers below :
But the code below does not work. What might be the cause?
public Entity getEntityByUri(String uri) {
String req = "http://live.dbpedia.org/sparql?query=DESCRIBE%20%3Chttp://dbpedia.org/resource/Concept_learning%3E&format=application%2Fjson-ld";
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.ALL));
HttpEntity<String> httpEntity = new HttpEntity<String>(headers);
new RestTemplate().exchange(req, HttpMethod.GET, httpEntity, Map.class);
Entity entity = new Entity();
return entity;
}
Your url is already encoded. Popular browsers such as Chrome are capable of understanding and responding appropriately. However, it's not the same case with RestTemplate.
I had to decode your uri here and the decoded uri is DESCRIBE <http://dbpedia.org/resource/Concept_learning>
Having checked the browser console, I got to know you have two query strings passed in the url, they are query and format holding values DESCRIBE <http://dbpedia.org/resource/Concept_learning> and application/json-ld respectively.
I assume Entity class is the pojo class of json response.
Have created Entity as from your json response:
public class Entity {
private String value;
private String type;
// getters and setters omitted for brevity
}
Finally in your getEntityByUri method have got the instance of UriComponentsBuilder which handles uri encoding and query params.
To sum up, your getEntityByUri looks below.
public HttpEntity<Entity> getEntityByUri() {
String req = "http://live.dbpedia.org/sparql";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(req)
.queryParam("query",
"DESCRIBE <http://dbpedia.org/resource/Concept_learning>")
.queryParam("format", "application/json-ld");
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.ALL));
HttpEntity<String> httpEntity = new HttpEntity<String>(headers);
return new RestTemplate().exchange(builder.build().encode().toUri(), HttpMethod.GET, httpEntity, Entity.class);
}
The above method didn't throw HTTP400 as the required query params have been passed in builder object.
Hope this helps and good luck!
Anyone getting same error make sure your URL is decoded means no percent symbols in url (if space in param values).
This worked for me
try {
requestURL = URLDecoder.decode("http://api.com?p=1&groups=3212&affected-since=2019-06-06T14%3A11%3A14.880&detail=full&after-id=43536", "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Maybe
headers.setAccept(Arrays.asList(MediaType.ALL));
generates a malformed "Accept" header field? (FWIW, why do you send it at all???)

How to send Multipart form data and upload PDF with RestTemplate Spring Boot

Good day Pals,
In my microservice and spring-boot app, I have a frontend employee microservice which consumes another microservice with file upload endpoint.
The calling service is based on spring rest controller and I am trying to consume a File-Upload endpoint using RestTemplate in a Spring Boot application. In a nutshell, trying to upload a PDF file.
I have explored the following SO post, but its not working for me:
jackson disable fail_on_empty_beans
I am testing this in postman and getting the following error:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer.
Any help will be appreciated pleasee ....
Below are the main components -
Rest Controller ############
#RestController 
#RequestMapping(path = “/employee”) 
public class EmployeeController {
private EmployeeService empService;
#RequestMapping(value =“/emp/load”, method = RequestMethod.POST)
public
#ResponseBody
ResponseEntity<byte[]> handleFileUpload(
 #RequestParam("file") MultipartFile file, #RequestParam String a, #RequestParam String b, #RequestParam String c, #RequestParam String d, #RequestParam String e) throws Exception {
return empService.handleFileUpload(file, a, b, c, d, e);
}
}
The service Implementation
#Service
public class EmployeeServiceImpl implements EmployeeService{
 
 #Value(“${emp.base.url}")
private String EMP_BASE_URI;
public ResponseEntity<byte[]>handleFileUpload(MultipartFile file, String a, String b, String c, String d, String e) {
final String uri = EMP_BASE_URI + "/upload";
RestTemplate restTemplate = getMappedRestTemplate();
MultiValueMap<String, Object> params = new LinkedMultiValueMap<String, Object>();
params.add("file", file);
params.add(“a”, a);

params.add(“b”, b);
params.add(“c”, c);

params.add(“d”, d);
params.add(“e”, e);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "multipart/form-data");
ResponseEntity<byte[]> response = restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(params, headers), byte[].class);
return new ResponseEntity<>(response.getBody(), response.getStatusCode());
} 
 
 
 
private RestTemplate getMappedRestTemplate(){
RestTemplate restTemplate = new RestTemplate();
ObjectMapper newObjectMapper = new ObjectMapper();

 newObjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);

newObjectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter=new MappingJackson2HttpMessageConverter();
FormHttpMessageConverter formConvertor = new FormHttpMessageConverter();

restTemplate.getMessageConverters().add(formConvertor);
restTemplate.getMessageConverters().add(mappingJacksonHttpMessageConverter);
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());

return restTemplate;
}
}
I am getting the following error:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"])
Please, any help with be appreciated.
I have been stuck on this all day.
I have gone for sending the params (including the pdf file as a byte stream i.e. byte[]) as json in the request body using the following method signature:
#RequestMapping(value = "/upload", method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
public #ResponseBody Long handleFileUpload(#Valid #RequestBody Invoice uploadedInvoice){
...
}

HTTP get with headers using RestTemplate

How can I send a GET request using the Spring RestTemplate?
Other questions have used POST, but I need to use GET.
When I run this, the program continues to work, but it seems that the network is clogged because this is in an AsyncTask, and when I try to run another asynctask after I click on the button for this one, they won't work.
I tried doing
String url = "https://api.blah.com/2.0/search/cubes?w=jdfkl&whitespace=1";
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("Bearer", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); //copied this from somewhere else, not sure what its for
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
HttpMessageConverter<String> stringConverter = new StringHttpMessageConverter();
FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
List<HttpMessageConverter<?>> msgConverters = new ArrayList<HttpMessageConverter<?>>();
msgConverters.add(formConverter);
msgConverters.add(new MappingJacksonHttpMessageConverter());
msgConverters.add(stringConverter);
template.setMessageConverters(msgConverters);
//SetSearchResponseData is my custom class to store the incoming JSON
ResponseEntity<SetSearchResponseData> result = template.exchange(url, HttpMethod.GET, request, SetSearchResponseData.class);
//If I was using post, i could have done SetSearchResponseDataresponse = restTemplate.postForObject(url, request, SetSearchResponseData.class);
The RestTemplate getForObject() method does not support setting headers. The solution is to use the exchange() method.
So instead of restTemplate.getForObject(url, String.class, param) (which has no headers), use
HttpHeaders headers = new HttpHeaders();
headers.set("Header", "value");
headers.set("Other-Header", "othervalue");
...
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, String.class, param);
Finally, use response.getBody() to get your result.
This question is similar to this question.
Take a look at the JavaDoc for RestTemplate.
There is the corresponding getForObject methods that are the HTTP GET equivalents of postForObject, but they doesn't appear to fulfil your requirements of "GET with headers", as there is no way to specify headers on any of the calls.
Looking at the JavaDoc, no method that is HTTP GET specific allows you to also provide header information. There are alternatives though, one of which you have found and are using. The exchange methods allow you to provide an HttpEntity object representing the details of the request (including headers). The execute methods allow you to specify a RequestCallback from which you can add the headers upon its invocation.
The getForObject() method of RestTemplate does not support setting headers. you can use this
syntax:
restTemplate.exchange(url endpoint,
HttpMethod.GET,entity, params)
public List<Employee> getListofEmployee()
{
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<List<Employee>> response = restTemplate.exchange("http://hello-server/rest/employees",
HttpMethod.GET,entity, new ParameterizedTypeReference<List<Employee>>() {});
return response.getBody(); //this returns List of Employee
}

Resources