4
\$\begingroup\$

I am working on a JSF 1.1 webapp. Lots of POST request take some time and users have the bad habit of clicking again when it takes too long. I am planning to target all the long running processes and add an overlay to prevent this, but it will take some time. Before it is finished, I am asked to setup a quickwin on server side do block double submission of the same POST request.

My comprehension of the mechanisms is that when the users clicks on the submit button a second time, the browser aborts the first post submission (as I can see it in Firebug), but on server side both are processed in parallel. So in order to avoid to process two times the same request while sending the expected response to the client, I am planning to set up a Filter on incoming POST request with the following process:

Suppose someone sends for example 3 exact same POST requests Rq1, Rq2, Rq3 with the corresponding responses being Rs1, Rs2, Rs3. We want to block Rq2 and Rq3 while Rq1 is processing. When Rs1 is ready we will block everything while we are copying Rs1 into Rs3. Then we send Rs3 back to the client and once this is done, we terminate Rs1 and Rs2 by sending back a 200 (they should have already been aborted on client side anyway)

Here is the implementation I thought about. The synchronization stuff has purposely been skipped as the filter is quite heavy already. I need reviews about this because I really feel like having imagined a really dirty thing.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
        ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse res = (HttpServletResponse) response;
    HttpSession session = req.getSession(false);
    if (session == null || notAPost(request)) {
        chain.doFilter(request, response);
        return;
    }

    Map<String, String> hashTokenMap = (Map<String, String>) session.getAttribute("HASH_TOKEN_MAP");
    Map<String, HttpServletResponse> hashResponseMap = (Map<String, HttpServletResponse>) session
            .getAttribute("HASHS_RESPONSE_MAP");
    String requestHash = makeCheckSumString(request);
    String token = generateUniqueToken();

    // double POST case
    if (hashTokenMap.containsKey(requestHash)) {
        hashTokenMap.put(requestHash, token); // this will allow the previous POST to understand that it has been overriden
        // wait until we detect that either the response is available or has already been processed
        while (hashResponseMap.get(requestHash) == null && hashTokenMap.get(requestHash) != null) {
            sleep(1000);
        }

        // this is the last submitted post => we copy the response and return it
        if (token.equals(hashTokenMap.get(requestHash))) {
            HttpServletResponse realResponse = hashResponseMap.remove(requestHash);
            copyResponses(realResponse, res);
            hashTokenMap.remove(requestHash);
            return;
        }
        // there was another post submitted inbetween
        else {
            // since we have been overriden, lets wait till other business is finished
            while (hashTokenMap.get(requestHash) != null) {
                sleep(1000);
            }
            res.sendError(HttpServletResponse.SC_OK, "Already replied");
            return;
        }
    }
    // first POST case
    else {
        hashTokenMap.put(requestHash, token);
        chain.doFilter(request, response);

        // there was no double POST
        if (token.equals(hashTokenMap.get(requestHash))) {
            return;
        }
        // some other POST has been submitted during processing
        else {
            hashResponseMap.put(requestHash, res);
            while (hashResponseMap.get(requestHash) != null) {
                sleep(1000);
            }
            res.sendError(HttpServletResponse.SC_OK, "Already replied");
            return;
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

Just some suggestions:

1.Change you UI concept.

  • Hide Submit button until the server responds.
  • Show Please Wait notification / dialog

2.Use JavaScript to send request and prevent multiple submits:

if(ignoreSubmitEvent){
    return;
}

hideSubmitButton();
ignoreSubmitEvent = true;

var jqxhr = $.post( "/url...", { submittoken: "unique-one-time-token", name: "dit" });

jqxhr.done(function( data ) {
     [...] // your code 
});

jqxhr.fail(function() {
     [...] // your code 
});

// DO IT ALLWAYS
jqxhr.always(function() {
    showSubmitButton();
    ignoreSubmitEvent = false;
});

3.It could be a good idea to use HandlerInterceptor instead of Filter

public class OneTimeTokenPostInterceptor extends HandlerInterceptorAdapter {

    private static final String UNIQUE_ONE_TIME_TOKEN = "unique-one-time-token";
    private static final Logger logger                = LoggerFactory.getLogger(OneTimeTokenPostInterceptor.class);

    private Set<String>         tokens                = Collections.synchronizedSet(new HashSet<String>());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getParameter(UNIQUE_ONE_TIME_TOKEN);

        logger.debug("preHandle: token = {}", token);

        if (token == null || tokens.contains(token)) {
            logger.error("token {} is null or allready used", token);
            response.sendError(202); // ignore request
            return false;
        }
        tokens.add(token);
        return true;
    }

    //
    // GET CALLED AFTER CONTROLLER DID HIS JOB
    //

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        String token = request.getParameter(UNIQUE_ONE_TIME_TOKEN);

        logger.debug("postHandle: token = {}", token);

        if (token != null) {
            tokens.remove(token);
        }
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ your #1 is what I also want to do, but I have to do this in hundreds of places in the webapp. As for #2 I don't have control on ajax requests, those are generated. Concerning #3, I also considered something like that. Actually it strongly recalls an implementation of CSRF protection. I am also working on somthing like that, but it needs some testing as I am not sure the user will still get the expected result when you basically refuse any order beside the first one. \$\endgroup\$ – Aldian Apr 22 '15 at 14:17
  • \$\begingroup\$ Me as user would really expect some notification which should keep me from clicking around till initial operation done. about #2. You do not need any control on ajax request. all you need is to send a post request over little java script function, instead of default send action by browser. \$\endgroup\$ – dieter Apr 22 '15 at 14:25
  • \$\begingroup\$ About #2 I have no control on that either. This is JSF code, all the logic to send post is encapsulated I can't modify it or have to hack into the tool's code. About #3 I just made a test and this is what I was afraid about: if you just drop the redundant request and send back a 202 the server is ok but the client just gets a blank page because first post have been aborted by the second one. Then I tried to put on old till first request has finished to send back the 202 but result is the same. So you must make sure that the reply the user gets come from its last post submission \$\endgroup\$ – Aldian Apr 22 '15 at 15:15
  • \$\begingroup\$ @Aldian Wouldn't this answer help? \$\endgroup\$ – maaartinus Apr 22 '15 at 20:53
  • \$\begingroup\$ @maaartinus I already reviewed a lot of propositions about client side javascript solutions since there are many people on the forums that have a similar problem to mine and it always come down there. I was searching for something else, but seems that it is almost impossible without massive hacking \$\endgroup\$ – Aldian Apr 23 '15 at 9:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.