diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java index 5e12e2dfa..c64cdf5a0 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java @@ -125,7 +125,7 @@ public boolean exitAfterIntercept(Channel channel, NettyResponseFuture future return continue100Interceptor.exitAfterHandling100(channel, future); } - if (Redirect30xInterceptor.REDIRECT_STATUSES.contains(statusCode)) { + if (Redirect30xInterceptor.isRedirect(statusCode)) { return redirect30xInterceptor.exitAfterHandlingRedirect(channel, future, response, request, statusCode, realm); } 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 189e78309..910d4f209 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 @@ -18,6 +18,7 @@ import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpStatusClass; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.cookie.Cookie; import org.asynchttpclient.AsyncHttpClientConfig; @@ -70,6 +71,16 @@ public class Redirect30xInterceptor { REDIRECT_STATUSES.add(PERMANENT_REDIRECT_308); } + /** + * Whether {@code statusCode} is a redirect this interceptor follows. Only a 3xx can be, and the class + * check takes an {@code int}, so it keeps the {@link #REDIRECT_STATUSES} lookup, which boxes, off the + * responses that make up almost all traffic. The set is still consulted rather than inlined here because + * it is public and mutable, so an extra 3xx status a caller registered stays honoured. + */ + static boolean isRedirect(int statusCode) { + return HttpStatusClass.REDIRECTION.contains(statusCode) && REDIRECT_STATUSES.contains(statusCode); + } + private final ChannelManager channelManager; private final AsyncHttpClientConfig config; private final NettyRequestSender requestSender; diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptorTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptorTest.java new file mode 100644 index 000000000..797cceea3 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptorTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.netty.handler.intercept; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests {@link Redirect30xInterceptor#isRedirect(int)}: the 3xx class check and the + * {@link Redirect30xInterceptor#REDIRECT_STATUSES} membership must agree, so a 3xx that is not a followed + * redirect is rejected just like a non-3xx status. + */ +public class Redirect30xInterceptorTest { + + @Test + public void acceptsTheFollowedRedirectStatuses() { + for (int statusCode : new int[]{301, 302, 303, 307, 308}) { + assertTrue(Redirect30xInterceptor.isRedirect(statusCode), statusCode + " should be a redirect"); + } + } + + @Test + public void rejects3xxStatusesThatAreNotFollowed() { + // in the 3xx class, but not redirects this interceptor acts on: 304 in particular must fall through + // to the normal response path rather than be treated as a redirect + for (int statusCode : new int[]{300, 304, 305, 306, 399}) { + assertFalse(Redirect30xInterceptor.isRedirect(statusCode), statusCode + " should not be a redirect"); + } + } + + @Test + public void rejectsStatusesOutsideThe3xxClass() { + for (int statusCode : new int[]{100, 200, 204, 299, 400, 404, 500}) { + assertFalse(Redirect30xInterceptor.isRedirect(statusCode), statusCode + " should not be a redirect"); + } + } +}