Exposing additional headers in requests to Spring MVC so that they can be used in Angular


Posted by Steven

Working on new features for IT Hub Brunswick, I encountered a curious problem. Sending a POST request from the Angular 8 frontend to the Spring MVC backend returned incomplete header information. This article is about how to expose additional headers to the client.

One of the most important features of the IT Hub is the list of events and the list of community groups. Events are organized by a specific group and for both of these, information is provided on the website. For groups, a logo-image can be uploaded. Here's the call to upload a new logo in the (frontend-) controller:

  1. postNewLogo(Number(newGroup.id), this.currentFileUpload).subscribe((httpResponse) => {
  2.  
  3. // Force reloading the logo image in the template via call to server with randomized URI. URI of image is the
  4. // same, however it has to change for Angular to reload it.
  5. let location = httpResponse.headers.get('Location').toString();
  6. newGroup._links.image.href = location += '?random+\=' + Math.random();
  7. this.currentFileUpload = null;
  8. this.logoUploaderNewGroup.clear();
  9. });
  10.  
  11. postNewLogo(groupId: number, logo: File): Observable<HttpResponse<object>> {
  12. const formData = new FormData();
  13. formData.append('groupID', groupId + '');
  14. formData.append('file', logo);
  15.  
  16. return this.http.post<HttpResponse<object>>(environment.adminGroupsUrl + '/logo', formData, {
  17. observe: 'response'
  18. });
  19. }

What happens here is that in line 16, the Angular HttpClient sends a POST request to the backend. The result of this POST is returned by postNewLogo(). It returns an Observable of HttpResponse (line 11) which is used to extract the location of the saved logo (line 5) by accessing the header, more specific the "Location" in the header.

The first thing to notice here is line 17. Accessing the header is only possible by providing options to the POST-call to tell the HttpClient to observe the whole response. The default setting here is that it will only observe the body of the response. Hence, information from the header is not accessible. This makes perfect sense because most of the time, the body from a response is type-casted into an object that is used in the client, for example to display new values. To get to the header instead of the body requires additional code.

Second, the postNewLogo-method should return the correct type, an Observable of HttpResponse. This is necessary to access the headers of the response in line 5.

This code should have worked without a problem, as many stackoverflow articles promissed. However, it didn't. Running this code results in an error in line 5 because the header did not contain a key named "Location". The funny thing is that all of the headers are visible in the debug console in the browser, including "Location". Angular however could not see those headers.

I found the missing part was not in the frontend, but in the backend. Here's (part of) the WebConfig that solved the problem:

  1. @Configuration
  2. @EnableWebMvc
  3. @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
  4. public class WebConfig implements WebMvcConfigurer {
  5.  
  6. // ...
  7.  
  8. @Bean
  9. public CorsConfigurationSource corsConfigurationSource() {
  10.  
  11. // ...
  12.  
  13. final CorsConfiguration configuration = new CorsConfiguration();
  14. configuration.setAllowedOrigins(allowedOrigins);
  15. configuration.setAllowedMethods(List.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
  16. // setAllowCredentials(true) is important, otherwise:
  17. // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
  18. configuration.setAllowCredentials(true);
  19. // setAllowedHeaders is important! Without it, OPTIONS preflight request
  20. // will fail with 403 Invalid CORS request
  21. configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));
  22. // allow header "Location" to be read by clients to enable them to read the location of an uploaded group logo
  23. configuration.addExposedHeader("Location");
  24. final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  25. source.registerCorsConfiguration("/**", configuration);
  26. return source;
  27. }
  28.  
  29. // ...
  30. }

The important line is 23. Here, an additional header "Location" is exposed. Because of security reasons, only six headers are exposed by default: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified and Pragma. To allow the frontend to see the additional, manually added "Location", it has to be exposed explicitly.

TLDR;

Although all of the headers of a POST-request can be seen in the debug console of the browser, web application frameworks like Angular cannot use them because of security reasons. Those additional headers have to be exposed in the backend. Also, the Angular HttpClient has to be set up to observe the header instead of the body of a POST-call.

Category: 
Share: