2

We have an eCommerce webapp with the front-end server and the API server being in separate EC2 Ubuntus. The API server exposes 168 API RESTFul endpoints using Spring MVC. Each class has the @RestController annotation. Now, 166 endpoints are called and work correctly except for 2 of them. This question is for one of these 2 which we added to the system 2 weeks ago.

When this endpoint (which is a POST) is called by the front-end, we see in the Chrome DevTools (network tab) that first the OPTIONS call occur and right after the POST call. That call is supposed to place a shopping order. The problem is that this POST call is invoked twice within a second of each other. We see the results of both of them, because we see 2 new orders in the database with their own OrderID and we see the 2 calls in the tomcat access logs. The front-end makes only 1 API call.

We use OpenJDK 11.0.11+9, tomcat 9.0.59, Spring Framework 5.3.16, Spring Security 5.6.2, javaee-api 8.0.1 and servlet 4.0. This webapp has been working for 14 months, but when we added a new endpoint for the user to place orders, our clients see the duplicate orders and they have to cancel one of them which is very annoying to them. Here is the endpoint method:

@RestController
@RequestMapping(value = "/api/v1")
public class DeliveryCourierMultidropAPIService extends OrionWebService
{
    @PostMapping(value = "/orders", produces = "application/json")
    public ResponseEntity<OrderResponseBean> placeOrder(@RequestBody OrderRequestBean requestBean, HttpServletRequest request, HttpServletResponse response, Model model)
    {
        try
        {
            return OrderService.placeOrder(requestBean, request, response);
        }
        catch(Throwable e)
        {
            return ResponseEntity.badRequest().body(OrderResponseBean.builder()
                            .hasErrors(true)
                            .build());
        }
    }
}

The tomcat server.xml (for the connectors) has:

<Executor name="tomcatThreadPool" namePrefix="tomcat9-thread-" maxIdleTime="60000"
maxThreads="50" minSpareThreads="5"/>

<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" redirectPort="8443" maxConnections="100" acceptCount="300"/>

<Connector executor="tomcatThreadPool" SSLEnabled="true" maxPostSize="5000000" port="8443" protocol="org.apache.coyote.http11.Http11Nio2Protocol" scheme="https"
    secure="true" compression="on" maxKeepAliveRequests="10" connectionTimeout="60000"
    maxConnections="100" acceptCount="300" enableLookups="false" connectionUploadTimeout="120000" disableUploadTimeout="false">
    <SSLHostConfig>
       <Certificate certificateFile="conf/cert.pem" certificateKeyFile="conf/privkey.pem" certificateChainFile="conf/chain.pem" />
    </SSLHostConfig>

    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" compression="on" keepAliveTimeout="360000"/>
</Connector>

This is the AJAX call (redacted), but it is a reusable piece of code. I included the endpoint and the method name manually here. This piece of code works for all the other 167 endpoints. It is the same reusable AJAX code:

setTimeout(function()
{
    $.ajax(
    {
        type : "POST",
        url : "https://awsec2.com/api/v1/orders",
        contentType : "application/json; charset=UTF-8",
        data : parameters,
        dataType : "json",
        async : true,
        crossDomain: true,
        headers: headers,
        
        success : function(ajaxResponseData)
        {
            navigation.hidePreloader();
            
            if(typeof ajaxResponseData === "object")
            {
                alert("order placed");
            }
        },
        
        
        error : function(jqXHR, text, errorThrown)
        {
            navigation.hidePreloader();
            alert("error");
        },


        complete : function(jqXHR, textStatus)
        {
            navigation.hidePreloader();
        }
    });
}, 300);

This is the Spring Security config (redacted):

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = {"package1", "package2", "package3"})
public class OurCustomSpringSecurityConfiguration extends WebSecurityConfigurerAdapter
{
    ..........................
    ..........................
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http = http.csrf().ignoringAntMatchers(URLPatternsThatDoNotNeedCSRFToken).and();
        http = http
                        .sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and();
        http = http
                        .exceptionHandling()
                        .accessDeniedPage("/error")
                        .and();
        http = http.authorizeRequests()
                        .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                        .antMatchers("/api/v1/drivers/**").hasAuthority(UserAuthority.Driver.get())
                        .antMatchers("/api/v1/orders/**").hasAuthority(UserAuthority.Business.get())
                        .antMatchers("/api/v1/**").authenticated()
                        .anyRequest().denyAll()
                        .and();
        http = http.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        http = http.addFilterBefore(new JavaThreadIDGeneratorFilter(), JWTTokenFilter.class);
        http = http.headers().cacheControl().disable().and();
        http = http.headers().contentSecurityPolicy("frame-src 'none'").and().and();
        http = http.headers().contentTypeOptions().and().and();
        http = http.headers().frameOptions().sameOrigin().and();
        http = http.headers().httpStrictTransportSecurity().includeSubDomains(true).preload(true).and().and();
        http = http.headers().referrerPolicy(ReferrerPolicy.STRICT_ORIGIN)
                        .policy(ReferrerPolicy.STRICT_ORIGIN).and().and();
        http = http.headers().xssProtection().block(true).xssProtectionEnabled(true).and().and();
    }
}

UPDATE I "solved" the problem by implementing a GUID for that endpoint (generated by the front-end) and used HazelCast to handle a lockable cache of these GUIDs making sure that a duplicate GUID will not go through

Dimitrios Efthymiou
  • 535
  • 1
  • 5
  • 17
  • does the new api endpoint respond with a 307 , 308, 301, or 302 redirect? any of those might, and the first 2 would, mean that the POST was re-submitted – erik258 Mar 19 '22 at 17:07
  • They both respond with 200 every time according to the tomcat logs. The browser makes only one API call and it gets 200. However, the back-end handles 2 identical ones. Only for that endpoint. I google-searched this problem and I found, like, 2 questions. One of them was never answered since 2017, and for the other people were asking the person if they have used @ResponseBody and produces and stuff. I have been using these things for years which is why those answers did not help. That's why I asked the question myself here. If this is not solved in a week, we will lose all of our clients – Dimitrios Efthymiou Mar 19 '22 at 17:24
  • Is there any timeout and retry logic for the calling code? I suggest if it possible for you add the code which calls this endpoint and Spring security configurations to your question. – badger Mar 19 '22 at 17:30
  • no. There is no timeout. In the network tab in the Chrome DevTools we see the OPTIONS call first and immediately after that the POST call. Then it says "pending" for 3-4 seconds (for the back-end to do its job) and then we see 200. One API call. No timeouts or repeated calls in the browser. Only in tomcat – Dimitrios Efthymiou Mar 19 '22 at 17:53
  • if you change `async : true` to `false` problem still exists? – badger Mar 19 '22 at 18:03
  • We tried that, as well, yes. We made the whole ajax code to process only 1 API call at a time and reject any other calls while ajax is already processing. Same problem. I think it is tomcat or spring – Dimitrios Efthymiou Mar 19 '22 at 18:05
  • OPTIONS before POST sounds like CORS Preflight, so maybe look there. (I wrote up a bit with my experiences with WebSphere, Spring MVC, and CORS Preflight at https://dougbreaux.github.io/2021/01/12/cors-and-websphere.html ) – dbreaux Mar 21 '22 at 15:07
  • We know about the OPTIONS call and we have configured it for all 168 API endpoints and it always works. We receive 2 POST calls within a few milliseconds of each other – Dimitrios Efthymiou Mar 21 '22 at 16:04

0 Answers0