Today I struggled with more redirect related issues as I continued rewriting our Facebook app with Wicket - I'll note my observations here (keep in mind that I am a first-timer with Wicket).

Caveats!

  • The newWebResponse() implementation below ignores the getRequestCycleSettings().getBufferResponse() value, which the original implementation takes into consideration.
  • If you embed iframes to your fbml pages with the Fb:iframe tag that point to the app's callback url, you will want to "enable" the normal redirects for those requests.

Problem explained

When I submit one of my forms, my access log reveals that Wicket follows the "redirect after post" pattern.

"POST /appcontext/somepage?wicket:interface=:0:somepanel:someform::IFormSubmitListener:: HTTP/1.1" 200
"POST /appcontext/somepage?wicket:interface=:1:::: HTTP/1.1" 200

In the context of a Facebook FBML application, the second access log entry never appears, and the url in the browser remains as follows:

http://apps.facebook.com/myapp/somepage?wicket:interface=:0:somepanel:someform::IFormSubmitListener::

which seems to represent some expired state of the page. If one does a refresh on the page, the browser's "repost popup" appears, and if one chooses to resubmit the form, Wicket responds with a 404 - Page expired.

This is by Facebook's design - the redirect issued by Wicket after the form post is simply discarded and the browser is never made aware of it.

Solution explained

Facebook provides a custom tag to accomplish a redirect within the Facebook canvas - Fb:redirect (http://wiki.developers.facebook.com/index.php/Fb:redirect).

The trick is to make Wicket use the Fb:redirect tag instead of doing the "normal" redirect.

org.apache.wicket.protocol.http.WebResponse

If you check out the source for org.apache.wicket.protocol.http.WebResponse, you'll find a redirect() method that contains the following line:

httpServletResponse.sendRedirect(url);

This is what we wish to avoid, and instead output the Fb:redirect tag into the response body:.

FBWebResponse

Let's override the redirect() method:

import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.protocol.http.WebResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FBWebResponse extends WebResponse
{
    private static final Logger log = LoggerFactory.getLogger(FBWebResponse.class);

    private final HttpServletResponse httpServletResponse;

    public FBWebResponse(HttpServletResponse response)
    {
        super(response);
        this.httpServletResponse = response;
    }

    @Override
    public void redirect(String url)
    {
        if (!redirect)
        {
            if (httpServletResponse != null)
            {
                // encode to make sure no caller forgot this
                url = httpServletResponse.encodeRedirectURL(url);
                try
                {
                    if (httpServletResponse.isCommitted())
                    {
                        log.error("Unable to redirect to: " + url +
                                ", HTTP Response has already been committed.");
                    }

                    if (log.isDebugEnabled())
                    {
                        log.debug("Redirecting to " + url);
                    }

                    if (isAjax())
                    {
                        /*
                         * By reaching this point, make sure the HTTP response status code is set to
                         * 200, otherwise wicket-ajax.js will not process the Ajax-Location header
                         */
                        httpServletResponse.addHeader("Ajax-Location", url);

                        // safari chokes on empty response. but perhaps this is
                        // not the best place?
                        httpServletResponse.getWriter().write(" ");
                    }
                    else
                    {
                        // httpServletResponse.sendRedirect(url);
                        httpServletResponse.getWriter().write("<fb:redirect url=\"" + url + "\" />");
                    }
                    redirect = true;
                }
                catch (IOException e)
                {
                    log.warn("redirect to " + url + " failed: " + e.getMessage());
                }
            }
        }
        else
        {
            log.info("Already redirecting to an url current one ignored: " + url);
        }
    }
}

The redirect() method was a copied from WebResponse, and the following change was applied:

// httpServletResponse.sendRedirect(url);
httpServletResponse.getWriter().write("<fb:redirect url=\"" + url + "\" />");

Make use of FBWebResponse in your application

In your application class, override the newWebResponse() method:

@Override
protected WebResponse newWebResponse(HttpServletResponse servletResponse)
{
    return new FBWebResponse(servletResponse);
}

You should be set!

A big thank you goes to jwcarman on ##wicket for helpful pointers.

  • No labels