jsr369-experts@servlet-spec.java.net

[jsr369-experts] Re: [servlet-spec users] Call for latest Push proposal

From: Greg Wilkins <gregw_at_webtide.com>
Date: Wed, 12 Aug 2015 09:16:19 +1000

Ed et al,

We've not done a lot of work or analysis on this recently.... however it is
included in our stable release and here is the API that we are currently
providing:

https://github.com/eclipse/jetty.project/blob/master/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java

and here is a filter that uses it:

https://github.com/eclipse/jetty.project/blob/master/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushSessionCacheFilter.java

However, while it is being used, I would not characterize this as
extensively tested or analysed. To my knowledge nobody has used the API
directly in a framework, but some have deployed the push filter in their
applications.


Note that while I like the idea of the simple degenerate case, my limited
experience is that building the synthetic request is actually a lot more
complex than you'd think and there are lots of corner/edge cases. Even the
PushBuilder needs to synthesize/validate headers and the rules for that
will need to be carefully crafted and explained to achieve portability.

cheers







On 11 August 2015 at 07:40, Edward Burns <edward.burns_at_oracle.com> wrote:

> Hello Volunteers,
>
> Can I ask Greg to share again the latest incarnation of his HTTP/2
> Server Push proposal? What I presented at GeekOut was a two-pronged
> approach [1]:
>
> 1. A builder API as Greg proposed
>
> 2. A simple requestDispatcher call that does the "synthetic request"
> thing, making it trivially easier for frameworks such as JSF to
> leverage.
>
> I would really like to have 2 be available as a degenerate case of 1,
> but I'd like to see Greg's latest builder proposal. I've included below
> the most recent one I could find but it's very old already and I'm
> pretty sure Greg has evolved Jetty beyond that.
>
> If there's no response, I'll just try to carry this one forward.
>
> Thanks,
>
> Ed
>
> --
> | edward.burns_at_oracle.com | office: +1 407 458 0017
> | 61 Business days til JavaOne 2015
> | 76 Business days til DOAG 2015
>
>
> [1] https://vimeo.com/131394612
>
> >>>>> On Fri, 5 Dec 2014 11:52:01 +0100, Greg Wilkins <gregw_at_intalio.com>
> said:
>
> GW> I've been working with Push a little bit more and have an alternative
> GW> proposal for the API.
>
> GW> I've started work on this, but thought I'd seek some feedback before I
> GW> commit too much effort. The idea is that often there will be many
> GW> resources to push and that a lot of the work needed to work out
> headers,
> GW> sessions and cookies is common to all the pushes. So we need a new
> GW> PushBuilder instance that does all this work once. We would obtain a
> GW> PushBuilder from a new method on the request:
>
> GW> /** Get a PushBuilder associated with this request initialized as
> GW> follows:<ul>
> GW> * <li>The method is initialized to "GET"</li>
> GW> * <li>The headers from this request are copied to the Builder,
> except
> GW> for:<ul>
> GW> * <li>Conditional headers (eg. If-Modified-Since)
> GW> * <li>Range headers
> GW> * <li>Expect headers
> GW> * <li>Authentication headers
> GW> * <li>Referrer headers
> GW> * </ul></li>
> GW> * <li>If the request was Authenticated, an Authentication header
> will
> GW> * be set with a container generated token that will result in
> GW> equivalent
> GW> * authentication</li>
> GW> * <li>The query string from {_at_link #getQueryString()}
> GW> * <li>The {_at_link #getRequestedSessionId()} value, unless at the
> time
> GW> * of the call {_at_link #getSession(boolean)}
> GW> * has previously been called to create a new {_at_link HttpSession},
> in
> GW> * which case the new session ID will be used as the PushBuilders
> GW> * requested session ID.</li>
> GW> * <li>The source of the requested session id will be the same as
> for
> GW> * this request</li>
> GW> * <li>The builders Referer header will be set to {_at_link
> GW> #getRequestURL()}
> GW> * plus any {_at_link #getQueryString()} </li>
> GW> * <li>If {_at_link HttpServletResponse#addCookie(Cookie)} has been
> called
> GW> * on the associated response, then a corresponding Cookie header
> will
> GW> be added
> GW> * to the PushBuilder, unless the {_at_link Cookie#getMaxAge()} is
> <=0, in
> GW> which
> GW> * case the Cookie will be removed from the builder.</li>
> GW> * <li>If this request has has the conditional headers
> GW> If-Modified-Since or
> GW> * If-None-Match then the {_at_link PushBuilder#isConditional()}
> header is
> GW> set
> GW> * to true.
> GW> * </ul>
> GW> *
> GW> * <p>Each call to getPushBuilder() will return a new instance
> GW> * of a PushBuilder based off this Request. Any mutations to the
> GW> * returned PushBuilder are not reflected on future returns.
> GW> * @return A new PushBuilder or null if push is not supported
> GW> */
> GW> public PushBuilder getPushBuilder()
> GW> {
> GW> ...
> GW> }
>
> GW> The javadoc for that method expands as
>
> GW> Get a PushBuilder associated with this request initialized as follows:
>
> GW> - The method is initialized to "GET"
> GW> - The headers from this request are copied to the Builder, except
> for:
> GW> - Conditional headers (eg. If-Modified-Since)
> GW> - Range headers
> GW> - Expect headers
> GW> - Authentication headers
> GW> - Referrer headers
> GW> - If the request was Authenticated, an Authentication header will
> be set
> GW> with a container generated token that will result in equivalent
> GW> authentication
> GW> - The query string from getQueryString()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82%E2%98%82getQueryString%E2%98%82>
> GW> - The getRequestedSessionId()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82%E2%98%82getRequestedSessionId%E2%98%82>
> GW> value, unless at the time of the call getSession(boolean)
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82%E2%98%82getSession%E2%98%82boolean>
> GW> has previously been called to create a new HttpSession
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82HttpSession>,
> GW> in which case the new session ID will be used as the PushBuilders
> requested
> GW> session ID.
> GW> - The source of the requested session id will be the same as for
> this
> GW> request
> GW> - The builders Referer header will be set to getRequestURL()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82%E2%98%82getRequestURL%E2%98%82>
> GW> plus any getQueryString()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82%E2%98%82getQueryString%E2%98%82>
> GW> - If HttpServletResponse.addCookie(Cookie)
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82HttpServletResponse%E2%98%82addCookie%E2%98%82Cookie>
> GW> has been called on the associated response, then a corresponding
> Cookie
> GW> header will be added to the PushBuilder, unless the
> Cookie.getMaxAge()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82Cookie%E2%98%82getMaxAge%E2%98%82>
> GW> is <=0, in which case the Cookie will be removed from the builder.
> GW> - If this request has has the conditional headers If-Modified-Since
> or
> GW> If-None-Match then the PushBuilder.isConditional()
> GW>
> <eclipse-javadoc:%E2%98%82=jetty-server/src%5C/main%5C/java%3Corg.eclipse.jetty.server%7BRequest.java%E2%98%83Request~getPushBuilder%E2%98%82PushBuilder%E2%98%82isConditional%E2%98%82>
> GW> header is set to true.
>
> GW> Each call to getPushBuilder() will return a new instance of a
> PushBuilder
> GW> based off this Request. Any mutations to the returned PushBuilder are
> not
> GW> reflected on future returns.
> GW> The PushBuilder itself allows most of the initialisation to be
> overridden
> GW> if need be, but hopefully it wont:
>
> GW> public interface PushBuilder
> GW> {
> GW> public String getMethod();
> GW> public void setMethod(String method);
>
> GW> public Enumeration<String> getHeaderNames();
> GW> public String getHeader(String name);
> GW> public void setHeader(String name,String value);
>
> GW> public String getQueryString();
> GW> public void setQueryString(String query);
>
> GW> public boolean isRequestedSessionIdValid();
> GW> public boolean isRequestedSessionIdFromCookie();
> GW> public boolean isRequestedSessionIdFromURL();
> GW> public String getRequestedSessionId();
>
> GW> public void setRequestedSessionId(String id);
>
> GW> public Cookie[] getCookies();
> GW> public void removeCookie(String name);
> GW> public void addCookie(Cookie cookie);
>
> GW> public boolean isConditional();
> GW> public void setConditional(boolean conditional);
>
> GW> /* ------------------------------------------------------------ */
> GW> /** Push a resource.
> GW> * Push a resource based on the current state of the PushBuilder.
> If
> GW> {_at_link #isConditional()}
> GW> * is true and an etag or lastModified value is provided, then an
> GW> appropriate conditional header
> GW> * will be generated. If an etag and lastModified value are
> provided
> GW> only an If-None-Match header
> GW> * will be generated. If the builder has a session ID, then the
> pushed
> GW> request
> GW> * will include the session ID either as a Cookie or as a URI
> parameter
> GW> as appropriate. The builders
> GW> * query string is merged with any passed query string.
> GW> * @param uriInContext The URI within the current context of the
> GW> resource to push.
> GW> * @param etag The etag for the resource or null if not available
> GW> * @param lastModified The last modified date of the resource or
> null
> GW> if not available
> GW> * @throws IllegalArgumentException if the method set expects a
> request
> GW> * body (eg POST)
> GW> */
> GW> void push(String uriInContext,String etag,long lastModified);
> GW> }
>
> GW> So normal usage of the push builder should be pretty simple, but it is
> GW> flexible if need be. The key aspect of the API is that it separates
> GW> out the two phases of customising push request: firstly the
> customisation
> GW> for session, authentication etc that are common to all pushed
> GW> requests; secondly each pushed request can be customised with
> conditional
> GW> headers, session URI parameters and query string.
>
> GW> Here is an example usage in Jetty's PushCacheFilter:
>
> GW> // Are there associated resources to push?
> GW> if (!primaryResource.getAssociated().isEmpty())
> GW> {
> GW> // Let's make sure we push them to a valid session
> GW> request.getSession(true);
>
> GW> // Is push actually supported?
> GW> PushBuilder builder = baseRequest.getPushBuilder();
> GW> if (builder!=null)
> GW> {
> GW> // Yes, lets set a header as a demo
> GW> builder.setHeader("Via","Push Example");
>
> GW> // If query strings are not relevant, then clear
> GW> if (!isPreservingQueries())
> GW> builder.setQueryString(null);
>
> GW> // Push each resource
> GW> for (AssociatedResource resource :
> GW> primaryResource.getAssociated())
> GW> {
>
> GW>
> builder.push(resource.getURI(),resource.getETag(),resource.getLastModified());
> GW> }
> GW> }
> GW> }
>
> GW> Thoughts?
>



-- 
Greg Wilkins <gregw@webtide.com> CTO http://webtide.com