6

I am developing my first Spring Boot application and i ran into a weird problem. The configuration is very basic:

    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.pawsec</groupId>
    <artifactId>kitchen</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>kitchen</name>
    <description>The Kitchen restaurant system</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency> 
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.pawsec</groupId>
            <artifactId>common</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

We have some Javascript code on a page calling these two services. When the controller returns a Guy object in the first method, we get an empty response:

    {data: "", status: 200, statusText: "", headers: {…}, config: {…}, …}
config: {adapter: ƒ, transformRequest: {…}, transformResponse: {…}, timeout: 0, xsrfCookieName: "XSRF-TOKEN", …}
data: ""
headers: {}
request: XMLHttpRequest {onreadystatechange: ƒ, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: ""
: Object

When we return a List of Guy objects from the second method, however, we get the full Json structure

back:
{data: Array(3), status: 200, statusText: "", headers: {…}, config: {…}, …}
config: {adapter: ƒ, transformRequest: {…}, transformResponse: {…}, timeout: 0, xsrfCookieName: "XSRF-TOKEN", …}
data: Array(3)
0: {guyId: 1, name: "Walter Sobchak", age: 45}
1: {guyId: 2, name: "Jeffrey Lebowski", age: 42}
2: {guyId: 3, name: "Theodore Donald Kerabatsos", age: 39}
length: 3
: Array(0)
headers: {content-type: "application/json;charset=UTF-8", cache-control: "private", expires: "Thu, 01 Jan 1970 00:00:00 GMT"}
request: XMLHttpRequest {onreadystatechange: ƒ, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: ""
: Object

The controller looks like this:

package com.pawsec.kitchen.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.pawsec.kitchen.model.Guy;

@RestController
public class GuyController {

    @RequestMapping(value="/get/guy/{guyId}", method=RequestMethod.GET,
            headers={"Accept=application/json"})
    public Guy getGuy(@PathVariable("guyId") int guyId) {
        Guy someGuy = new Guy(guyId, "Walter Sobchak", 45);
        return someGuy;
    }

    @RequestMapping(value="/get/guys", method=RequestMethod.GET,
            headers={"Accept=application/json"})
    public List<Guy> getGuys() {
        Guy walter = new Guy(1, "Walter Sobchak", 45);
        Guy theDude = new Guy(2, "Jeffrey Lebowski", 42);
        Guy donny = new Guy(3, "Theodore Donald Kerabatsos", 39);
        List<Guy> guys = new ArrayList<Guy>();
        guys.add(walter);
        guys.add(theDude);
        guys.add(donny);
        return guys;
    }

}

Strangely, if i call these two services from a browser, i get the correct Json structure for both the calls.

When i run a mvn dependency:tree, the expected Jackson dependencies that come with a basic Boot Project are there.

This is what the JavaScript code looks like:

return dispatch => {
        dispatch(fetchMenuStart());
        const url = 'https://boot.ourcompany.com:8443/get/guy/1'; 
        const headers = {
            headers: {
                'Content-Type': 'application/json'
            }
        }
        axios.get(url, headers)
            .then(res => {
                console.log(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.log("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };

Can anyone suggest what might be causing this or steps to test to figure out the issue?

New javascript example code:

const doesNotWork = 'https://boot.exmpledomain.com:8443/get/guy/1'; 
const doesWork = 'https://boot.exmpledomain.com:8443/get/guys'; 
const headers = {
    headers: {
    'Content-Type': 'application/json;charset=UTF-8'
    }
}
axios.get(doesNotWork, headers)
    .then(res => {
        console.log(res); 
    })
    .catch(error => {   
        console.log("error", error);
        const errorMsg = 'There was an error fetching the menu';
    });
26
  • 2
    did you set the content-type as application/json in the ajax call? Commented Feb 18, 2019 at 16:13
  • 2
    If you're getting the right response from the browser but incorrect from the js code, obviously there is some issue with the js code. Could you please add the js code snippet that you're using? Commented Feb 18, 2019 at 16:15
  • 1
    ..maybe show us "some Javascript code". Commented Feb 18, 2019 at 18:36
  • 1
    @MatsAndersson, Since you get the proper response when call this via the browser. The problem is in your front-end code. Can you add the full front end code? Also, do add the code for the other request too Commented Feb 25, 2019 at 10:32
  • 2
    @MatsAndersson - If it works in the browser, your problem is not the backend. The moment the JSON goes on the wire, there's not type Guy, there's no "custom beans" and so on. The json is plain text and your JS frontend should know how to work with that. Can you add interceptors for the request and responses and paste the output on both? github.com/axios/axios#interceptors Commented Mar 3, 2019 at 7:39

6 Answers 6

1

Could you please try changing the header to accept in the javascript

return dispatch => {
        dispatch(fetchMenuStart());
        const url = 'https://boot.ourcompany.com:8443/get/guy/1'; 
        const headers = {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }
        }
        axios.get(url, headers)
            .then(res => {
                console.log(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.log("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };
Sign up to request clarification or add additional context in comments.

1 Comment

Yep... he's missing the 'Accept' header in the client as well as the return type on the server (produces=MediaType.APPLICATION_JSON_VALUE)
1

I have finally solved this issue by disabling CORS, with the following class:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Profile("devel")
@Configuration
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**");
            }
        };
    }

}

I have also added the @Profile annotation to disable CORS only on development time.

By the way, the reason for the issue seems to be explained in:

https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md#Protecting-JSON

When returning an object, it is interpreted as a non-empty JSON object (such as {"key": "value"}). When returning a list, the same text is wrapped in squared brackets and it passes the protection.

1 Comment

Thank you for your input, Benjamin Valero
0

If you use spring, you should use ResponseEntity instead of directly returning the object:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

This is how I write my controllers:

@RestController
@RequestMapping(USERS)
public class UserController {

  @Autowired
  private UserService userService;

  @Autowired
  private RoleService roleService;

  @Autowired
  private LdapUserDetailsManager userDetailsService;

  @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<?> list(PagedResourcesAssembler<User> pageAssembler, @PageableDefault(size = 20) Pageable pageable, UserDTO condition) {
    Page<User> page = userService.findAll(pageable, condition);
    PagedResources<?> resources = pageAssembler.toResource(page, new UserResourceAssembler());
    return ResponseEntity.ok(resources);
  }

  @GetMapping(value = CoreHttpPathStore.PARAM_ID, produces= MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<UserResource> get(@PathVariable("id") Long id) {
    User user = userService.get(id);
    UserResource resource = new UserResourceAssembler().toResource(user);
    return ResponseEntity.ok(resource);
  }

  private void preProcessEntity(@RequestBody UserDTO entity) {
    if (null != entity.getPassword()) {
      userDetailsService.changePassword(entity.getOldPassword(), entity.getPassword());
    }
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  Long create(@RequestBody User user) {
    userService.insert(user);
    return user.getId();
  }

  @PutMapping(CoreHttpPathStore.PARAM_ID)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void modify(@PathVariable("id") Long id, @RequestBody UserDTO user) {
    user.setId(id);
    preProcessEntity(user);
    userService.updateIgnore(user);
  }

  @DeleteMapping(CoreHttpPathStore.PARAM_ID)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void delete(@PathVariable("id") Long id) {
    userService.delete(id);
  }

  @DeleteMapping
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void bulkDelete(@RequestBody Long[] ids) {
    userService.delete(ids);
  }
}

1 Comment

Using ResponseEntity is not a necessity at all.
0
+50

As your javascript is on a different domain from the spring-boot service, you need to configure CORS.

This could be done globally adding @CrossOrigin like this :

@RestController
@CrossOrigin
public class GuyController {

3 Comments

Dont think this would be the issue, since the request which returns list of custom object works fine.
@MadhuBhat: I made a test with spring-boot & axiom and it works with 1 object and a list of object serving the JS from the spring-boot without any problems. Using different webservers I reproduced a CORB exception with 1 object that is not present with a list of objects, disabling CORS it works in both cases.
Sorry everyone, i've been sick a few days. I can add that we have multiple Spring-based systems running. They have hundreds of controller methods returning both our own classes and lists and other classes. We call these methods using this same javascript way and we have never had this issue before. This, however, is our first time we use a Spring boot application with its built-in Json support. It is also the first time we make the calls from a React client that is not integrated with the server side. Our other systems use JSP for the GUI.
0

OK everyone, thank you so much for your efforts. It turns out that the solution suggested by @mpromonet (adding CrossOrigin annotation on the controller) solves this problem. I am still very curious to know why a method returning List works and one returning a Guy does not if this is a cross-origin issue. It does not seem logical and it makes the issue a lot harder to figure out.

Comments

-1

You have to add @ResponseBody annotation before your method.

@ResponseBody
public Guy ....

1 Comment

@RestController doesn't need @ResponseBody - see this answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.