diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index 80cac6d4f..1b18cd974 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -21,6 +21,7 @@ import io.netty.handler.codec.http.HttpStatusClass; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2StreamChannel; import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.Realm; import org.asynchttpclient.Realm.AuthScheme; @@ -32,11 +33,16 @@ import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.request.NettyRequestSender; -import io.netty.handler.codec.http2.Http2StreamChannel; +import org.asynchttpclient.request.body.generator.FileBodyGenerator; +import org.asynchttpclient.request.body.generator.InputStreamBodyGenerator; +import org.asynchttpclient.request.body.multipart.InputStreamPart; +import org.asynchttpclient.request.body.multipart.Part; import org.asynchttpclient.uri.Uri; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; import java.util.HashSet; import java.util.Set; @@ -56,7 +62,6 @@ import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.SEE_OTHER_303; import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.TEMPORARY_REDIRECT_307; import static org.asynchttpclient.util.HttpUtils.followRedirect; -import static org.asynchttpclient.util.MiscUtils.isNonEmpty; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; public class Redirect30xInterceptor { @@ -132,13 +137,28 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture LOGGER.debug("Stripping credentials on redirect to {}", newUri); } - final RequestBuilder requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod) - .setChannelPoolPartitioning(request.getChannelPoolPartitioning()) + final RequestBuilder requestBuilder; + if (keepBody) { + ensureBodyReplayable(request); + requestBuilder = request.toBuilder(); + if (!sameBase) { + // An explicitly resolved address and virtual host belong to the previous target. + requestBuilder.setAddress(null); + requestBuilder.setVirtualHost(null); + } + } else { + requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod) + .setChannelPoolPartitioning(request.getChannelPoolPartitioning()) + .setLocalAddress(request.getLocalAddress()) + .setNameResolver(request.getNameResolver()) + .setProxyServer(request.getProxyServer()) + .setRangeOffset(request.getRangeOffset()); + } + + requestBuilder.setMethod(switchToGet ? GET : originalMethod) .setFollowRedirect(true) - .setLocalAddress(request.getLocalAddress()) - .setNameResolver(request.getNameResolver()) - .setProxyServer(request.getProxyServer()) .setRealm(stripAuth ? null : request.getRealm()) + .setHeaders(propagatedHeaders(request, realm, keepBody, stripAuth)) .setRequestTimeout(request.getRequestTimeout()) .setReadTimeout(request.getReadTimeout()); @@ -154,27 +174,10 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture if (stripAuth) { future.setRealm(null); future.setProxyRealm(null); + // Request.toBuilder copies Cookie objects separately from the Cookie header. + requestBuilder.resetCookies(); } - if (keepBody) { - requestBuilder.setCharset(request.getCharset()); - if (isNonEmpty(request.getFormParams())) { - requestBuilder.setFormParams(request.getFormParams()); - } else if (request.getStringData() != null) { - requestBuilder.setBody(request.getStringData()); - } else if (request.getByteData() != null) { - requestBuilder.setBody(request.getByteData()); - } else if (request.getByteBufferData() != null) { - requestBuilder.setBody(request.getByteBufferData()); - } else if (request.getBodyGenerator() != null) { - requestBuilder.setBody(request.getBodyGenerator()); - } else if (isNonEmpty(request.getBodyParts())) { - requestBuilder.setBodyParts(request.getBodyParts()); - } - } - - requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody, stripAuth)); - // in case of a redirect from HTTP to HTTPS, future // attributes might change final boolean initialConnectionKeepAlive = future.isKeepAlive(); @@ -192,7 +195,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture } } - if (sameBase) { + if (sameBase && !keepBody) { // we can only assume the virtual host is still valid if the baseUrl is the same requestBuilder.setVirtualHost(request.getVirtualHost()); } @@ -229,10 +232,76 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture return false; } + private static void ensureBodyReplayable(Request request) throws IOException { + for (Part part : request.getBodyParts()) { + if (part instanceof InputStreamPart) { + throw new IOException("Multipart InputStream body part '" + part.getName() + + "' cannot be replayed after redirect"); + } + } + + File file = selectedBodyFile(request); + if (file != null && !file.isFile()) { + throw new IOException("Redirect request body file " + file.getAbsolutePath() + + " is not a file or does not exist"); + } + } + + private static File selectedBodyFile(Request request) { + // Keep this precedence aligned with NettyRequestFactory.body. A File can remain set alongside a + // higher-priority representation, so only validate it when the original request actually sent it. + if (hasBodyBeforeFile(request)) { + return null; + } + if (request.getFile() != null) { + return request.getFile(); + } + return request.getBodyGenerator() instanceof FileBodyGenerator + ? ((FileBodyGenerator) request.getBodyGenerator()).getFile() + : null; + } + + private static boolean hasBodyBeforeFile(Request request) { + return hasBodyBeforeStream(request) + || request.getStreamData() != null + || !request.getFormParams().isEmpty() + || !request.getBodyParts().isEmpty(); + } + + private static boolean hasBodyBeforeStream(Request request) { + return request.getByteData() != null + || request.getCompositeByteData() != null + || request.getStringData() != null + || request.getByteBufferData() != null + || request.getByteBufData() != null; + } + + private static boolean selectedBodyHasUnknownLength(Request request) { + if (hasBodyBeforeStream(request)) { + return false; + } + if (request.getStreamData() != null) { + return true; + } + if (!request.getFormParams().isEmpty() + || !request.getBodyParts().isEmpty() + || request.getFile() != null) { + return false; + } + if (request.getBodyGenerator() instanceof InputStreamBodyGenerator) { + return ((InputStreamBodyGenerator) request.getBodyGenerator()).getContentLength() < 0; + } + return request.getBodyGenerator() != null + && !(request.getBodyGenerator() instanceof FileBodyGenerator); + } + private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) { - HttpHeaders headers = request.getHeaders() - .remove(HOST) - .remove(CONTENT_LENGTH); + HttpHeaders headers = request.getHeaders().copy().remove(HOST); + + // Preserve an explicit length when the selected stream representation cannot rebuild it. + if (!keepBody || !selectedBodyHasUnknownLength(request)) { + headers.remove(CONTENT_LENGTH); + } if (!keepBody) { headers.remove(CONTENT_TYPE); diff --git a/client/src/test/java/org/asynchttpclient/RedirectBodyTest.java b/client/src/test/java/org/asynchttpclient/RedirectBodyTest.java index 461c7a06a..1360091e0 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectBodyTest.java @@ -16,32 +16,62 @@ package org.asynchttpclient; import io.github.artsok.RepeatedIfExceptionsTest; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; +import org.asynchttpclient.filter.FilterContext; +import org.asynchttpclient.filter.ResponseFilter; +import org.asynchttpclient.request.body.generator.InputStreamBodyGenerator; +import org.asynchttpclient.request.body.multipart.InputStreamPart; +import org.asynchttpclient.request.body.multipart.StringPart; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; import org.junit.jupiter.api.BeforeEach; +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import static java.nio.charset.StandardCharsets.UTF_8; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION; import static org.asynchttpclient.Dsl.asyncHttpClient; import static org.asynchttpclient.Dsl.config; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; 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 List receivedContentLengths = new CopyOnWriteArrayList<>(); private static volatile boolean redirectAlreadyPerformed; private static volatile String receivedContentType; + private static volatile Path fileToDeleteBeforeRedirect; @BeforeEach public void setUp() { + receivedContentLengths.clear(); redirectAlreadyPerformed = false; receivedContentType = null; + fileToDeleteBeforeRedirect = null; } @Override @@ -50,9 +80,14 @@ public AbstractHandler configureHandler() throws Exception { @Override public void handle(String pathInContext, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException { + byte[] body = IOUtils.toByteArray(request.getInputStream()); + receivedContentLengths.add(String.valueOf(httpRequest.getHeader(CONTENT_LENGTH.toString()))); String redirectHeader = httpRequest.getHeader("X-REDIRECT"); if (redirectHeader != null && !redirectAlreadyPerformed) { redirectAlreadyPerformed = true; + if (fileToDeleteBeforeRedirect != null) { + Files.deleteIfExists(fileToDeleteBeforeRedirect); + } httpResponse.setStatus(Integer.valueOf(redirectHeader)); httpResponse.setContentLength(0); httpResponse.setHeader(LOCATION.toString(), getTargetUrl()); @@ -60,12 +95,9 @@ public void handle(String pathInContext, Request request, HttpServletRequest htt } else { receivedContentType = request.getContentType(); httpResponse.setStatus(200); - int len = request.getContentLength(); - httpResponse.setContentLength(len); - if (len > 0) { - byte[] buffer = new byte[len]; - IOUtils.read(request.getInputStream(), buffer); - httpResponse.getOutputStream().write(buffer); + httpResponse.setContentLength(body.length); + if (body.length > 0) { + httpResponse.getOutputStream().write(body); } } httpResponse.getOutputStream().flush(); @@ -121,4 +153,287 @@ public void regular307KeepsBody() throws Exception { assertEquals(receivedContentType, contentType); } } + + @RepeatedIfExceptionsTest(repeats = 5) + public void redirectPreservesPerRequestSettings() throws Exception { + Duration readTimeout = Duration.ofSeconds(7); + long rangeOffset = 41L; + List observedReadTimeouts = new CopyOnWriteArrayList<>(); + List observedRangeOffsets = new CopyOnWriteArrayList<>(); + ResponseFilter observer = new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + observedReadTimeouts.add(ctx.getRequest().getReadTimeout()); + observedRangeOffsets.add(ctx.getRequest().getRangeOffset()); + return ctx; + } + }; + + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true).addResponseFilter(observer))) { + Response response = c.preparePost(getTargetUrl()) + .setReadTimeout(readTimeout) + .setRangeOffset(rangeOffset) + .setBody(REDIRECT_BODY) + .setHeader("X-REDIRECT", "307") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + + assertArrayEquals(REDIRECT_BODY, response.getResponseBodyAsBytes()); + assertEquals(List.of(readTimeout, readTimeout), observedReadTimeouts); + assertEquals(List.of(rangeOffset, rangeOffset), observedRangeOffsets); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void bodylessRedirectPreservesPerRequestSettings() throws Exception { + Duration readTimeout = Duration.ofSeconds(7); + long rangeOffset = 41L; + List observedReadTimeouts = new CopyOnWriteArrayList<>(); + List observedRangeOffsets = new CopyOnWriteArrayList<>(); + ResponseFilter observer = new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + observedReadTimeouts.add(ctx.getRequest().getReadTimeout()); + observedRangeOffsets.add(ctx.getRequest().getRangeOffset()); + return ctx; + } + }; + + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true).addResponseFilter(observer))) { + Response response = c.preparePost(getTargetUrl()) + .setReadTimeout(readTimeout) + .setRangeOffset(rangeOffset) + .setBody(REDIRECT_BODY) + .setHeader("X-REDIRECT", "303") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + + assertEquals("", response.getResponseBody()); + assertEquals(List.of(readTimeout, readTimeout), observedReadTimeouts); + assertEquals(List.of(rangeOffset, rangeOffset), observedRangeOffsets); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void compositeByteArray307KeepsBody() throws Exception { + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + byte[] first = "redirect ".getBytes(UTF_8); + byte[] second = "body".getBytes(UTF_8); + + Response response = execute307(c.preparePost(getTargetUrl()).setBody(Arrays.asList(first, second))); + + assertRedirectBody(response); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void byteBuf307KeepsBody() throws Exception { + ByteBuf body = Unpooled.wrappedBuffer(REDIRECT_BODY); + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = execute307(c.preparePost(getTargetUrl()).setBody(body)); + + assertRedirectBody(response); + assertEquals(1, body.refCnt(), "the caller must retain ownership of its ByteBuf"); + } finally { + body.release(); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void resettableInputStream307KeepsBody() throws Exception { + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = execute307(c.preparePost(getTargetUrl()).setBody(new ByteArrayInputStream(REDIRECT_BODY))); + + assertRedirectBody(response); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void inputStream307PreservesExplicitContentLength() throws Exception { + try (InputStream body = new ByteArrayInputStream(REDIRECT_BODY); + AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = execute307(c.preparePost(getTargetUrl()) + .setHeader(CONTENT_LENGTH, REDIRECT_BODY.length) + .setBody(body)); + + assertRedirectBody(response); + String expectedLength = Integer.toString(REDIRECT_BODY.length); + assertEquals(List.of(expectedLength, expectedLength), receivedContentLengths); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void inputStreamBodyGenerator307PreservesExplicitContentLength() throws Exception { + try (InputStream body = new ByteArrayInputStream(REDIRECT_BODY); + AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = execute307(c.preparePost(getTargetUrl()) + .setHeader(CONTENT_LENGTH, REDIRECT_BODY.length) + .setBody(new InputStreamBodyGenerator(body))); + + assertRedirectBody(response); + String expectedLength = Integer.toString(REDIRECT_BODY.length); + assertEquals(List.of(expectedLength, expectedLength), receivedContentLengths); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void nonResettableInputStream307FailsPromptly() throws Exception { + 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, + () -> 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()); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void fileInputStream307FailsPromptly() throws Exception { + Path bodyFile = Files.createTempFile("ahc-redirect-stream-", ".bin"); + try { + Files.write(bodyFile, REDIRECT_BODY); + try (InputStream body = Files.newInputStream(bodyFile); + AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + ExecutionException thrown = assertThrows(ExecutionException.class, + () -> 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()); + } + } finally { + Files.deleteIfExists(bodyFile); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void file307KeepsBody() throws Exception { + Path body = Files.createTempFile("ahc-redirect-body-", ".bin"); + try { + Files.write(body, REDIRECT_BODY); + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = execute307(c.preparePost(getTargetUrl()).setBody(body.toFile())); + + assertRedirectBody(response); + } + } finally { + Files.deleteIfExists(body); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void vanishedFile307FailsPromptly() throws Exception { + Path body = Files.createTempFile("ahc-redirect-vanished-", ".bin"); + try { + Files.write(body, REDIRECT_BODY); + fileToDeleteBeforeRedirect = body; + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + ExecutionException thrown = assertThrows(ExecutionException.class, + () -> execute307(c.preparePost(getTargetUrl()).setBody(body.toFile()))); + + IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); + assertEquals("Redirect request body file " + body.toAbsolutePath() + + " is not a file or does not exist", cause.getMessage()); + } + } finally { + Files.deleteIfExists(body); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void coexistingFileAndByteArray308UsesByteArray() throws Exception { + Path file = Files.createTempFile("ahc-redirect-precedence-", ".bin"); + try { + Files.write(file, "wrong file body".getBytes(UTF_8)); + fileToDeleteBeforeRedirect = file; + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = c.preparePost(getTargetUrl()) + .setBody(file.toFile()) + .setBody(REDIRECT_BODY) + .setHeader(CONTENT_TYPE, CONTENT_TYPE_VALUE) + .setHeader("X-REDIRECT", "308") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + + assertRedirectBody(response); + } + } finally { + Files.deleteIfExists(file); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void formParams307KeepBody() throws Exception { + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = c.preparePost(getTargetUrl()) + .addFormParam("field", "value") + .setHeader("X-REDIRECT", "307") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + + assertEquals("field=value", response.getResponseBody()); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void multipart307KeepsBody() throws Exception { + try (AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + Response response = c.preparePost(getTargetUrl()) + .addBodyPart(new StringPart("field", "multipart value")) + .setHeader("X-REDIRECT", "307") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + + assertTrue(response.getResponseBody().contains("multipart value")); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void inputStreamMultipart307FailsPromptly() throws Exception { + Path bodyFile = Files.createTempFile("ahc-redirect-multipart-", ".bin"); + try { + Files.write(bodyFile, REDIRECT_BODY); + try (InputStream body = Files.newInputStream(bodyFile); + AsyncHttpClient c = asyncHttpClient(config().setFollowRedirect(true))) { + ExecutionException thrown = assertThrows(ExecutionException.class, + () -> c.preparePost(getTargetUrl()) + .addBodyPart(new InputStreamPart("file", body, bodyFile.getFileName().toString(), + REDIRECT_BODY.length, CONTENT_TYPE_VALUE)) + .setHeader("X-REDIRECT", "307") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS)); + + IOException cause = assertInstanceOf(IOException.class, thrown.getCause()); + assertEquals("Multipart InputStream body part 'file' cannot be replayed after redirect", + cause.getMessage()); + } + } finally { + Files.deleteIfExists(bodyFile); + } + } + + private static Response execute307(BoundRequestBuilder requestBuilder) throws Exception { + return requestBuilder + .setHeader(CONTENT_TYPE, CONTENT_TYPE_VALUE) + .setHeader("X-REDIRECT", "307") + .execute() + .get(TIMEOUT, TimeUnit.SECONDS); + } + + private static void assertRedirectBody(Response response) { + assertArrayEquals(REDIRECT_BODY, response.getResponseBodyAsBytes()); + assertEquals(CONTENT_TYPE_VALUE, receivedContentType); + } } diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java index daa9676d4..99119925e 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -62,6 +62,7 @@ public class RedirectCredentialSecurityTest { private static final AtomicReference authOnBounceBack = new AtomicReference<>(); private static final AtomicReference authOn307Target = new AtomicReference<>(); private static final AtomicReference bodyOn307Target = new AtomicReference<>(); + private static final AtomicReference cookieOn307Target = new AtomicReference<>(); private static final AtomicReference authOn308Target = new AtomicReference<>(); private static final AtomicReference bodyOn308Target = new AtomicReference<>(); private static final AtomicReference lastCookieHeaderOnA = new AtomicReference<>(); @@ -158,6 +159,7 @@ public static void startServers() throws Exception { // 307 Temporary Redirect: A → B (body preserved, auth stripped) serverA.createContext("/redirect-307-to-b", exchange -> { + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-307"); exchange.sendResponseHeaders(307, -1); exchange.close(); @@ -165,6 +167,7 @@ public static void startServers() throws Exception { serverB.createContext("/target-307", exchange -> { authOn307Target.set(exchange.getRequestHeaders().getFirst("Authorization")); + cookieOn307Target.set(exchange.getRequestHeaders().getFirst("Cookie")); bodyOn307Target.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().close(); @@ -535,6 +538,29 @@ void crossDomainRedirectStripsCookieHeader() throws Exception { } } + @Test + void crossDomainRedirectStripsCookieObject() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + cookieOn307Target.set(null); + bodyOn307Target.set(null); + + client.preparePost("http://127.0.0.1:" + portA + "/redirect-307-to-b") + .addCookie(new DefaultCookie("session", "abc123")) + .setBody("request-body-content") + .execute() + .get(5, TimeUnit.SECONDS); + + assertEquals("session=abc123", lastCookieHeaderOnA.get()); + assertEquals("request-body-content", bodyOn307Target.get()); + assertNull(cookieOn307Target.get(), + "Cookie objects must not be copied to a cross-domain redirect target"); + } + } + /** * Same-origin redirect (same host and port) should preserve the Cookie header. */