Java Tip 136: Protect Web application control flow

A strategy built on Struts manages duplicate form submission

Web application designers and programmers often face situations where a form submission must be protected against a rupture in the normal control flow sequence. This situation typically occurs when a user clicks more than once on a submit button before the response is sent back or when a client accesses a view by returning to a previously bookmarked page. Control flow sequence is particularly important to preserve when form submission involves transaction processing on the server.

This article proposes a well-encapsulated solution to this problem: a strategy implemented as an abstract class that leverages the Struts framework.

Note: You can download this article's source code from Resources.

Client vs. server solutions

Different solutions can solve this multiple form submission situation. Some transactional sites simply warn the user to wait for a response after submitting and not to submit twice. More sophisticated solutions involve either client scripting or server programming.

In the client-only strategy, a flag is set on the first submission, and, from then on, the submit button is disabled based on this flag. While appropriate in some situations, this strategy is more or less browser dependent and not as dependable as server solutions.

For a server-based solution, the Synchronizer Token pattern (from Core J2EE Patterns) can be applied, which requires minimal contribution from the client side. The basic idea is to set a token in a session variable before returning a transactional page to the client. This page carries the token inside a hidden field. Upon submission, request processing first tests for the presence of a valid token in the request parameter by comparing it with the one registered in the session. If the token is valid, processing can continue normally, otherwise an alternate course of action is taken. After testing, the token resets to null to prevent subsequent submissions until a new token is saved in the session, which must be done at the appropriate time based on the desired application flow of control. In other words, the one-time privilege to submit data is given to one specific instance of a view. This Synchronizer Token pattern is used in the Apache Jakarta Project's Struts framework, the popular open source Model-View-Controller implementation.

A synchronized action

Based on the above, the solution appears complete. But an element is missing: how do we specify/implement the alternate course of action when an invalid token is detected. In fact, given the case where the submit button is reclicked, the second request will cause the loss of the first response containing the expected result. The thread that executes the first request still runs, but has no means of providing its response to the browser. Hence, the user may be left with the impression that the transaction did not complete, while in reality, it may have successfully completed.

This tip's proposed strategy builds on the Struts framework to provide a complete solution that prevents duplicate submission and still ensures the display of a response that represents the original request's outcome. The proposed implementation involves the abstract class SynchroAction, which actions can extend to behave in the specified synchronized manner. This class overrides the Action.perform() method and provides an abstract performSynchro() method with the same arguments. The original perform method dispatches control according to the synchronization status, as shown in the listing below:

public final ActionForward perform(ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response)
    throws IOException, ServletException {
    HttpSession session = request.getSession();
    ActionForward forward = null;
    if (isTokenValid(request)) {
        // Reset token and session attributes
        reset(request);
        try {
            // Perform the action and store the results
            forward = performSynchro(mapping, form, request, response);
            session.setAttribute(FORM_KEY, form);
            session.setAttribute(FORWARD_KEY, forward);
            ActionErrors errors = (ActionErrors) 
                        request.getAttribute(Action.ERROR_KEY);
            if (errors != null && !errors.empty()) {
                saveToken(request);
            }
            session.setAttribute(ERRORS_KEY, errors);
            session.setAttribute(COMPLETE_KEY, "true");
        } catch (IOException e) {
            // Store and rethrow the exception
            session.setAttribute(EXCEPTION_KEY, e);
            session.setAttribute(COMPLETE_KEY, "true");
            throw e;
        } catch (ServletException e) {
            // Store and rethrow the exception
            session.setAttribute(EXCEPTION_KEY, e);
            session.setAttribute(COMPLETE_KEY, "true");
            throw e;
        }
    } else {
        // If the action is complete
        if ("true".equals(session.getAttribute(COMPLETE_KEY))) {
            // Obtain the exception from the session
            Exception e = (Exception) session.getAttribute(EXCEPTION_KEY);
            // If it is not null, throw it
            if (e != null) {
                if (e instanceof IOException) {
                    throw (IOException) e;
                } else if (e instanceof ServletException) {
                    throw (ServletException) e;
                }
            }
            // Obtain the form from the session
            ActionForm f = (ActionForm) session.getAttribute(FORM_KEY);
            // Set it in the appropriate context
            if ("request".equals(mapping.getScope())) {
                request.setAttribute(mapping.getAttribute(), f);
            } else {
                session.setAttribute(mapping.getAttribute(), f);
            }
            // Obtain and save the errors from the session
            saveErrors(request, (ActionErrors)
                    session.getAttribute(ERRORS_KEY));
            // Obtain the forward from the session
            forward = (ActionForward) session.getAttribute(FORWARD_KEY);
        } else {
            // Perform the appropriate action in case of token error
            forward = performInvalidToken(mapping, form, request, response);
        }
    }
    return forward;
}

As you see above, the protected action is performed only once, that is, if the token is valid. If other requests are received while the action is running, they are directed to the performInvalidToken() method's result until the action completes. By default, this method simply returns an ActionForward named "synchroError". This forward should lead to a page signaling that the action is in progress and providing a button to continue. This button simply resubmits to the same action without any form or parameter in the request (they will not be considered anyway). When the action completes, it stores its forward, form, exception, and errors, if any, in the session, and it sets a flag to indicate it has completed. The first request coming after the action completion will get the forward, form, exception, and errors from the session and continue as if it was the first request itself.

I include an example in the source code to demonstrate the behavior of a simple synchronized action. The provided implementation is based on Struts 1.0.2, but can easily be adapted for release 1.1.

Preserve consistent flow control

Multiple form submissions may cause inconsistency in transactions and must be avoided. For that purpose, the Synchronizer Token pattern is a great help. This article's proposed strategy nicely complements this pattern in recovering the response to the original request and, as such, helps preserve a consistent flow of control in Web applications.

Romain Guay is a system designer and developer with Systematix Consulting. He has long-time experience with object-oriented designs, starting with Smalltalk. He has been involved in research projects and various developments in the manufacturing and government sectors.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more