diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index 894461c8fda0..a322a7619aef 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -320,7 +320,7 @@ private AfterAuthenticationListener(Authentication.Result authenticationResult) public void onSuccess(Response response) { int status = response.getStatus(); - if (HttpStatus.isSuccess(status) || HttpStatus.isRedirection(status)) + if (HttpStatus.isSuccess(status) || HttpStatus.isRedirectionWithLocation(status)) client.getAuthenticationStore().addAuthenticationResult(authenticationResult); } } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 71ed7ac544be..01c1fbf298cd 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -372,7 +372,7 @@ public static boolean isSuccess(int code) } /** - * Simple test against an code to determine if it falls into the + * Simple test against a code to determine if it falls into the * Redirection message category as defined in the RFC 1945 - HTTP/1.0, and RFC 7231 - HTTP/1.1. @@ -386,6 +386,22 @@ public static boolean isRedirection(int code) return ((300 <= code) && (code <= 399)); } + /** + * Simple test against a code to determine if it falls into the + * Redirection message category as defined in the RFC 1945 - HTTP/1.0, and RFC 7231 - HTTP/1.1; and + * is a code that can requires a location (i.e. not 304). + * + * @param code the code to test. + * @return true if within range of codes that belongs to + * Redirection messages and not a {@code 304} + */ + public static boolean isRedirectionWithLocation(int code) + { + return isRedirection(code) && code != 304; + } + /** * Simple test against an code to determine if it falls into the * Client Error message category as defined in the + diff --git a/jetty-core/jetty-server/src/main/config/modules/server.mod b/jetty-core/jetty-server/src/main/config/modules/server.mod index 2163a0938b44..42166f77e001 100644 --- a/jetty-core/jetty-server/src/main/config/modules/server.mod +++ b/jetty-core/jetty-server/src/main/config/modules/server.mod @@ -65,6 +65,9 @@ etc/jetty.xml ## Relative Redirect Locations allowed # jetty.httpConfig.relativeRedirectAllowed=true +## Redirect body generated +# jetty.httpConfig.generateRedirectBody=false + ## Whether to use direct ByteBuffers for reading or writing # jetty.httpConfig.useInputDirectByteBuffers=true # jetty.httpConfig.useOutputDirectByteBuffers=true diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java index ef4503c5b7af..771f8c427f9d 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionMetaData.java @@ -52,7 +52,9 @@ default boolean isSecure() /** * @return whether the functionality of pushing resources is supported + * @deprecated in favour of 103 Early Hints */ + @Deprecated(since = "12.0.1") default boolean isPushSupported() { return false; diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 69d9396470e2..c331c1a3f6ad 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -84,6 +84,7 @@ public class HttpConfiguration implements Dumpable private MultiPartCompliance _multiPartCompliance = MultiPartCompliance.RFC7578; private boolean _notifyRemoteAsyncErrors = true; private boolean _relativeRedirectAllowed = true; + private boolean _generateRedirectBody = false; private HostPort _serverAuthority; private SocketAddress _localAddress; private int _maxUnconsumedRequestContentReads = 16; @@ -158,6 +159,7 @@ public HttpConfiguration(HttpConfiguration config) _complianceViolationListeners.addAll(config._complianceViolationListeners); _notifyRemoteAsyncErrors = config._notifyRemoteAsyncErrors; _relativeRedirectAllowed = config._relativeRedirectAllowed; + _generateRedirectBody = config._generateRedirectBody; _uriCompliance = config._uriCompliance; _serverAuthority = config._serverAuthority; _localAddress = config._localAddress; @@ -716,6 +718,23 @@ public boolean isRelativeRedirectAllowed() return _relativeRedirectAllowed; } + /** + * @param generate True if a redirection body will be generated if no response body is supplied. + */ + public void setGenerateRedirectBody(boolean generate) + { + _generateRedirectBody = generate; + } + + /** + * @return True if a redirection body will be generated if no response body is supplied. + */ + @ManagedAttribute("Whether a redirection response body will be generated") + public boolean isGenerateRedirectBody() + { + return _generateRedirectBody; + } + /** * Get the SocketAddress override to be reported as the local address of all connections * diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 4e8b26a322ab..ec92395dabd5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -15,6 +15,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ListIterator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; @@ -32,12 +33,14 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.internal.HttpChannelState; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; @@ -262,10 +265,7 @@ static void sendRedirect(Request request, Response response, Callback callback, */ static void sendRedirect(Request request, Response response, Callback callback, String location, boolean consumeAvailable) { - int code = HttpMethod.GET.is(request.getMethod()) || request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() - ? HttpStatus.MOVED_TEMPORARILY_302 - : HttpStatus.SEE_OTHER_303; - sendRedirect(request, response, callback, code, location, consumeAvailable); + sendRedirect(request, response, callback, 0, location, consumeAvailable); } /** @@ -283,23 +283,47 @@ static void sendRedirect(Request request, Response response, Callback callback, */ static void sendRedirect(Request request, Response response, Callback callback, int code, String location, boolean consumeAvailable) { - if (!HttpStatus.isRedirection(code)) + sendRedirect(request, response, callback, code, location, consumeAvailable, null); + } + + /** + *

Sends a {@code 302} HTTP redirect status code to the given location.

+ * + * @param request the HTTP request + * @param response the HTTP response + * @param callback the callback to complete + * @param code the redirect HTTP status code, or 0 for a default + * @param location the redirect location as an absolute URI or encoded relative URI path. + * @param consumeAvailable whether to consumer the available request content + * @param content the content of the response, or null for a generated HTML message if {@link HttpConfiguration#isGenerateRedirectBody()} is {@code true}. + * @see #toRedirectURI(Request, String) + * @throws IllegalArgumentException if the status code is not a redirect, or the location is {@code null} + * @throws IllegalStateException if the response is already {@link #isCommitted() committed} + */ + static void sendRedirect(Request request, Response response, Callback callback, int code, String location, boolean consumeAvailable, ByteBuffer content) + { + if (response.isCommitted()) { - callback.failed(new IllegalArgumentException("Not a 3xx redirect code")); + callback.failed(new IllegalStateException("Committed")); return; } - if (location == null) + if (code <= 0) + code = HttpMethod.GET.is(request.getMethod()) || request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() + ? HttpStatus.MOVED_TEMPORARILY_302 + : HttpStatus.SEE_OTHER_303; + if (!HttpStatus.isRedirectionWithLocation(code)) { - callback.failed(new IllegalArgumentException("No location")); + callback.failed(new IllegalArgumentException("Not a 3xx redirect code")); return; } - if (response.isCommitted()) + if (location == null) { - callback.failed(new IllegalStateException("Committed")); + callback.failed(new IllegalArgumentException("No location")); return; } + location = toRedirectURI(request, location); if (consumeAvailable) { @@ -317,9 +341,22 @@ static void sendRedirect(Request request, Response response, Callback callback, } } - response.getHeaders().put(HttpHeader.LOCATION, toRedirectURI(request, location)); + if (content == null && request.getConnectionMetaData().getHttpConfiguration().isGenerateRedirectBody()) + { + response.getHeaders().put(MimeTypes.Type.TEXT_HTML_8859_1.getContentTypeField()); + String body = """ + + + Redirecting... +

If you are not redirected, click here.

+ + """.formatted(location, location); + content = BufferUtil.toBuffer(body, StandardCharsets.ISO_8859_1); + } + + response.getHeaders().put(HttpHeader.LOCATION, location); response.setStatus(code); - response.write(true, null, callback); + response.write(true, content, callback); } /** diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java index 1ced4688d603..e320b63ec3ee 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java @@ -119,7 +119,7 @@ protected boolean shouldBuffer(Response response, boolean last) return false; int status = response.getStatus(); - if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirection(status)) + if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirectionWithLocation(status)) return false; String ct = response.getHeaders().get(HttpHeader.CONTENT_TYPE); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java index 3953334cb143..419136bc74b1 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java @@ -56,7 +56,7 @@ public int getStatusCode() */ public void setStatusCode(int statusCode) { - if (!HttpStatus.isRedirection(statusCode)) + if (!HttpStatus.isRedirectionWithLocation(statusCode)) throw new IllegalArgumentException("Invalid HTTP redirection status code: " + statusCode); _statusCode = statusCode; } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java index 10e9b4c3c062..d8b34c76ee36 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/SecuredRedirectHandler.java @@ -79,7 +79,7 @@ public SecuredRedirectHandler(Handler handler) public SecuredRedirectHandler(Handler handler, int code) { super(handler); - if (!HttpStatus.isRedirection(code)) + if (!HttpStatus.isRedirectionWithLocation(code)) throw new IllegalArgumentException("Not a 3xx redirect code"); _redirectCode = code; } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index 15484e0409a9..7f1a91586ab8 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -13,9 +13,13 @@ package org.eclipse.jetty.server; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.stream.Stream; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -23,18 +27,29 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.SetCookieParser; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.LifeCycle; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -163,6 +178,112 @@ public boolean handle(Request request, Response response, Callback callback) assertThat(response.get("Test"), is("after reset")); } + public static Stream redirects() + { + List cases = new ArrayList<>(); + + for (int code : new int[] {0, 307}) + { + for (String location : new String[] {"somewhere/else", "/somewhere/else", "http://else/where" }) + { + for (boolean relative : new boolean[] {true, false}) + { + for (boolean generate : new boolean[] {true, false}) + { + for (String content : new String[] {null, "alternative text" }) + { + cases.add(Arguments.of(code, location, relative, generate, content)); + } + } + } + } + } + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("redirects") + public void testRedirect(int code, String location, boolean relative, boolean generate, String content) throws Exception + { + server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class) + .getHttpConfiguration().setRelativeRedirectAllowed(relative); + server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class) + .getHttpConfiguration().setGenerateRedirectBody(generate); + + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + if (content == null) + { + Response.sendRedirect(request, response, callback, code, location, true); + } + else + { + response.getHeaders().put(MimeTypes.Type.TEXT_PLAIN_UTF_8.getContentTypeField()); + Response.sendRedirect(request, response, callback, code, location, true, BufferUtil.toBuffer(content, StandardCharsets.UTF_8)); + } + + return true; + } + }); + server.start(); + + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/ctx/servlet/test"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = connector.getResponse(request.generate()); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + assertThat(response.getStatus(), is(code == 0 ? HttpStatus.FOUND_302 : code)); + + String destination = location; + if (relative) + { + if (!location.startsWith("/") && !location.startsWith("http:/")) + destination = "/ctx/servlet/" + location; + } + else + { + if (location.startsWith("/")) + destination = "http://test" + location; + else if (!location.startsWith("http:/")) + destination = "http://test/ctx/servlet/" + location; + } + + HttpField to = response.getField(HttpHeader.LOCATION); + assertThat(to, notNullValue()); + assertThat(to.getValue(), is(destination)); + + String actual = response.getContent(); + + if (content == null) + { + if (generate) + { + assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html")); + assertThat(actual, containsString("If you are not redirected, click here".formatted(destination))); + assertThat(actual, not(containsString("oops"))); + } + else + { + assertThat(response.get().get(HttpHeader.CONTENT_TYPE), nullValue()); + assertThat(actual, emptyString()); + } + } + else + { + assertThat(response.get().get(HttpHeader.CONTENT_TYPE), notNullValue()); + assertThat(actual, not(containsString("oops"))); + assertThat(actual, containsString(content)); + } + } + @Test public void testRedirectGET() throws Exception { @@ -199,6 +320,67 @@ public boolean handle(Request request, Response response, Callback callback) assertThat(response.get(HttpHeader.LOCATION), is("http://hostname/somewhere/else")); } + @Test + public void testRedirectGetWithContent() throws Exception + { + server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class) + .getHttpConfiguration().setRelativeRedirectAllowed(false); + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "exotic"); + Response.sendRedirect(request, response, callback, 0, "/somewhere/else", false, + BufferUtil.toBuffer("something special")); + return true; + } + }); + server.start(); + + String request = """ + GET /path HTTP/1.0\r + Host: hostname\r + \r + """; + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); + assertEquals(HttpStatus.MOVED_TEMPORARILY_302, response.getStatus()); + assertThat(response.get(HttpHeader.LOCATION), is("http://hostname/somewhere/else")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), is("exotic")); + assertThat(response.getContent(), containsString("something special")); + } + + @Test + public void testRedirectGetWithGeneratedContent() throws Exception + { + server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class) + .getHttpConfiguration().setRelativeRedirectAllowed(true); + server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class) + .getHttpConfiguration().setGenerateRedirectBody(true); + + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Response.sendRedirect(request, response, callback, 0, "/somewhere/else", false, null); + return true; + } + }); + server.start(); + + String request = """ + GET /path HTTP/1.0\r + Host: hostname\r + \r + """; + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); + assertEquals(HttpStatus.MOVED_TEMPORARILY_302, response.getStatus()); + assertThat(response.get(HttpHeader.LOCATION), is("/somewhere/else")); + assertThat(response.get(HttpHeader.CONTENT_TYPE), equalToIgnoringCase(MimeTypes.Type.TEXT_HTML_8859_1.asString())); + assertThat(response.getContent(), containsString("")); + } + @Test public void testEncodedRedirectGET() throws Exception { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java index 16896fd65b07..ce35fd536a58 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java @@ -23,6 +23,8 @@ import java.util.function.Supplier; import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletRequestInfo; @@ -621,4 +623,119 @@ public String toString() return HttpCookie.toString(this); } } + + /** + * Servlet API wrapper for cross context included responses. + * It prevents the headers or response code from being updated. + * @see jakarta.servlet.RequestDispatcher#include(ServletRequest, ServletResponse) + */ + public static class CrossContextInclude extends ServletApiResponse + { + public CrossContextInclude(ServletContextResponse servletContextResponse) + { + super(servletContextResponse); + } + + @Override + public void setCharacterEncoding(String charset) + { + // NOOP for include. + } + + @Override + public void setLocale(Locale loc) + { + // NOOP for include. + } + + @Override + public void setContentLength(int len) + { + // NOOP for include. + } + + @Override + public void setContentLengthLong(long len) + { + // NOOP for include. + } + + @Override + public void setContentType(String type) + { + // NOOP for include. + } + + @Override + public void reset() + { + // NOOP for include. + } + + @Override + public void resetBuffer() + { + // NOOP for include. + } + + @Override + public void setDateHeader(String name, long date) + { + // NOOP for include. + } + + @Override + public void addDateHeader(String name, long date) + { + // NOOP for include. + } + + @Override + public void setHeader(String name, String value) + { + // NOOP for include. + } + + @Override + public void addHeader(String name, String value) + { + // NOOP for include. + } + + @Override + public void setIntHeader(String name, int value) + { + // NOOP for include. + } + + @Override + public void addIntHeader(String name, int value) + { + // NOOP for include. + } + + @Override + public void setStatus(int sc) + { + // NOOP for include. + } + + @Override + public void sendError(int sc, String msg) throws IOException + { + // NOOP for include. + } + + @Override + public void sendError(int sc) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location) throws IOException + { + // NOOP for include. + } + } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java index aa7a48fb89f1..481612154127 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextRequest.java @@ -130,8 +130,6 @@ protected ServletContextRequest( _httpInput = _servletChannel.getHttpInput(); _decodedPathInContext = decodedPathInContext; _sessionManager = sessionManager; - _servletApiRequest = newServletApiRequest(); - _response = newServletContextResponse(response); _attributes = new Attributes.Synthetic(request) { @Override @@ -156,6 +154,8 @@ protected Set getSyntheticNameSet() return ATTRIBUTES; } }; + _servletApiRequest = newServletApiRequest(); + _response = newServletContextResponse(response); addIdleTimeoutListener(_servletChannel.getServletRequestState()::onIdleTimeout); } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java index 2ff380037196..975bc2933602 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.function.Supplier; +import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletResponse; import jakarta.servlet.ServletResponseWrapper; @@ -174,6 +175,9 @@ protected ServletContextRequest getServletContextRequest() protected ServletApiResponse newServletApiResponse() { + if (_servletChannel.getServletContextHandler().isCrossContextDispatchSupported() && + DispatcherType.INCLUDE.toString().equals(getRequest().getContext().getCrossContextDispatchType(getRequest()))) + return new ServletApiResponse.CrossContextInclude(this); return new ServletApiResponse(this); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/Dispatcher.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/Dispatcher.java index 66d6ca64eef4..f533f551479e 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/Dispatcher.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/Dispatcher.java @@ -664,6 +664,24 @@ public void sendRedirect(String location) throws IOException { // NOOP for include. } + + @Override + public void sendRedirect(String location, boolean clearBuffer) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location, int sc) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException + { + // NOOP for include. + } } private class AsyncRequest extends ParameterRequestWrapper diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/HttpOutput.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/HttpOutput.java index 13128e9a4ee8..4ac51b2adbb8 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/HttpOutput.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/HttpOutput.java @@ -319,6 +319,20 @@ public void softClose() } } + public ByteBuffer takeContentAndClose() + { + try (AutoLock l = _channelState.lock()) + { + if (_state != State.OPEN) + throw new IllegalStateException(stateString()); + // TODO avoid this copy. + ByteBuffer content = _aggregate != null && _aggregate.hasRemaining() ? BufferUtil.copy(_aggregate.getByteBuffer()) : BufferUtil.EMPTY_BUFFER; + _state = State.CLOSED; + releaseBuffer(); + return content; + } + } + /** * This method is invoked for the COMPLETE action handling in * HttpChannel.handle. The callback passed typically will call completed diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java index 2999d99ca37a..c5668fe30955 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletApiResponse.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Collection; import java.util.EnumSet; @@ -24,6 +25,8 @@ import java.util.function.Supplier; import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee11.servlet.ServletContextHandler.ServletRequestInfo; @@ -34,6 +37,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.WriteThroughWriter; +import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.session.ManagedSession; @@ -174,9 +178,17 @@ public void sendRedirect(String location) throws IOException } @Override - public void sendRedirect(String s, int i, boolean b) throws IOException + public void sendRedirect(String location, int code, boolean clear) throws IOException { - //TODO servlet6.1 + if (clear) + { + sendRedirect(code, location); + } + else + { + ByteBuffer buffer = getServletChannel().getHttpOutput().takeContentAndClose(); + sendRedirect(code, location, buffer); + } } /** @@ -187,11 +199,25 @@ public void sendRedirect(String s, int i, boolean b) throws IOException * @throws IOException if unable to send the redirect */ public void sendRedirect(int code, String location) throws IOException + { + resetBuffer(); + sendRedirect(code, location, null); + } + + /** + * Sends a response with one of the 300 series redirection codes. + * + * @param code the redirect status code + * @param location the location to send in {@code Location} headers + * @param content the content of the response, or null for a generated HTML message if {@link HttpConfiguration#isGenerateRedirectBody()} is {@code true}. + * @throws IOException if unable to send the redirect + */ + private void sendRedirect(int code, String location, ByteBuffer content) throws IOException { resetBuffer(); try (Blocker.Callback callback = Blocker.callback()) { - Response.sendRedirect(getServletRequestInfo().getRequest(), getResponse(), callback, code, location, false); + Response.sendRedirect(getServletRequestInfo().getRequest(), getResponse(), callback, code, location, false, content); callback.block(); } } @@ -634,4 +660,137 @@ public String toString() return HttpCookie.toString(this); } } + + /** + * Servlet API wrapper used on the post-dispatch side of a cross-context include. + * It prevents the headers or response code from being updated. + * @see jakarta.servlet.RequestDispatcher#include(ServletRequest, ServletResponse) + */ + public static class CrossContextInclude extends ServletApiResponse + { + public CrossContextInclude(ServletContextResponse servletContextResponse) + { + super(servletContextResponse); + } + + @Override + public void setCharacterEncoding(String charset) + { + // NOOP for include. + } + + @Override + public void setLocale(Locale loc) + { + // NOOP for include. + } + + @Override + public void setContentLength(int len) + { + // NOOP for include. + } + + @Override + public void setContentLengthLong(long len) + { + // NOOP for include. + } + + @Override + public void setContentType(String type) + { + // NOOP for include. + } + + @Override + public void reset() + { + // NOOP for include. + } + + @Override + public void resetBuffer() + { + // NOOP for include. + } + + @Override + public void setDateHeader(String name, long date) + { + // NOOP for include. + } + + @Override + public void addDateHeader(String name, long date) + { + // NOOP for include. + } + + @Override + public void setHeader(String name, String value) + { + // NOOP for include. + } + + @Override + public void addHeader(String name, String value) + { + // NOOP for include. + } + + @Override + public void setIntHeader(String name, int value) + { + // NOOP for include. + } + + @Override + public void addIntHeader(String name, int value) + { + // NOOP for include. + } + + @Override + public void setStatus(int sc) + { + // NOOP for include. + } + + @Override + public void sendError(int sc, String msg) throws IOException + { + // NOOP for include. + } + + @Override + public void sendError(int sc) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location, boolean clearBuffer) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location, int sc) throws IOException + { + // NOOP for include. + } + + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException + { + // NOOP for include. + } + } } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextRequest.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextRequest.java index 3cbee385abc2..047d975fb75a 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextRequest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextRequest.java @@ -130,8 +130,6 @@ protected ServletContextRequest( _httpInput = _servletChannel.getHttpInput(); _decodedPathInContext = decodedPathInContext; _sessionManager = sessionManager; - _servletApiRequest = newServletApiRequest(); - _response = newServletContextResponse(response); _attributes = new Attributes.Synthetic(request) { @Override @@ -156,6 +154,8 @@ protected Set getSyntheticNameSet() return ATTRIBUTES; } }; + _servletApiRequest = newServletApiRequest(); + _response = newServletContextResponse(response); addIdleTimeoutListener(_servletChannel.getServletRequestState()::onIdleTimeout); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextResponse.java b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextResponse.java index 5043bfd8c4e1..3c83c20304be 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextResponse.java +++ b/jetty-ee11/jetty-ee11-servlet/src/main/java/org/eclipse/jetty/ee11/servlet/ServletContextResponse.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.function.Supplier; +import jakarta.servlet.DispatcherType; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletResponse; import jakarta.servlet.ServletResponseWrapper; @@ -174,6 +175,9 @@ protected ServletContextRequest getServletContextRequest() protected ServletApiResponse newServletApiResponse() { + if (_servletChannel.getServletContextHandler().isCrossContextDispatchSupported() && + DispatcherType.INCLUDE.toString().equals(getRequest().getContext().getCrossContextDispatchType(getRequest()))) + return new ServletApiResponse.CrossContextInclude(this); return new ServletApiResponse(this); } diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CrossContextDispatcherTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CrossContextDispatcherTest.java index 2b506ebb33f7..87e48222d737 100644 --- a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CrossContextDispatcherTest.java +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/CrossContextDispatcherTest.java @@ -65,8 +65,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -81,7 +79,6 @@ public class CrossContextDispatcherTest { - private static final Logger LOG = LoggerFactory.getLogger(CrossContextDispatcherTest.class); public static final String MULTIPART = "--AaB03x\r\n" + "content-disposition: form-data; name=\"field1\"\r\n" + "\r\n" + @@ -99,10 +96,6 @@ public class CrossContextDispatcherTest "Connection: close\r\n"; public static final String GET_INCLUDE = "GET /context/dispatch/?include=/reader HTTP/1.1\r\n"; - public static final String MULTIPART_INCLUDE_REQUEST = GET_INCLUDE + - MULTIPART_HEADERS + - "\r\n" + - MULTIPART; public static final String GET_FORWARD = "GET /context/dispatch/?forward=/reader HTTP/1.1\r\n"; diff --git a/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java new file mode 100644 index 000000000000..23f9577b94bf --- /dev/null +++ b/jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ResponseTest.java @@ -0,0 +1,245 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.ee11.servlet; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class ResponseTest +{ + private final HttpConfiguration _httpConfiguration = new HttpConfiguration(); + private Server _server; + private LocalConnector _connector; + + public void startServer(ServletContextHandler contextHandler) throws Exception + { + _server = new Server(); + _connector = new LocalConnector(_server, new HttpConnectionFactory(_httpConfiguration)); + _server.addConnector(_connector); + + _server.setHandler(contextHandler); + _server.start(); + } + + @AfterEach + public void stopServer() + { + LifeCycle.stop(_server); + } + + @Test + public void testSimple() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + HttpServlet servlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentType("text/plain; charset=US-ASCII"); + response.getWriter().println("Hello"); + } + }; + + contextHandler.addServlet(servlet, "/servlet/*"); + startServer(contextHandler); + + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/servlet/"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + assertThat(response.getStatus(), is(200)); + assertThat(response.get("Content-Type"), is("text/plain; charset=US-ASCII")); + assertThat(response.getContent(), containsString("Hello")); + } + + public static Stream redirects() + { + List cases = new ArrayList<>(); + + for (int code : new int[] {0, 307}) + { + for (String location : new String[] {"somewhere/else", "/somewhere/else", "http://else/where" }) + { + for (boolean relative : new boolean[] {true, false}) + { + for (boolean generate : new boolean[] {true, false}) + { + for (String content : new String[] {null, "clear", "alternative text" }) + { + cases.add(Arguments.of(code, location, relative, generate, content)); + } + } + } + } + } + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("redirects") + public void testRedirect(int code, String location, boolean relative, boolean generate, String content) throws Exception + { + _httpConfiguration.setRelativeRedirectAllowed(relative); + _httpConfiguration.setGenerateRedirectBody(generate); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/ctx"); + HttpServlet servlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + if (code > 0) + { + if (content == null) + { + response.getOutputStream().write("oops".getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location, code); + } + else if ("clear".equals(content)) + { + response.getOutputStream().write("oops".getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location, code, true); + } + else + { + response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()); + response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location, code, false); + } + } + else + { + if (content == null) + { + response.getOutputStream().write("oops".getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location); + } + else if ("clear".equals(content)) + { + response.getOutputStream().write("oops".getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location, true); + } + else + { + response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()); + response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + response.sendRedirect(location, false); + } + } + } + }; + + contextHandler.addServlet(servlet, "/servlet/*"); + startServer(contextHandler); + + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/ctx/servlet/test"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + assertThat(response.getStatus(), is(code == 0 ? HttpStatus.FOUND_302 : code)); + + String destination = location; + if (relative) + { + if (!location.startsWith("/") && !location.startsWith("http:/")) + destination = "/ctx/servlet/" + location; + } + else + { + if (location.startsWith("/")) + destination = "http://test" + location; + else if (!location.startsWith("http:/")) + destination = "http://test/ctx/servlet/" + location; + } + + HttpField to = response.getField(HttpHeader.LOCATION); + assertThat(to, notNullValue()); + assertThat(to.getValue(), is(destination)); + + String expected = content; + if ("clear".equals(expected)) + expected = null; + + String actual = response.getContent(); + + if (expected == null) + { + if (generate) + { + assertThat(response.get(HttpHeader.CONTENT_TYPE), containsString("text/html")); + assertThat(actual, containsString("If you are not redirected, click here".formatted(destination))); + assertThat(actual, not(containsString("oops"))); + } + else + { + assertThat(response.get().get(HttpHeader.CONTENT_TYPE), nullValue()); + assertThat(actual, emptyString()); + } + } + else + { + assertThat(response.get().get(HttpHeader.CONTENT_TYPE), notNullValue()); + assertThat(actual, not(containsString("oops"))); + assertThat(actual, containsString(expected)); + } + } +} diff --git a/jetty-ee11/pom.xml b/jetty-ee11/pom.xml index 803d6fd09b33..a1bbce93c306 100644 --- a/jetty-ee11/pom.xml +++ b/jetty-ee11/pom.xml @@ -61,7 +61,7 @@ 4.0.1 4.0.0-M1 - 11.0.0-SNAPSHOT + 11.0.0-M19 2.0.1 true diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/BufferedResponseHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/BufferedResponseHandler.java index 61fa4cd6738f..f6a58b08f685 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/BufferedResponseHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/BufferedResponseHandler.java @@ -111,7 +111,7 @@ protected boolean shouldBuffer(HttpChannel channel, boolean last) Response response = channel.getResponse(); int status = response.getStatus(); - if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirection(status)) + if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirectionWithLocation(status)) return false; String ct = response.getContentType(); diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index 10e33baa484e..ee95e158a633 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -561,7 +561,7 @@ public void sendRedirect(int code, String location, boolean consumeAll) throws I { if (consumeAll) getHttpChannel().ensureConsumeAllOrNotPersistent(); - if (!HttpStatus.isRedirection(code)) + if (!HttpStatus.isRedirectionWithLocation(code)) throw new IllegalArgumentException("Not a 3xx redirect code"); if (!isMutable()) diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SecuredRedirectHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SecuredRedirectHandler.java index 668d0ef1f688..dc9ac789fc34 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SecuredRedirectHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SecuredRedirectHandler.java @@ -52,7 +52,7 @@ public SecuredRedirectHandler() */ public SecuredRedirectHandler(final int code) { - if (!HttpStatus.isRedirection(code)) + if (!HttpStatus.isRedirectionWithLocation(code)) throw new IllegalArgumentException("Not a 3xx redirect code"); _redirectCode = code; }