Issue #4713 Async dispatch with query. (#4721)

* Issue #4713 Async dispatch with query.

+ Preserve the entire URI with query when startAsync(req,res) is used.
+ merge any query string from dispatch path with either original query or preserved query from forward

Signed-off-by: Greg Wilkins <gregw@webtide.com>

* Issue #4713 asyncDispatch with query parameters

Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2020-03-30 16:23:26 +02:00 committed by GitHub
parent fa54c74946
commit e3d670d61d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 91 deletions

View File

@ -760,15 +760,15 @@ public class HttpURI
_decodedPath = path;
}
public void setPathQuery(String path)
public void setPathQuery(String pathQuery)
{
_uri = null;
_path = null;
_decodedPath = null;
_param = null;
_fragment = null;
if (path != null)
parse(State.PATH, path);
if (pathQuery != null)
parse(State.PATH, pathQuery);
}
public void setQuery(String query)

View File

@ -25,6 +25,7 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.util.thread.Scheduler;
@ -32,6 +33,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
{
private final Context _context;
private final AsyncContextState _asyncContext;
private final HttpURI _baseURI;
private volatile HttpChannelState _state;
private ServletContext _dispatchContext;
private String _dispatchPath;
@ -39,11 +41,17 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
private Throwable _throwable;
public AsyncContextEvent(Context context, AsyncContextState asyncContext, HttpChannelState state, Request baseRequest, ServletRequest request, ServletResponse response)
{
this (context, asyncContext, state, baseRequest, request, response, null);
}
public AsyncContextEvent(Context context, AsyncContextState asyncContext, HttpChannelState state, Request baseRequest, ServletRequest request, ServletResponse response, HttpURI baseURI)
{
super(null, request, response, null);
_context = context;
_asyncContext = asyncContext;
_state = state;
_baseURI = baseURI;
// If we haven't been async dispatched before
if (baseRequest.getAttribute(AsyncContext.ASYNC_REQUEST_URI) == null)
@ -74,6 +82,11 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
}
}
public HttpURI getBaseURI()
{
return _baseURI;
}
public ServletContext getSuspendedContext()
{
return _context;
@ -94,14 +107,6 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
return _dispatchContext == null ? _context : _dispatchContext;
}
/**
* @return The path in the context (encoded with possible query string)
*/
public String getPath()
{
return _dispatchPath;
}
public void setTimeoutTask(Scheduler.Task task)
{
_timeoutTask = task;
@ -137,6 +142,14 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable
_dispatchContext = context;
}
/**
* @return The path in the context (encoded with possible query string)
*/
public String getDispatchPath()
{
return _dispatchPath;
}
/**
* @param path encoded URI
*/

View File

@ -1598,7 +1598,10 @@ public class Request implements HttpServletRequest
{
MetaData.Request metadata = _metaData;
if (metadata != null)
{
metadata.setURI(uri);
_queryParameters = null;
}
}
public UserIdentity getUserIdentity()
@ -2178,17 +2181,8 @@ public class Request implements HttpServletRequest
HttpChannelState state = getHttpChannelState();
if (_async == null)
_async = new AsyncContextState(state);
AsyncContextEvent event = new AsyncContextEvent(_context, _async, state, this, servletRequest, servletResponse);
AsyncContextEvent event = new AsyncContextEvent(_context, _async, state, this, servletRequest, servletResponse, getHttpURI());
event.setDispatchContext(getServletContext());
String uri = unwrap(servletRequest).getRequestURI();
if (_contextPath != null && uri.startsWith(_contextPath))
uri = uri.substring(_contextPath.length());
else
// TODO probably need to strip encoded context from requestURI, but will do this for now:
uri = URIUtil.encodePath(URIUtil.addPaths(getServletPath(), getPathInfo()));
event.setDispatchPath(uri);
state.startAsync(event);
return _async;
}
@ -2391,7 +2385,7 @@ public class Request implements HttpServletRequest
setQueryString(oldQuery);
else if (oldQuery == null)
setQueryString(newQuery);
else
else if (oldQueryParams.keySet().stream().anyMatch(newQueryParams.keySet()::contains))
{
// Build the new merged query string, parameters in the
// new query string hide parameters in the old query string.
@ -2413,6 +2407,10 @@ public class Request implements HttpServletRequest
}
setQueryString(mergedQuery.toString());
}
else
{
setQueryString(newQuery + '&' + oldQuery);
}
}
}

View File

