spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.95k stars 40.65k forks source link

Spring Boot request is returning another endpoint API #19895

Closed thiagosantoscunha closed 4 years ago

thiagosantoscunha commented 4 years ago

I set up a test scenario to deal with an error that was happening on my frontend.

Problem situation:

The problem was that I was making a request for a ‘/ api / v1 / phones’ route, Spring ends up returning ‘/ api / v1 / profiles’, as if the behavior were a cache. The data in the frontend, in some moments were not rendered, and that was when I decided to implement the stress test since I was not able to get it through the debug mode.

Besides one having thought of being a cache problem, like a route that returns a Phone API, it returns a Profile API, since the endpoint is configured with relevant entities?

Stress test scenario

The tests performed captured the following amount of errors:

Json

The time and number of requests made are stored in the image below:

Requests

The scenario is a mass of requests made in alternate times, for two endpoints, the profile and the phone, simulating parallel requests. See the Javascript code I implemented to perform the tests:

setInterval(() => {

var request = new XMLHttpRequest();

request.onreadystatechange = function() {
  if(request.readyState === 4) {
    if(request.status === 200) { 
      // console.log(request.responseText);
    } else {
      console.log('An error occurred during your request: ' +  request.status + ' ' + request.statusText);
    } 
  }
}

request.open('Get', 'http://localhost:8080/api/v1/perfis');
request.send();

} , 225);

setInterval(() => {

var request = new XMLHttpRequest();

request.onreadystatechange = function() {
  if(request.readyState === 4) {
    if(request.status === 200) { 

    if (request.response !== '{"data":[{"id":"9040de2b-137a-45d3-902e-5ecdf4cf8c6d","codigoDeArea":"22","numero":"123654789"},{"id":"c5490c98-6a3f-4bd7-a7bc-b282750cec97","codigoDeArea":"22","numero":"997855566"}],"errors":[]}') {
        console.log(request.responseText);  
    }
    } else {
      console.log('An error occurred during your request: ' +  request.status + ' ' + request.statusText);
    } 
  }
}

request.open('Get', 'http://localhost:8080/api/v1/telefones/lista');
request.send();

} , 300);

The problem was identified when the request was being made for the telephone API. The Java code that implements the Endpoint is very simple and is built in the following way:

@RestController
@RequestMapping(path = "api/v1/telefones")
public class TelefoneController implements ControllerTemplate<TelefoneDto> {

    @Qualifier("telefoneServiceImpl")
    @Autowired
    private TelefoneService service;

    @Autowired
    private RestResponse<List<TelefoneDto>> responseList;

    (...)

    @GetMapping(path = "/lista")
    public ResponseEntity<RestResponse<List<TelefoneDto>>> buscaTudoPorUsusarioCorrente(@AuthenticationPrincipal UserDetails userDetails) {
        String apelido = userDetails.getUsername();
        List<TelefoneDto> dtos = service.buscaTudoPorApelido(apelido);
        responseList.setData(dtos);
        return ResponseEntity.ok(responseList);
    }

}

My service method code is:

@Override
    public List<TelefoneDto> buscaTudoPorApelido(String apelido) {
        List<TelefoneEntity> entities = repository.buscaTudoPorApelidoDoUsuario(apelido);
        return converter.toDtoList(entities);
    }

My custom query is:

@Query("select t from Telefone t join Perfil p on p.pessoa = t.pessoa join Pessoa pe on pe = p.pessoa join Usuario u on p.usuario = u where u.apelido = :apelido")
    List<TelefoneEntity> buscaTudoPorApelidoDoUsuario(@Param("apelido") String apelido);

My Profile controller is:

@RestController
@RequestMapping(path = "api/v1/perfis")
public class PerfilController implements ControllerTemplate<PerfilDto> {

    @Qualifier("perfilServiceImpl")
    @Autowired
    private PerfilService service;

    @Autowired
    private RestResponse<PerfilDto> response;

    @Autowired
    private RestResponse<List<PerfilDto>> responseList;

    @Autowired
    private PerfilValidator validator;

    @Override
    @GetMapping
    public ResponseEntity<RestResponse<List<PerfilDto>>> buscaTudo() {
        List<PerfilDto> entities = service.buscaTudo();
        responseList.setData(entities);
        return ResponseEntity.ok(responseList);
    }

    @Override
    @GetMapping(path = "/{id}")
    public ResponseEntity<RestResponse<PerfilDto>> buscaPorId(@PathVariable("id") String id) {
        PerfilDto entity = service.buscaPorId(id);
        response.setData(entity);
        return ResponseEntity.ok(response);
    }

    @Override
    @PostMapping
    public ResponseEntity<RestResponse<PerfilDto>> salvar(@RequestBody PerfilDto perfilDto) {
        validator.naoPodeAdicionar(perfilDto);
        PerfilDto entity = service.salvar(perfilDto);
        response.setData(entity);
        return ResponseEntity.ok(response);
    }

    @Override
    @PutMapping
    public ResponseEntity<RestResponse<PerfilDto>> atualizar(@RequestBody PerfilDto perfilDto) {
        validator.naoPodeAtualizar(perfilDto);
        PerfilDto entity = service.atualizar(perfilDto);
        response.setData(entity);
        return ResponseEntity.ok(response);
    }

