-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix(netty): preserve non-POST redirect methods #2325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
015ed89
ba32a32
872800a
78930e8
2710101
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,7 @@ | |
| import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION; | ||
| import static org.asynchttpclient.Dsl.asyncHttpClient; | ||
| import static org.asynchttpclient.Dsl.config; | ||
| import static org.asynchttpclient.netty.handler.intercept.Redirect30xInterceptor.REDIRECT_STATUSES; | ||
| import static org.asynchttpclient.util.HttpConstants.Methods.GET; | ||
| import static org.asynchttpclient.util.HttpConstants.Methods.POST; | ||
| import static org.asynchttpclient.util.HttpConstants.Methods.QUERY; | ||
|
|
@@ -66,9 +67,12 @@ public class RedirectBodyTest extends AbstractBasicTest { | |
|
|
||
| private static final byte[] REDIRECT_BODY = "redirect body".getBytes(UTF_8); | ||
| private static final String CONTENT_TYPE_VALUE = "application/octet-stream"; | ||
| private static final String NON_REPLAYABLE_STREAM_MESSAGE = | ||
| "Redirect request body InputStream does not support mark/reset and cannot be replayed"; | ||
|
|
||
| private static final List<String> receivedContentLengths = new CopyOnWriteArrayList<>(); | ||
| private static volatile boolean redirectAlreadyPerformed; | ||
| private static volatile byte[] receivedBody; | ||
| private static volatile String receivedContentType; | ||
| private static volatile String receivedMethod; | ||
| private static volatile Path fileToDeleteBeforeRedirect; | ||
|
|
@@ -77,6 +81,7 @@ public class RedirectBodyTest extends AbstractBasicTest { | |
| public void setUp() { | ||
| receivedContentLengths.clear(); | ||
| redirectAlreadyPerformed = false; | ||
| receivedBody = null; | ||
| receivedContentType = null; | ||
| receivedMethod = null; | ||
| fileToDeleteBeforeRedirect = null; | ||
|
|
@@ -101,6 +106,7 @@ public void handle(String pathInContext, Request request, HttpServletRequest htt | |
| httpResponse.setHeader(LOCATION.toString(), getTargetUrl()); | ||
|
|
||
| } else { | ||
| receivedBody = body; | ||
| receivedContentType = request.getContentType(); | ||
| receivedMethod = request.getMethod(); | ||
| httpResponse.setStatus(200); | ||
|
|
@@ -276,34 +282,110 @@ public synchronized void reset() throws IOException { | |
| .get(TIMEOUT, TimeUnit.SECONDS)); | ||
|
|
||
| IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); | ||
| assertEquals("HTTP/1 request body InputStream already consumed and cannot be reset for a retry", | ||
| cause.getMessage()); | ||
| assertEquals(NON_REPLAYABLE_STREAM_MESSAGE, cause.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "{0} on {1} keeps the existing GET rewrite") | ||
| @RepeatedIfExceptionsTest(repeats = 5) | ||
| public void put301WithNonRepeatableBodyGeneratorFailsPromptly() throws Exception { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in two commits. 872800a centralizes the selected body representation using the exact precedence from
The preflight is necessarily partial: a stream can advertise mark support but still be closed or fail to reset after the first write, so the existing write-time replay guard remains the final authority. A generic |
||
| try (InputStream body = new FilterInputStream(new ByteArrayInputStream(REDIRECT_BODY)) { | ||
| @Override | ||
| public boolean markSupported() { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public synchronized void reset() throws IOException { | ||
| throw new IOException("reset not supported"); | ||
| } | ||
| }; | ||
| AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
| ExecutionException thrown = assertThrows(ExecutionException.class, | ||
| () -> c.preparePut(getTargetUrl()) | ||
| .setBody(new InputStreamBodyGenerator(body)) | ||
| .setHeader("X-REDIRECT", "301") | ||
| .execute() | ||
| .get(TIMEOUT, TimeUnit.SECONDS)); | ||
|
|
||
| IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); | ||
| assertEquals(NON_REPLAYABLE_STREAM_MESSAGE, cause.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "{0} on {1} keeps its method and body") | ||
| @CsvSource({ | ||
| "PUT, 301", | ||
| "PUT, 302", | ||
| "PATCH, 301", | ||
| "PATCH, 302", | ||
| "DELETE, 301", | ||
| "DELETE, 302" | ||
| "DELETE, 302", | ||
| "CUSTOM, 301", | ||
| "CUSTOM, 302", | ||
| "GET, 301", | ||
| "GET, 302", | ||
| "HEAD, 301", | ||
| "HEAD, 302", | ||
| "OPTIONS, 301", | ||
| "OPTIONS, 302" | ||
| }) | ||
| public void putPatchAndDelete301And302KeepExistingBehavior(String method, int statusCode) throws Exception { | ||
| public void nonPost301And302KeepMethodAndBody(String method, int statusCode) throws Exception { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These all use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a dedicated different-host case in 2710101. It starts the PUT at |
||
| try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
| String body = "hello there"; | ||
| String contentType = "text/plain; charset=UTF-8"; | ||
|
|
||
| Response response = c.prepare(method, getTargetUrl()) | ||
| c.prepare(method, getTargetUrl()) | ||
| .setHeader(CONTENT_TYPE, contentType) | ||
| .setBody(body) | ||
| .setHeader("X-REDIRECT", Integer.toString(statusCode)) | ||
| .execute() | ||
| .get(TIMEOUT, TimeUnit.SECONDS); | ||
| assertEquals("", response.getResponseBody()); | ||
| assertEquals(GET, receivedMethod); | ||
| assertNull(receivedContentType); | ||
| assertArrayEquals(body.getBytes(UTF_8), receivedBody); | ||
| assertEquals(method, receivedMethod); | ||
| assertEquals(contentType, receivedContentType); | ||
| } | ||
| } | ||
|
|
||
| @RepeatedIfExceptionsTest(repeats = 5) | ||
| public void put301AcrossDifferentHostsKeepsMethodAndBody() throws Exception { | ||
| try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
| String body = "hello there"; | ||
| String contentType = "text/plain; charset=UTF-8"; | ||
| String originalUrl = getTargetUrl().replace("localhost", "127.0.0.1"); | ||
|
|
||
| c.preparePut(originalUrl) | ||
| .setHeader(CONTENT_TYPE, contentType) | ||
| .setBody(body) | ||
| .setHeader("X-REDIRECT", "301") | ||
| .execute() | ||
| .get(TIMEOUT, TimeUnit.SECONDS); | ||
| assertArrayEquals(body.getBytes(UTF_8), receivedBody); | ||
| assertEquals("PUT", receivedMethod); | ||
| assertEquals(contentType, receivedContentType); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "{0} on caller-added 300 keeps its method and body") | ||
| @CsvSource({"POST", "PUT"}) | ||
| public void callerAddedRedirectStatusKeepsMethodAndBody(String method) throws Exception { | ||
| boolean added = REDIRECT_STATUSES.add(300); | ||
| try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
| String body = "hello there"; | ||
| String contentType = "text/plain; charset=UTF-8"; | ||
|
|
||
| c.prepare(method, getTargetUrl()) | ||
| .setHeader(CONTENT_TYPE, contentType) | ||
| .setBody(body) | ||
| .setHeader("X-REDIRECT", "300") | ||
| .execute() | ||
| .get(TIMEOUT, TimeUnit.SECONDS); | ||
| assertArrayEquals(body.getBytes(UTF_8), receivedBody); | ||
| assertEquals(method, receivedMethod); | ||
| assertEquals(contentType, receivedContentType); | ||
| } finally { | ||
| if (added) { | ||
| REDIRECT_STATUSES.remove(300); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -447,8 +529,7 @@ public synchronized void reset() throws IOException { | |
| () -> execute307(c.preparePost(getTargetUrl()).setBody(body))); | ||
|
|
||
| IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); | ||
| assertEquals("HTTP/1 request body InputStream already consumed and cannot be reset for a retry", | ||
| cause.getMessage()); | ||
| assertEquals(NON_REPLAYABLE_STREAM_MESSAGE, cause.getMessage()); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -463,8 +544,7 @@ public void fileInputStream307FailsPromptly() throws Exception { | |
| () -> execute307(c.preparePost(getTargetUrl()).setBody(body))); | ||
|
|
||
| IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); | ||
| assertEquals("HTTP/1 request body InputStream already consumed and cannot be reset for a retry", | ||
| cause.getMessage()); | ||
| assertEquals(NON_REPLAYABLE_STREAM_MESSAGE, cause.getMessage()); | ||
| } | ||
| } finally { | ||
| Files.deleteIfExists(bodyFile); | ||
|
|
@@ -527,6 +607,19 @@ public void coexistingFileAndByteArray308UsesByteArray() throws Exception { | |
| } | ||
| } | ||
|
|
||
| @RepeatedIfExceptionsTest(repeats = 5) | ||
| public void coexistingMultipartStreamAndByteArray307UsesByteArray() throws Exception { | ||
| try (InputStream unusedPart = new ByteArrayInputStream("unused part".getBytes(UTF_8)); | ||
| AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
| Response response = execute307(c.preparePost(getTargetUrl()) | ||
| .setBody(REDIRECT_BODY) | ||
| .setBodyParts(List.of(new InputStreamPart("file", unusedPart, "unused.bin", | ||
| "unused part".length(), CONTENT_TYPE_VALUE)))); | ||
|
|
||
| assertRedirectBody(response); | ||
| } | ||
| } | ||
|
|
||
| @RepeatedIfExceptionsTest(repeats = 5) | ||
| public void formParams307KeepBody() throws Exception { | ||
| try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we keep the RFC 10008 reference ? QUERY is still correct after this but only as a side effect of
!isPostnow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept in ba32a32. The policy comment now cites RFC 9110 for the POST-scoped historical rewrite and RFC 10008 section 2.5 for QUERY, even though QUERY is handled by the general non-POST rule.