Page tree
Skip to end of metadata
Go to start of metadata

Your Rating: Results: 1 Star2 Star3 Star4 Star5 Star 20 rates

Introduction

This page is an attempt at describing how request wrappers work. The information below is what i learned from reading the spec and digging through the tomcat source code. It is likely valid for all containers. The spec used is version 2.5.

Summary of the constraints that the spec puts on the container

Forward

  • When the request dispatcher does a forward it MUST
    • Provide additional attributes available to the servlet/filter being called to process the forward.
    • Expose the path info of the url given to request.getRequestDispatcher() through the methods getRequestURI, getContextPath, getServletPath, getPathInfo, getQueryString.
    • Aggregate the query string parameters.

Include

  • When the request dispatcher does an include it MUST
    • Provide additional attributes available to the servlet/filter being called to process the include.
      • In a subsequent include these are replaced (they're scoped to be valid only within a call to include())
    • Aggregate the query string parameters.

Wrapper object identity

  • The request object you get as an argument to your filter or servlet MUST be the same instance that was passed to forward() include() or doFilter() to invoke your filter or servlet.
    • This means that the container CANNOT add wrappers to fulfill the above requirements.

How the container solves the above requirements

The container will add a wrapper when it does an include or a forward to change the behavior as the spec states. BUT it CANNOT add it at the front of the request wrappers. Instead, it adds it behind any custom wrappers. That is, behind all wrappers that the container has not put there.

An example:

This is what the request wrapper chain looks like in Tomcat when it is about to render a template.

  1. catalina.RequestFacade
  2. magnolia.MultipartFilter.RequestWrapper
  3. magnolia.CacheFilter.RequestWrapper

After the forward the chain looks like this:

  1. catalina.RequestFacade
  2. catalina.DispatcherWrapper <--- changes the path methods (request uri, servlet path, query string etc) and adds parameters and attributes
  3. magnolia.MultipartFilter.RequestWrapper
  4. magnolia.CacheFilter.RequestWrapper

After an include to a paragraph the chain looks like this:

  1. catalina.RequestFacade
  2. catalina.DispatcherWrapper
  3. catalina.DispatcherWrapper <--- adds attributes (and parameters and query string)
  4. magnolia.MultipartFilter.RequestWrapper
  5. magnolia.CacheFilter.RequestWrapper

When the include returns the container removes the second wrapper, and when the forward returns the container removes the first wrapper.

The container installs its wrappers by calling setRequest to change the wrapped instance. This is why you must always inherit your wrappers from HttpServletRequestWrapper.

How to write proper request wrappers

What you need to know is that things will change BEHIND you. You must never shadow path info, parameters or attributes. If you intend to change the return value of getRequestURI you must still return any new value that pops up behind you.

You can do this like this:

public class RequestUriChangingWrapper extends HttpServletRequestWrapper {
    private String originalValue;
    private String newValue;
    public RequestUriChangingWrapper(HttpServletRequest request, String newValue) {
        super(request);
        this.newValue = newValue;
        this.originalValue = request.getRequestURI()
    }
    public String getRequestURI() {
        String s = super.getRequestURI()
        if (StringUtils.equals(s, originalValue)
            return newValue;
        else
            return s;
    }
}

This is an example of what you should NOT do, since parameters that appear behind this wrapper on forward() and include() will not be visible.

public class NaiveParameterAddingRequestWrapper extends HttpServletRequestWrapper {
    private Map parameters;
    public NaiveParameterAddingRequestWrapper(HttpServletRequest request) {
        super(request);
        parameters = new HashMap(request.getParameterMap());
    }
    public void addParameter(String key, String value) {
        parameters.put(key, value);
    }
    public String getParameter(String name) {
        return parameters.get("key");
    }
}

But the container do add wrappers, doesnt that break wrapper object identity if the application doesnt add any custom wrappers?

Yes. The container doesnt meet this requirement when the application adds no wrappers of its own. Therefore you can never write applications that store the request object in thread local without updating it there after every call to include() and forward().

Impact in the Magnolia code base

ServletDispatchingFilter
  • Overrides getServletPath and doesnt check for changes behind it, it tries to detect when the container adds its wrapper behind it. This only works if the wrapper added by ServletDispatchingFilter is the one closest to the containers wrappers. Therefore it fails to do forwards when UnicodeNormalizationFilter is active.
UnicodeNormalizationFilter
  • Takes a copy of the request parameters when the wrapper is created and then uses only this copy. This effectively blocks access to any new parameters that appear after a forward() or include().
  • Holds a reference internally to what was the request being wrapped at time of creation. Since the container inserts its wrappers into the chain this reference gets out of sync when its the wrapper closest to the containers wrappers.

See MAGNOLIA-3264, MAGNOLIA-3310

MultipartFilter
  • Completely replaces the parameter functionality of the wrapped instance. This effectively blocks access to any new parameters that appear after a forward() or include().
Since the filter chain doesnt run on includes we loose the wrapper added by the container when we add no wrappers of our own.

Consider this example:

Request reaches RenderingFilter

  1. catalina.RequestFacade

Request after JspTemplateRenderer does a foward()

  1. catalina.RequestFacade
  2. catalina.DispatcherWrapper(1) <-- forward

Request after JspParagraphRenderer does an include()

  1. catalina.RequestFacade
  2. catalina.DispatcherWrapper(1) <-- forward
  3. catalina.DispatcherWrapper(2) <-- include (!! this is not in WebContextImpl.requestStack since it hasnt passed through ContextFilter)

Request after JspParagraphRenderer does another include()

  1. catalina.RequestFacade
  2. catalina.DispatcherWrapper(1) <-- forward
  3. catalina.DispatcherWrapper(3) <-- include (!! this is not in WebContextImpl.requestStack since it hasnt passed through ContextFilter)

Note how catalina.DispatcherWrapper(2) is missing in the last example, this is because the include() operation was performed on the last wrapper set by ContextFilter. Which is catalina.DispatcherWrapper(1)

Bug introduced in MAGNOLIA-2364 (WebContextImpl.include())

Bad approaches

You should not rely on the call to setRequest which the container use to change the request. Since this only works for the first applied custom wrapper, that is, the wrapper closest to the container provided wrappers.

Never store the wrapped request in a field. It is stored in your base class HttpServletRequestWrapper and you can get it from there. Since the container modified the value there you risk getting your own field out of sync.

What the spec says

These are excerpts from the spec.

SRV.6.2.2 Wrapping Requests and Responses

Central to the notion of filtering is the concept of wrapping a request or response in order that it can override behavior to perform a filtering task. In this model, the developer not only has the ability to override existing methods on the request and response objects, but to provide new API suited to a particular filtering task to a filter or target web resource down the chain. [...]

In order to support this style of filter the container must support the following requirement. When a filter invokes the doFilter method on the container's filter chain implementation, the container must ensure that the request and response object that it passes to the next entity in the filter chain, or to the target web resource if the filter was the last in the chain, is the same object that was passed into the doFilter method by the calling filter.

The same requirement of wrapper object identity applies to the calls from a servlet or a filter to RequestDispatcher.forward or RequestDispatcher.include, when the caller wraps the request or response objects. In this case, the request and response objects seen by the called servlet must be the same wrapper objects that were passed in by the calling servlet or filter.

SRV.8.2 Using a Request Dispatcher

To use a request dispatcher, a servlet calls either the include method or forward method of the RequestDispatcher interface. The parameters to these methods can be either the request and response arguments that were passed in via the service method of the javax.servlet interface, or instances of subclasses of the request and response wrapper classes that were introduced for version 2.3 of the specification. In the latter case, the wrapper instances must wrap the request or response objects that the container passed into the service method.

SRV.8.3.1 Included Request Parameters

[...] a servlet that has been invoked by another servlet using the include method of RequestDispatcher has access to the path by which it was invoked.

The following request attributes must be set:

  • javax.servlet.include.request_uri
  • javax.servlet.include.context_path
  • javax.servlet.include.servlet_path
  • javax.servlet.include.path_info
  • javax.servlet.include.query_string

These attributes are accessible from the included servlet via the getAttribute method on the request object and their values must be equal to the request URI, context path, servlet path, path info, and query string of the included servlet, respectively. If the request is subsequently included, these attributes are replaced for that include.
[...]

SRV.8.4 The Forward Method

[...]The path elements of the request object exposed to the target servlet must reflect the path used to obtain the RequestDispatcher. [...]

SRV.8.4.1 Query String

The request dispatching mechanism is responsible for aggregating query string parameters when forwarding or including requests.

SRV.8.4.2 Forwarded Request Parameters

[...] a servlet that has been invoked by another servlet using the forward method of RequestDispatcher has access to the path of the original request.

The following request attributes must be set:

  • javax.servlet.forward.request_uri
  • javax.servlet.forward.context_path
  • javax.servlet.forward.servlet_path
  • javax.servlet.forward.path_info
  • javax.servlet.forward.query_string

The values of these attributes must be equal to the return values of the HttpServletRequest methods getRequestURI, getContextPath, getServletPath, getPathInfo, getQueryString respectively, invoked on the request object passed to the first servlet object in the call chain that received the request from the client.

These attributes are accessible from the forwarded servlet via the getAttribute method on the request object. Note that these attributes must always reflect the information in the original request even under the situation that multiple forwards and subsequent includes are called.
[...]

  • No labels