    @Override
    @DeleteMapping
    public ResponseEntity<?> removePorId(@PathParam("id") String id) {
        validator.naoPodeRemover(id);
        service.removePorId(id);
        return ResponseEntity.ok(true);
    }

    @GetMapping(path = "/usuarioCorrente")
    public ResponseEntity<RestResponse<?>> buscaPorApelidoUsuarioCorrente(@AuthenticationPrincipal UserDetails userDetails) {
        PerfilDto resp = service.buscaPorApelidoUsuario(userDetails.getUsername());
        response.setData(resp);
        return ResponseEntity.ok(response);
    }
}

Libraries and versions:

Java 8 (Downgraded to Java 8, before it was 13, because I thought it might be a version problem) Spring 2.2.2.RELEASE Lombok JPA Postgre How to solve?

Would you like to know how to solve this problem? Why is a request being made to the phone api, returns an endpoint of the profile api?

Possible solution:

I don't know if this is a bug or some type of Spring Framework feature. But, when I remove Spring's responsibility to take care of the responseand responseListinstance, the error stops. I read Thread Safity too, can it be related?

OBS:

I AM NOT USING CACHE!

Video link with stress test

wilkinsona commented 4 years ago

Thanks for the report. Given that no one else seems to be affected, my initial suspicion is that the problem's on the client side and the request is being sent to the wrong URL rather than the server sending the wrong response. When an error occurs, have you checked the URL that the faulty request was sent to was correct?

Unfortunately, I think it's going to be very hard for us to diagnose this without being able to reproduce the problem. Can you please use what you've shared above as the basis for a minimal sample that reproduces the problem in the form of a project that we can build and run? You can share it with us as a zip attached to this issue or in a separate repo on GitHub.

knoobie commented 4 years ago

That autowiring of RestResponse looks kinda odd to me. I'm wondering, that's a custom class of yours?

wilkinsona commented 4 years ago

Well spotted, @knoobie. Thank you. Unless that bean is request-scoped, it's going to cause thread-safety problems.

thiagosantoscunha commented 4 years ago

@knoobie e @wilkinsona.

I am extremely grateful for the answer. Come on!

RestResponse is a custom class for returning custom fields, in addition to which one or more entities can deliver. Here is a brief example of the class:

@Getter @Setter
@Component
public class RestResponse<T> {
    private T data;
    private List<ErrorResponse> errors;

    public RestResponse(T data) {
        this.data = data;
    }

    public List<ErrorResponse> getErrors() {
        if (errors == null) {
            errors = new ArrayList<>();
        }
        return errors;
    }
}

@wilkinsona As seen in the javascript code sent, it is just a test that slightly simulates the palarerism of requests made asynchronously. For some reason, depending on the video, Spring returns data from another endpoint, as if it were in a cache. When I remove Spring's responsibility to inject my component, which is a very common pattern, it works normally. Despite being silly, it was very complex to understand this behavior, since Spring would take care of instantiating the class at the opportune moments of its call. Remembering, the error is not at the client's side. I say that with absolute certainty.

I have a friend, with extensive experience in the field, where he was helping me to identify, and he did something similar, which generated a similar error.

wilkinsona commented 4 years ago

As suspected, your RestResponse isn’t thread-safe. If the same instance is shared between your two controllers you will see the output from one controller in a response sent from the other controller. Rather than RestResponse being a bean that is injected, each controller should create and return a new instance each time it handles a request.

Please let us know if the above fixes your problem. If it does not, we will need a minimal sample to be able to help further.

thiagosantoscunha commented 4 years ago

Yes, you are absolutely right @wilkinsona, but I thought Spring Dependency Injection would deal with this situation. When I remove the note and eliminate Spring's responsibility to take care, it works just fine.

insinfo commented 4 years ago

I'm having the same problem

wilkinsona commented 4 years ago

@thiagosantoscunha Spring cannot make your code thread-safe. It could have helped you here by making your RestResponse bean request-scoped but there is no need to do so in this case. It will be more efficient to create an instance per request as I recommended above.

When I remove the note and eliminate Spring's responsibility to take care, it works just fine.

I'm not sure exactly what change you are describing here, but if you're creating a local RestResponse instance in your @RequestMapping method and returning it, this is exactly what you should do. RestResponse isn't a dependency to be shared across your whole app. It's specific to each individual request and its lifecycle should match.

I am going to close this issue as it appears that everything is working as designed. If you have any follow-on questions about threading in Spring Framework, Spring MVC, and Spring Boot please ask on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.

insinfo commented 4 years ago

@wilkinsona It is not a customer problem, the test was run without problems in several browsers directly through the browser terminal, and I also tested it with other implementations in the Backend such as PHP Slim Framework and java Jersey and the problem did not occur

wilkinsona commented 4 years ago

@insinfo Without some more context about your specific problem, I'm afraid we won't be able to help you. If you'd like some help, please ask a question on Stack Overflow or Gitter and the Spring Boot team and the rest of the Spring community will do their best to help you.