@ -47,6 +47,8 @@ import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.Uptime;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
@ -563,22 +565,74 @@ public class Server extends HandlerWrapper implements Attributes
{
final HttpChannelState state = channel.getRequest().getHttpChannelState();
final AsyncContextEvent event = state.getAsyncContextEvent();
final Request baseRequest = channel.getRequest();
final String path = event.getPath();
final HttpURI baseUri = event.getBaseURI();
String encodedPathQuery = event.getDispatchPath();
if (path != null)
if (encodedPathQuery == null && baseUri == null)
{
// this is a dispatch with a path
ServletContext context = event.getServletContext();
String query = baseRequest.getQueryString();
baseRequest.setURIPathQuery(URIUtil.addEncodedPaths(context == null ? null : URIUtil.encodePath(context.getContextPath()), path));
HttpURI uri = baseRequest.getHttpURI();
baseRequest.setPathInfo(uri.getDecodedPath());
if (uri.getQuery() != null)
baseRequest.mergeQueryParameters(query, uri.getQuery(), true); //we have to assume dispatch path and query are UTF8
// Simple case, no request modification or merging needed
handleAsync(channel, event, baseRequest);
return;
}
// this is a dispatch with either a provided URI and/or a dispatched path
// We will have to modify the request and then revert
final ServletContext context = event.getServletContext();
final HttpURI oldUri = baseRequest.getHttpURI();
final String oldQuery = baseRequest.getQueryString();
final MultiMap<String> oldQueryParams = baseRequest.getQueryParameters();
try
{
baseRequest.resetParameters();
HttpURI newUri = baseUri == null ? new HttpURI(oldUri) : baseUri;
if (encodedPathQuery == null)
{
baseRequest.setHttpURI(newUri);
}
else
{
if (context != null && !StringUtil.isEmpty(context.getContextPath()))
encodedPathQuery = URIUtil.addEncodedPaths(URIUtil.encodePath(context.getContextPath()), encodedPathQuery);
if (newUri.getQuery() == null)
{
// parse new path and query
newUri.setPathQuery(encodedPathQuery);
baseRequest.setHttpURI(newUri);
}
else
{
// do we have a new query in the encodedPathQuery
int q = encodedPathQuery.indexOf('?');
if (q < 0)
{
// No query, so we can just set the encoded path
newUri.setPath(encodedPathQuery);
baseRequest.setHttpURI(newUri);
}
else
{
newUri.setPath(encodedPathQuery.substring(0, q));
baseRequest.setHttpURI(newUri);
baseRequest.mergeQueryParameters(oldQuery, encodedPathQuery.substring(q + 1), true);
}
}
}
baseRequest.setPathInfo(newUri.getDecodedPath());
handleAsync(channel, event, baseRequest);
}
finally
{
baseRequest.setHttpURI(oldUri);
baseRequest.setQueryParameters(oldQueryParams);
baseRequest.resetParameters();
}
}
private void handleAsync(HttpChannel channel, AsyncContextEvent event, Request baseRequest) throws IOException, ServletException
{
final String target = baseRequest.getPathInfo();
final HttpServletRequest request = Request.unwrap(event.getSuppliedRequest());
final HttpServletResponse response = Response.unwrap(event.getSuppliedResponse());

View File

@ -163,7 +163,7 @@ public class AsyncServletTest
String response = process("sleep=200", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?sleep=200",
"initial"));
assertContains("SLEPT", response);
assertFalse(__history.contains("onTimeout"));
@ -173,7 +173,7 @@ public class AsyncServletTest
@Test
public void testNonAsync() throws Exception
{
String response = process("", null);
String response = process(null, null);
assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
@ -186,7 +186,7 @@ public class AsyncServletTest
public void testAsyncNotSupportedNoAsync() throws Exception
{
_expectedCode = "200 ";
String response = process("noasync", "", null);
String response = process("noasync", null, null);
assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/noasync/info",
@ -205,9 +205,9 @@ public class AsyncServletTest
String response = process("noasync", "start=200", null);
assertThat(response, Matchers.startsWith("HTTP/1.1 500 "));
assertThat(__history, contains(
"REQUEST /ctx/noasync/info",
"REQUEST /ctx/noasync/info?start=200",
"initial",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=200",
"!initial"
));
@ -224,11 +224,11 @@ public class AsyncServletTest
String response = process("start=200", null);
assertThat(response, Matchers.startsWith("HTTP/1.1 500 Server Error"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200",
"initial",
"start",
"onTimeout",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=200",
"!initial",
"onComplete"));
@ -241,12 +241,12 @@ public class AsyncServletTest
String response = process("start=200&timeout=dispatch", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&timeout=dispatch",
"initial",
"start",
"onTimeout",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=200&timeout=dispatch",
"!initial",
"onComplete"));
@ -260,12 +260,12 @@ public class AsyncServletTest
String response = process("start=200&timeout=error", null);
assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&timeout=error",
"initial",
"start",
"onTimeout",
"error",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=200&timeout=error",
"!initial",
"onComplete"));
@ -278,7 +278,7 @@ public class AsyncServletTest
String response = process("start=200&timeout=complete", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&timeout=complete",
"initial",
"start",
"onTimeout",
@ -294,11 +294,11 @@ public class AsyncServletTest
String response = process("start=200&dispatch=10", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&dispatch=10",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=200&dispatch=10",
"!initial",
"onComplete"));
assertFalse(__history.contains("onTimeout"));
@ -310,11 +310,11 @@ public class AsyncServletTest
String response = process("start=200&dispatch=0", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&dispatch=0",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=200&dispatch=0",
"!initial",
"onComplete"));
}
@ -326,11 +326,11 @@ public class AsyncServletTest
String response = process("start=200&throw=1", null);
assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&throw=1",
"initial",
"start",
"onError",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=200&throw=1",
"!initial",
"onComplete"));
assertContains("ERROR DISPATCH: /ctx/error/custom", response);
@ -342,7 +342,7 @@ public class AsyncServletTest
String response = process("start=200&complete=50", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&complete=50",
"initial",
"start",
"complete",
@ -358,7 +358,7 @@ public class AsyncServletTest
String response = process("start=200&complete=0", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&complete=0",
"initial",
"start",
"complete",
@ -374,16 +374,16 @@ public class AsyncServletTest
String response = process("start=1000&dispatch=10&start2=1000&dispatch2=10", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=1000&dispatch=10&start2=1000&dispatch2=10",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=1000&dispatch=10&start2=1000&dispatch2=10",
"!initial",
"onStartAsync",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=1000&dispatch=10&start2=1000&dispatch2=10",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
@ -395,11 +395,11 @@ public class AsyncServletTest
String response = process("start=1000&dispatch=10&start2=1000&complete2=10", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=1000&dispatch=10&start2=1000&complete2=10",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=1000&dispatch=10&start2=1000&complete2=10",
"!initial",
"onStartAsync",
"start",
@ -415,16 +415,16 @@ public class AsyncServletTest
String response = process("start=1000&dispatch=10&start2=10", null);
assertThat(response, startsWith("HTTP/1.1 500 Server Error"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=1000&dispatch=10&start2=10",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=1000&dispatch=10&start2=10",
"!initial",
"onStartAsync",
"start",
"onTimeout",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=1000&dispatch=10&start2=10",
"!initial",
"onComplete"));
assertContains("ERROR DISPATCH: /ctx/error/custom", response);
@ -436,16 +436,16 @@ public class AsyncServletTest
String response = process("start=10&start2=1000&dispatch2=10", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=10&start2=1000&dispatch2=10",
"initial",
"start",
"onTimeout",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=10&start2=1000&dispatch2=10",
"!initial",
"onStartAsync",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=10&start2=1000&dispatch2=10",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
@ -457,11 +457,11 @@ public class AsyncServletTest
String response = process("start=10&start2=1000&complete2=10", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=10&start2=1000&complete2=10",
"initial",
"start",
"onTimeout",
"ERROR /ctx/error/custom",
"ERROR /ctx/error/custom?start=10&start2=1000&complete2=10",
"!initial",
"onStartAsync",
"start",
@ -478,16 +478,16 @@ public class AsyncServletTest
String response = process("start=10&start2=10", null);
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=10&start2=10",
"initial",
"start",
"onTimeout",
"ERROR /ctx/path/error",
"ERROR /ctx/path/error?start=10&start2=10",
"!initial",
"onStartAsync",
"start",
"onTimeout",
"ERROR /ctx/path/error",
"ERROR /ctx/path/error?start=10&start2=10",
"!initial",
"onComplete")); // Error Page Loop!
assertContains("AsyncContext timeout", response);
@ -499,11 +499,11 @@ public class AsyncServletTest
String response = process("wrap=true&start=200&dispatch=20", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?wrap=true&start=200&dispatch=20",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?wrap=true&start=200&dispatch=20",
"wrapped REQ RSP",
"!initial",
"onComplete"));
@ -516,11 +516,11 @@ public class AsyncServletTest
String response = process("start=200&dispatch=20&path=/p%20th3", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=200&dispatch=20&path=/p%20th3",
"initial",
"start",
"dispatch",
"ASYNC /ctx/p%20th3",
"ASYNC /ctx/p%20th3?start=200&dispatch=20&path=/p%20th3",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
@ -532,13 +532,13 @@ public class AsyncServletTest
String response = process("fwd", "start=200&dispatch=20", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"FWD REQUEST /ctx/fwd/info",
"FORWARD /ctx/path1",
"FWD REQUEST /ctx/fwd/info?start=200&dispatch=20",
"FORWARD /ctx/path1?forward=true&start=200&dispatch=20",
"initial",
"start",
"dispatch",
"FWD ASYNC /ctx/fwd/info",
"FORWARD /ctx/path1",
"FWD ASYNC /ctx/fwd/info?start=200&dispatch=20",
"FORWARD /ctx/path1?forward=true&start=200&dispatch=20",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
@ -550,12 +550,12 @@ public class AsyncServletTest
String response = process("fwd", "start=200&dispatch=20&path=/path2", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"FWD REQUEST /ctx/fwd/info",
"FORWARD /ctx/path1",
"FWD REQUEST /ctx/fwd/info?start=200&dispatch=20&path=/path2",
"FORWARD /ctx/path1?forward=true&start=200&dispatch=20&path=/path2",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path2",
"ASYNC /ctx/path2?start=200&dispatch=20&path=/path2",
"!initial",
"onComplete"));
assertContains("DISPATCHED", response);
@ -567,12 +567,12 @@ public class AsyncServletTest
String response = process("fwd", "wrap=true&start=200&dispatch=20", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"FWD REQUEST /ctx/fwd/info",
"FORWARD /ctx/path1",
"FWD REQUEST /ctx/fwd/info?wrap=true&start=200&dispatch=20",
"FORWARD /ctx/path1?forward=true&wrap=true&start=200&dispatch=20",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path1",
"ASYNC /ctx/path1?forward=true&wrap=true&start=200&dispatch=20",
"wrapped REQ RSP",
"!initial",
"onComplete"));
@ -585,12 +585,12 @@ public class AsyncServletTest
String response = process("fwd", "wrap=true&start=200&dispatch=20&path=/path2", null);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"FWD REQUEST /ctx/fwd/info",
"FORWARD /ctx/path1",
"FWD REQUEST /ctx/fwd/info?wrap=true&start=200&dispatch=20&path=/path2",
"FORWARD /ctx/path1?forward=true&wrap=true&start=200&dispatch=20&path=/path2",
"initial",
"start",
"dispatch",
"ASYNC /ctx/path2",
"ASYNC /ctx/path2?forward=true&wrap=true&start=200&dispatch=20&path=/path2",
"wrapped REQ RSP",
"!initial",
"onComplete"));
@ -619,12 +619,12 @@ public class AsyncServletTest
__latch.await(1, TimeUnit.SECONDS);
assertThat(response, startsWith("HTTP/1.1 200 OK"));
assertThat(__history, contains(
"REQUEST /ctx/path/info",
"REQUEST /ctx/path/info?start=2000&dispatch=1500",
"initial",
"start",
"async-read=10",
"dispatch",
"ASYNC /ctx/path/info",
"ASYNC /ctx/path/info?start=2000&dispatch=1500",
"!initial",
"onComplete"));
}
@ -685,10 +685,10 @@ public class AsyncServletTest
@Override
public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
{
historyAdd("FWD " + request.getDispatcherType() + " " + request.getRequestURI());
historyAdd("FWD " + request.getDispatcherType() + " " + URIUtil.addPathQuery(request.getRequestURI(), request.getQueryString()));
if (request instanceof ServletRequestWrapper || response instanceof ServletResponseWrapper)
historyAdd("wrapped" + ((request instanceof ServletRequestWrapper) ? " REQ" : "") + ((response instanceof ServletResponseWrapper) ? " RSP" : ""));
request.getServletContext().getRequestDispatcher("/path1").forward(request, response);
request.getServletContext().getRequestDispatcher("/path1?forward=true").forward(request, response);
}
}
@ -711,7 +711,7 @@ public class AsyncServletTest
// ignored
}
historyAdd(request.getDispatcherType() + " " + request.getRequestURI());
historyAdd(request.getDispatcherType() + " " + URIUtil.addPathQuery(request.getRequestURI(),request.getQueryString()));
if (request instanceof ServletRequestWrapper || response instanceof ServletResponseWrapper)
historyAdd("wrapped" + ((request instanceof ServletRequestWrapper) ? " REQ" : "") + ((response instanceof ServletResponseWrapper) ? " RSP" : ""));

View File

@ -714,6 +714,20 @@ public class URIUtil
return buf.toString();
}
/** Add a path and a query string
* @param path The path which may already contain contain a query
* @param query The query string or null if no query to be added
* @return The path with any non null query added after a '?' or '&amp;' as appropriate.
*/
public static String addPathQuery(String path, String query)
{
if (query == null)
return path;
if (path.indexOf('?') >= 0)
return path + '&' + query;
return path + '?' + query;
}
/**
* Given a URI, attempt to get the last segment.
* <p>