diff --git a/vertx-core/src/main/asciidoc/http.adoc b/vertx-core/src/main/asciidoc/http.adoc index a4c289abf8a..59bdeeba2d3 100644 --- a/vertx-core/src/main/asciidoc/http.adoc +++ b/vertx-core/src/main/asciidoc/http.adoc @@ -1609,9 +1609,13 @@ Alternatively you can just parse the `Set-Cookie` headers yourself in the respon The client can be configured to follow HTTP redirections provided by the `Location` response header when the client receives: -* a `301`, `302`, `307` or `308` status code along with an HTTP GET or HEAD method +* a `301`, `302`, `307` or `308` status code along with an HTTP GET, HEAD, or QUERY method * a `303` status code, in addition the directed request perform an HTTP GET method +When redirecting a `QUERY` request, the request body is buffered in memory and resent to the redirected location. +By default, the maximum size of this buffer is limited to `4KB` (configured via {@link io.vertx.core.http.HttpClientConfig#setMaxRedirectBufferSize(int)}). +If the body size exceeds this limit, the redirection is not followed. + Here's an example: [source,$lang] diff --git a/vertx-core/src/main/generated/io/vertx/core/http/HttpClientOptionsConverter.java b/vertx-core/src/main/generated/io/vertx/core/http/HttpClientOptionsConverter.java index be792704237..c7aea12652e 100644 --- a/vertx-core/src/main/generated/io/vertx/core/http/HttpClientOptionsConverter.java +++ b/vertx-core/src/main/generated/io/vertx/core/http/HttpClientOptionsConverter.java @@ -127,6 +127,11 @@ static void fromJson(Iterable> json, HttpCli obj.setMaxRedirects(((Number)member.getValue()).intValue()); } break; + case "maxRedirectBufferSize": + if (member.getValue() instanceof Number) { + obj.setMaxRedirectBufferSize(((Number)member.getValue()).intValue()); + } + break; case "forceSni": if (member.getValue() instanceof Boolean) { obj.setForceSni((Boolean)member.getValue()); @@ -193,6 +198,7 @@ static void toJson(HttpClientOptions obj, java.util.Map json) { json.put("http2ClearTextUpgrade", obj.isHttp2ClearTextUpgrade()); json.put("http2ClearTextUpgradeWithPreflightRequest", obj.isHttp2ClearTextUpgradeWithPreflightRequest()); json.put("maxRedirects", obj.getMaxRedirects()); + json.put("maxRedirectBufferSize", obj.getMaxRedirectBufferSize()); json.put("forceSni", obj.isForceSni()); json.put("decoderInitialBufferSize", obj.getDecoderInitialBufferSize()); if (obj.getTracingPolicy() != null) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpClientConfig.java b/vertx-core/src/main/java/io/vertx/core/http/HttpClientConfig.java index d019898f498..1c5184a905f 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpClientConfig.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpClientConfig.java @@ -10,6 +10,7 @@ */ package io.vertx.core.http; +import io.vertx.core.impl.Arguments; import io.vertx.codegen.annotations.DataObject; import io.vertx.codegen.annotations.GenIgnore; import io.vertx.codegen.annotations.Unstable; @@ -79,6 +80,7 @@ private static QuicClientConfig defaultQuicConfig() { private String defaultHost; private int defaultPort; private int maxRedirects; + private int maxRedirectBufferSize; private ObservabilityConfig observabilityConfig; private boolean shared; private String name; @@ -97,6 +99,7 @@ public HttpClientConfig() { this.defaultHost = HttpClientOptions.DEFAULT_DEFAULT_HOST; this.defaultPort = HttpClientOptions.DEFAULT_DEFAULT_PORT; this.maxRedirects = HttpClientOptions.DEFAULT_MAX_REDIRECTS; + this.maxRedirectBufferSize = HttpClientOptions.DEFAULT_MAX_REDIRECT_BUFFER_SIZE; this.observabilityConfig = null; this.shared = HttpClientOptions.DEFAULT_SHARED; this.name = HttpClientOptions.DEFAULT_NAME; @@ -116,6 +119,7 @@ public HttpClientConfig(HttpClientConfig other) { this.defaultHost = other.defaultHost; this.defaultPort = other.defaultPort; this.maxRedirects = other.maxRedirects; + this.maxRedirectBufferSize = other.maxRedirectBufferSize; this.observabilityConfig = other.observabilityConfig != null ? new ObservabilityConfig(other.observabilityConfig) : null; this.shared = other.shared; this.name = other.name; @@ -143,6 +147,7 @@ public HttpClientConfig(HttpClientOptions options) { this.defaultHost = options.getDefaultHost(); this.defaultPort = options.getDefaultPort(); this.maxRedirects = options.getMaxRedirects(); + this.maxRedirectBufferSize = options.getMaxRedirectBufferSize(); this.observabilityConfig = observabilityConfig; this.shared = options.isShared(); this.name = options.getName(); @@ -451,6 +456,25 @@ public HttpClientConfig setMaxRedirects(int maxRedirects) { return this; } + /** + * @return the maximum size in bytes of the redirect buffer when redirecting QUERY requests + */ + public int getMaxRedirectBufferSize() { + return maxRedirectBufferSize; + } + + /** + * Set the maximum size of the redirect buffer in bytes when redirecting QUERY requests. + * + * @param maxRedirectBufferSize the maximum buffer size + * @return a reference to this, so the API can be used fluently + */ + public HttpClientConfig setMaxRedirectBufferSize(int maxRedirectBufferSize) { + Arguments.require(maxRedirectBufferSize >= 0, "Max redirect buffer size must be >= 0"); + this.maxRedirectBufferSize = maxRedirectBufferSize; + return this; + } + /** * @return the client observability config. */ diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpClientOptions.java b/vertx-core/src/main/java/io/vertx/core/http/HttpClientOptions.java index f7f1bcb6b51..45664b44397 100755 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpClientOptions.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpClientOptions.java @@ -11,6 +11,7 @@ package io.vertx.core.http; +import io.vertx.core.impl.Arguments; import io.netty.handler.logging.ByteBufFormat; import io.vertx.codegen.annotations.DataObject; import io.vertx.codegen.json.annotations.JsonGen; @@ -133,6 +134,11 @@ public class HttpClientOptions extends ClientOptionsBase { */ public static final int DEFAULT_MAX_REDIRECTS = 16; + /** + * Default max redirect buffer size = 4 * 1024 bytes (4KB) + */ + public static final int DEFAULT_MAX_REDIRECT_BUFFER_SIZE = 4 * 1024; + /* * Default force SNI = {@code false} */ @@ -171,6 +177,7 @@ public class HttpClientOptions extends ClientOptionsBase { private int defaultPort; private HttpVersion protocolVersion; private int maxRedirects; + private int maxRedirectBufferSize; private boolean forceSni; private TracingPolicy tracingPolicy; @@ -211,6 +218,7 @@ public HttpClientOptions(HttpClientOptions other) { this.defaultPort = other.defaultPort; this.protocolVersion = other.protocolVersion; this.maxRedirects = other.maxRedirects; + this.maxRedirectBufferSize = other.maxRedirectBufferSize; this.forceSni = other.forceSni; this.tracingPolicy = other.tracingPolicy; this.shared = other.shared; @@ -248,6 +256,7 @@ private void init() { defaultPort = DEFAULT_DEFAULT_PORT; protocolVersion = DEFAULT_PROTOCOL_VERSION; maxRedirects = DEFAULT_MAX_REDIRECTS; + maxRedirectBufferSize = DEFAULT_MAX_REDIRECT_BUFFER_SIZE; forceSni = DEFAULT_FORCE_SNI; tracingPolicy = DEFAULT_TRACING_POLICY; shared = DEFAULT_SHARED; @@ -896,6 +905,25 @@ public HttpClientOptions setMaxRedirects(int maxRedirects) { return this; } + /** + * @return the maximum size in bytes of the redirect buffer when redirecting QUERY requests + */ + public int getMaxRedirectBufferSize() { + return maxRedirectBufferSize; + } + + /** + * Set the maximum size of the redirect buffer in bytes when redirecting QUERY requests. + * + * @param maxRedirectBufferSize the maximum buffer size + * @return a reference to this, so the API can be used fluently + */ + public HttpClientOptions setMaxRedirectBufferSize(int maxRedirectBufferSize) { + Arguments.require(maxRedirectBufferSize >= 0, "Max redirect buffer size must be >= 0"); + this.maxRedirectBufferSize = maxRedirectBufferSize; + return this; + } + /** * @return whether the client should always use SNI on TLS/SSL connections */ diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java index 5b955e233d0..57efb332dff 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java @@ -116,6 +116,20 @@ public interface HttpClientRequest extends WriteStream { */ int getMaxRedirects(); + /** + * @return the maximum size in bytes of the redirect buffer when redirecting QUERY requests + */ + int maxRedirectBufferSize(); + + /** + * Set the maximum size of the redirect buffer in bytes when redirecting QUERY requests. + * + * @param maxRedirectBufferSize the maximum buffer size + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpClientRequest maxRedirectBufferSize(int maxRedirectBufferSize); + /** * @return the number of followed redirections for the current HTTP request */ diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java b/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java index bd3acd5ac4f..efccd840601 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java @@ -71,6 +71,14 @@ public interface HttpHeaders { @GenIgnore(GenIgnore.PERMITTED_TYPE) CharSequence ACCEPT_PATCH = HttpHeaderNames.ACCEPT_PATCH; + /** + * Accept-Query header name + *

+ * TODO: Use {@code HttpHeaderNames.ACCEPT_QUERY} when bumping to the next Netty release + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + CharSequence ACCEPT_QUERY = createOptimized("accept-query"); + /** * Access-Control-Allow-Credentials header name */ diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpMethod.java b/vertx-core/src/main/java/io/vertx/core/http/HttpMethod.java index 752001264d2..c4901e453b8 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpMethod.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpMethod.java @@ -73,6 +73,11 @@ public class HttpMethod { */ public static final HttpMethod PATCH; + /** + * The RFC 10008 {@code QUERY} method, this instance is interned and uniquely used. + */ + public static final HttpMethod QUERY; + /** * The RFC 2518/4918 {@code PROPFIND} method, this instance is interned and uniquely used. */ @@ -198,6 +203,8 @@ public class HttpMethod { TRACE = new HttpMethod(io.netty.handler.codec.http.HttpMethod.TRACE); CONNECT = new HttpMethod(io.netty.handler.codec.http.HttpMethod.CONNECT); PATCH = new HttpMethod(io.netty.handler.codec.http.HttpMethod.PATCH); + // TODO: Use Netty HttpMethod.QUERY in the next Netty bump + QUERY = new HttpMethod(io.netty.handler.codec.http.HttpMethod.valueOf("QUERY")); PROPFIND = new HttpMethod(io.netty.handler.codec.http.HttpMethod.valueOf("PROPFIND")); PROPPATCH = new HttpMethod(io.netty.handler.codec.http.HttpMethod.valueOf("PROPPATCH")); MKCOL = new HttpMethod(io.netty.handler.codec.http.HttpMethod.valueOf("MKCOL")); @@ -252,7 +259,8 @@ public class HttpMethod { HttpMethod.MKACTIVITY, HttpMethod.ORDERPATCH, HttpMethod.ACL, - HttpMethod.SEARCH + HttpMethod.SEARCH, + HttpMethod.QUERY )); } @@ -303,6 +311,8 @@ private static HttpMethod _fromNetty(io.netty.handler.codec.http.HttpMethod sMet return CONNECT; case "PATCH": return PATCH; + case "QUERY": + return QUERY; case "PROPFIND": return PROPFIND; case "PROPPATCH": @@ -386,6 +396,8 @@ public static HttpMethod valueOf(String value) { return CONNECT; case "PATCH": return PATCH; + case "QUERY": + return QUERY; case "PROPFIND": return PROPFIND; case "PROPPATCH": diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/DefaultRedirectHandler.java b/vertx-core/src/main/java/io/vertx/core/http/impl/DefaultRedirectHandler.java index 4134e54daed..4c37bddb38a 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/DefaultRedirectHandler.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/DefaultRedirectHandler.java @@ -35,7 +35,7 @@ public Future apply(HttpClientResponse resp) { HttpMethod m = resp.request().getMethod(); if (statusCode == 303) { m = HttpMethod.GET; - } else if (m != HttpMethod.GET && m != HttpMethod.HEAD) { + } else if (m != HttpMethod.GET && m != HttpMethod.HEAD && m != HttpMethod.QUERY) { return null; } URI uri = HttpUtils.resolveURIReference(resp.request().absoluteURI(), location); @@ -72,6 +72,7 @@ public Future apply(HttpClientResponse resp) { options.setURI(requestURI); options.setHeaders(resp.request().headers()); options.removeHeader(CONTENT_LENGTH); + options.removeHeader(HttpHeaders.TRANSFER_ENCODING); return Future.succeededFuture(options); } return null; diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java index 345459d7639..e09fa1fd483 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientBuilderInternal.java @@ -183,6 +183,7 @@ private HttpClientImpl createHttpClientImpl(HttpClientConfig config, config.getDefaultHost(), config.getDefaultPort(), config.getMaxRedirects(), + config.getMaxRedirectBufferSize(), versions, sslOptions, connectHandler, @@ -210,6 +211,7 @@ public LegacyHttpClient( String defaultHost, int defaultPort, int maxRedirects, + int maxRedirectBufferSize, List versions, ClientSSLOptions sslOptions, Handler connectHandler, @@ -217,7 +219,7 @@ public LegacyHttpClient( HttpClientTransport quicTransport, HttpClientConfig config, HttpClientOptions options) { - super(vertx, resolver, redirectHandler, httpMetrics, poolOptions, defaultProxyOptions, nonProxyHosts, loadBalancer, followAlternativeServices, resolverIdeTimeout, verifyHost, defaultSsl, defaultHost, defaultPort, maxRedirects, versions, sslOptions, connectHandler, tcpTransport, quicTransport); + super(vertx, resolver, redirectHandler, httpMetrics, poolOptions, defaultProxyOptions, nonProxyHosts, loadBalancer, followAlternativeServices, resolverIdeTimeout, verifyHost, defaultSsl, defaultHost, defaultPort, maxRedirects, maxRedirectBufferSize, versions, sslOptions, connectHandler, tcpTransport, quicTransport); this.config = config; this.options = options; } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java index 7aa63b9e92a..492ddccbd3d 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java @@ -69,6 +69,7 @@ public class HttpClientImpl extends HttpClientBase implements HttpClientInternal private final String defaultHost; private final int defaultPort; private final int maxRedirects; + private final int maxRedirectBufferSize; private final List versions; private final Handler connectHandler; private volatile Handler exceptionHandler; @@ -89,6 +90,7 @@ public class HttpClientImpl extends HttpClientBase implements HttpClientInternal String defaultHost, int defaultPort, int maxRedirects, + int maxRedirectBufferSize, List versions, ClientSSLOptions sslOptions, Handler connectHandler, @@ -117,6 +119,7 @@ public class HttpClientImpl extends HttpClientBase implements HttpClientInternal this.defaultHost = defaultHost; this.defaultPort = defaultPort; this.maxRedirects = maxRedirects; + this.maxRedirectBufferSize = maxRedirectBufferSize; this.versions = versions; this.sslOptions = sslOptions; this.connectHandler = connectHandler; @@ -791,6 +794,7 @@ public ConnectionObtainedResult(HttpClientStream stream, Lease> rHandler = redirectHandler; if (rHandler != null) { request.setMaxRedirects(maxRedirects); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java index 8fddd226ffa..93536464627 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java @@ -26,6 +26,10 @@ import io.vertx.core.net.ProxyOptions; import io.vertx.core.net.ProxyType; +import java.util.ArrayList; +import java.util.List; +import io.netty.buffer.Unpooled; +import io.netty.buffer.CompositeByteBuf; import java.util.Base64; import java.util.Map; import java.util.Objects; @@ -40,7 +44,6 @@ public class HttpClientRequestImpl extends HttpClientRequestBase implements HttpClientRequest { static final Logger log = LoggerFactory.getLogger(HttpClientRequestImpl.class); - private final Promise endPromise; private final Future endFuture; private boolean chunked; @@ -58,6 +61,8 @@ public class HttpClientRequestImpl extends HttpClientRequestBase implements Http private StreamPriority priority; private boolean isConnect; private String traceOperation; + private int maxRedirectBufferSize = HttpClientOptions.DEFAULT_MAX_REDIRECT_BUFFER_SIZE; + private List bodyBuffer; public HttpClientRequestImpl(HostAndPort authority, HttpConnection connection, HttpClientStream stream) { super(authority, connection, stream, stream.context().promise(), HttpMethod.GET, "/"); @@ -160,6 +165,18 @@ public synchronized int getMaxRedirects() { return maxRedirects; } + @Override + public synchronized HttpClientRequest maxRedirectBufferSize(int maxRedirectBufferSize) { + Arguments.require(maxRedirectBufferSize >= 0, "Max redirect buffer size must be >= 0"); + this.maxRedirectBufferSize = maxRedirectBufferSize; + return this; + } + + @Override + public synchronized int maxRedirectBufferSize() { + return maxRedirectBufferSize; + } + @Override public int numberOfRedirections() { return numberOfRedirections; @@ -384,13 +401,29 @@ private void handleNextRequest(HttpClientRequest next, Promise { if (ar.succeeded()) { if (timeoutMs > 0) { next.idleTimeout(timeoutMs); } - next.end(); + if (next.getMethod() == HttpMethod.QUERY && bodyBuffer != null && !bodyBuffer.isEmpty()) { + Buffer redirectBody; + if (bodyBuffer.size() == 1) { + redirectBody = bodyBuffer.get(0); + } else { + CompositeByteBuf composite = Unpooled.compositeBuffer(); + for (Buffer b : bodyBuffer) { + composite.addComponent(true, ((BufferInternal) b).getByteBuf()); + } + redirectBody = BufferInternal.buffer(composite); + } + next.end(redirectBody); + } else { + next.end(); + } } else { next.reset(0); } @@ -505,6 +538,25 @@ private Future doWrite(Buffer buff, boolean end, boolean connect) { if (trailersSent) { return context.failedFuture(new IllegalStateException("Request already complete")); } + if (followRedirects && getMethod() == HttpMethod.QUERY) { + if (buff != null) { + int currentLen = 0; + if (bodyBuffer != null) { + for (Buffer b : bodyBuffer) { + currentLen += b.length(); + } + } + if (currentLen + buff.length() > maxRedirectBufferSize) { + followRedirects = false; + bodyBuffer = null; + } else { + if (bodyBuffer == null) { + bodyBuffer = new ArrayList<>(); + } + bodyBuffer.add(buff.copy()); + } + } + } if (!headersSent) { if (!connect) { boolean requiresContentLength = !this.chunked && !headers.contains(CONTENT_LENGTH); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java index e19046751b7..dd969304876 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java @@ -107,6 +107,16 @@ public int numberOfRedirections() { return 0; } + @Override + public int maxRedirectBufferSize() { + return 0; + } + + @Override + public HttpClientRequest maxRedirectBufferSize(int maxRedirectBufferSize) { + throw new IllegalStateException(); + } + @Override public HttpClientRequest redirectHandler(@Nullable Function> handler) { throw new IllegalStateException(); diff --git a/vertx-core/src/test/java/io/vertx/test/http/HttpClientConfigurator.java b/vertx-core/src/test/java/io/vertx/test/http/HttpClientConfigurator.java index 6125f904e81..d4dd9f90344 100644 --- a/vertx-core/src/test/java/io/vertx/test/http/HttpClientConfigurator.java +++ b/vertx-core/src/test/java/io/vertx/test/http/HttpClientConfigurator.java @@ -23,6 +23,7 @@ public interface HttpClientConfigurator { HttpClientConfigurator setLogActivity(boolean logActivity); HttpClientConfigurator setMetricsName(String name); HttpClientConfigurator configureSsl(Consumer configurator); + HttpClientConfigurator setMaxRedirectBufferSize(int maxRedirectBufferSize); default HttpClientAgent create(Vertx vertx, PoolOptions poolOptions) { return builder(vertx).with(poolOptions).build(); } diff --git a/vertx-core/src/test/java/io/vertx/test/http/HttpConfigurator.java b/vertx-core/src/test/java/io/vertx/test/http/HttpConfigurator.java index 2e8979f1175..e12be1cd298 100644 --- a/vertx-core/src/test/java/io/vertx/test/http/HttpConfigurator.java +++ b/vertx-core/src/test/java/io/vertx/test/http/HttpConfigurator.java @@ -111,6 +111,11 @@ public HttpClientConfigurator configureSsl(Consumer configurat return this; } @Override + public HttpClientConfigurator setMaxRedirectBufferSize(int maxRedirectBufferSize) { + options.setMaxRedirectBufferSize(maxRedirectBufferSize); + return this; + } + @Override public HttpClientBuilder builder(Vertx vertx) { return vertx.httpClientBuilder().with(options); } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/HttpMethodTest.java b/vertx-core/src/test/java/io/vertx/tests/http/HttpMethodTest.java index caeb901b2dc..2af31a859fb 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/HttpMethodTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/HttpMethodTest.java @@ -59,6 +59,7 @@ public void testConstantNames() { assertEquals("ORDERPATCH", HttpMethod.ORDERPATCH.name()); assertEquals("ACL", HttpMethod.ACL.name()); assertEquals("SEARCH", HttpMethod.SEARCH.name()); + assertEquals("QUERY", HttpMethod.QUERY.name()); } @Test @@ -94,7 +95,8 @@ public void testConstants() { HttpMethod.MKACTIVITY, HttpMethod.ORDERPATCH, HttpMethod.ACL, - HttpMethod.SEARCH + HttpMethod.SEARCH, + HttpMethod.QUERY )) { assertSame(HttpMethod.valueOf(method.name()), method); assertSame(method.name(), method.toString()); @@ -162,6 +164,7 @@ public void testNettyInterop() { assertSame(HttpMethod.fromNetty(io.netty.handler.codec.http.HttpMethod.OPTIONS), HttpMethod.OPTIONS); assertSame(HttpMethod.fromNetty(io.netty.handler.codec.http.HttpMethod.PATCH), HttpMethod.PATCH); assertSame(HttpMethod.fromNetty(io.netty.handler.codec.http.HttpMethod.TRACE), HttpMethod.TRACE); + assertSame(HttpMethod.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("QUERY")), HttpMethod.QUERY); assertEquals(HttpMethod.valueOf("foo").toNetty().name(), "foo"); assertEquals(HttpMethod.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("foo")).name(), "foo"); } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java index a2aca76201a..8c638cc3bfb 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java @@ -3234,6 +3234,41 @@ public void testClientLocalAddress() throws Exception { assertEquals(200, (int) sc); } + @Test + public void testFollowRedirectQueryOn301() throws Exception { + testFollowRedirect(HttpMethod.QUERY, HttpMethod.QUERY, 301, 200, 2, + "http://" + config.host() + ":" + config.port() + "/redirected", + "http://" + config.host() + ":" + config.port() + "/redirected"); + } + + @Test + public void testFollowRedirectQueryOn302() throws Exception { + testFollowRedirect(HttpMethod.QUERY, HttpMethod.QUERY, 302, 200, 2, + "http://" + config.host() + ":" + config.port() + "/redirected", + "http://" + config.host() + ":" + config.port() + "/redirected"); + } + + @Test + public void testFollowRedirectQueryOn303() throws Exception { + testFollowRedirect(HttpMethod.QUERY, HttpMethod.GET, 303, 200, 2, + "http://" + config.host() + ":" + config.port() + "/redirected", + "http://" + config.host() + ":" + config.port() + "/redirected"); + } + + @Test + public void testFollowRedirectQueryOn307() throws Exception { + testFollowRedirect(HttpMethod.QUERY, HttpMethod.QUERY, 307, 200, 2, + "http://" + config.host() + ":" + config.port() + "/redirected", + "http://" + config.host() + ":" + config.port() + "/redirected"); + } + + @Test + public void testFollowRedirectQueryOn308() throws Exception { + testFollowRedirect(HttpMethod.QUERY, HttpMethod.QUERY, 308, 200, 2, + "http://" + config.host() + ":" + config.port() + "/redirected", + "http://" + config.host() + ":" + config.port() + "/redirected"); + } + @Test public void testFollowRedirectGetOn301() throws Exception { testFollowRedirect(HttpMethod.GET, HttpMethod.GET, 301, 200, 2, "http://" + config.host() + ":" + config.port() + "/redirected", "http://" + config.host() + ":" + config.port() + "/redirected"); @@ -3389,6 +3424,170 @@ private void testFollowRedirect( ).await(); } + @Test + public void testFollowRedirectQueryWithBody() throws Exception { + Buffer expected = TestUtils.randomBuffer(2048); + server.requestHandler(req -> { + if ("/".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + String scheme = req.connection().isSsl() ? "https" : "http"; + req.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, scheme + "://" + config.host() + ":" + config.port() + "/whatever").end(); + })); + } else if ("/whatever".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + req.response().end(); + })); + } else { + req.response().setStatusCode(404).end(); + } + }); + startServer(testAddress); + RequestOptions opts = new RequestOptions() + .setMethod(QUERY) + .setHost(config.host()) + .setPort(config.port()); + client.request(opts).compose(req -> req + .setFollowRedirects(true) + .send(expected) + .expecting(HttpResponseExpectation.SC_OK)) + .await(); + } + + @Test + public void testFollowRedirectQueryWithBodyExceedingLimit() throws Exception { + Buffer expected = TestUtils.randomBuffer(4 * 1024 + 1); // 4KB + 1B + server.requestHandler(req -> { + if ("/".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + String scheme = req.connection().isSsl() ? "https" : "http"; + req.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, scheme + "://" + config.host() + ":" + config.port() + "/whatever").end(); + })); + } else { + fail("Should not redirect"); + } + }); + startServer(testAddress); + RequestOptions opts = new RequestOptions() + .setMethod(QUERY) + .setHost(config.host()) + .setPort(config.port()); + client.request(opts).compose(req -> req + .setFollowRedirects(true) + .send(expected) + .expecting(HttpResponseExpectation.status(307))) + .await(); + } + + @Test + public void testFollowRedirectQueryWithConfiguredBodyExceedingLimit() throws Exception { + Buffer expected = TestUtils.randomBuffer(2048); + server.requestHandler(req -> { + if ("/".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + String scheme = req.connection().isSsl() ? "https" : "http"; + req.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, scheme + "://" + config.host() + ":" + config.port() + "/whatever").end(); + })); + } else { + fail("Should not redirect"); + } + }); + startServer(testAddress); + client = config.forClient().setMaxRedirectBufferSize(1024).create(vertx); + RequestOptions opts = new RequestOptions() + .setMethod(QUERY) + .setHost(config.host()) + .setPort(config.port()); + client.request(opts).compose(req -> req + .setFollowRedirects(true) + .send(expected) + .expecting(HttpResponseExpectation.status(307))) + .await(); + } + + @Test + public void testFollowRedirectQueryWithMultipleBuffers() throws Exception { + Buffer chunk1 = TestUtils.randomBuffer(1024); + Buffer chunk2 = TestUtils.randomBuffer(1024); + Buffer expected = Buffer.buffer().appendBuffer(chunk1).appendBuffer(chunk2); + server.requestHandler(req -> { + if ("/".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + String scheme = req.connection().isSsl() ? "https" : "http"; + req.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, scheme + "://" + config.host() + ":" + config.port() + "/whatever").end(); + })); + } else if ("/whatever".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + req.response().end(); + })); + } else { + req.response().setStatusCode(404).end(); + } + }); + startServer(testAddress); + RequestOptions opts = new RequestOptions() + .setMethod(QUERY) + .setHost(config.host()) + .setPort(config.port()); + client.request(opts).compose(req -> { + req.setFollowRedirects(true); + req.write(chunk1); + req.end(chunk2); + return req.response().expecting(HttpResponseExpectation.SC_OK); + }).await(); + } + + @Test + public void testFollowRedirectQueryWithStream() throws Exception { + Buffer chunk1 = TestUtils.randomBuffer(1024); + Buffer chunk2 = TestUtils.randomBuffer(1024); + Buffer expected = Buffer.buffer().appendBuffer(chunk1).appendBuffer(chunk2); + server.requestHandler(req -> { + if ("/".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + String scheme = req.connection().isSsl() ? "https" : "http"; + req.response().setStatusCode(307).putHeader(HttpHeaders.LOCATION, scheme + "://" + config.host() + ":" + config.port() + "/whatever").end(); + })); + } else if ("/whatever".equals(req.path())) { + assertEquals(HttpMethod.QUERY, req.method()); + req.body().onComplete(TestUtils.onSuccess(body -> { + assertEquals(expected, body); + req.response().end(); + })); + } else { + req.response().setStatusCode(404).end(); + } + }); + startServer(testAddress); + FakeStream stream = new FakeStream<>(); + stream.pause(); + RequestOptions opts = new RequestOptions() + .setMethod(QUERY) + .setHost(config.host()) + .setPort(config.port()); + Future responseFuture = client.request(opts).compose(req -> { + req.setFollowRedirects(true); + return req.send(stream); + }); + stream.write(chunk1); + stream.write(chunk2); + stream.end(); + responseFuture.compose(HttpClientResponse::end).await(); + } + @Test public void testFollowRedirectWithBody() throws Exception { testFollowRedirectWithBody(Function.identity()); @@ -3792,6 +3991,8 @@ class MockReq implements HttpClientRequest { public HttpClientRequest continueHandler(@Nullable Handler handler) { throw new UnsupportedOperationException(); } public boolean isFollowRedirects() { throw new UnsupportedOperationException(); } public int getMaxRedirects() { throw new UnsupportedOperationException(); } + public int maxRedirectBufferSize() { throw new UnsupportedOperationException(); } + public HttpClientRequest maxRedirectBufferSize(int maxRedirectBufferSize) { throw new UnsupportedOperationException(); } public int numberOfRedirections() { throw new UnsupportedOperationException(); } public HttpClientRequest redirectHandler(@Nullable Function> handler) { throw new UnsupportedOperationException(); } public HttpClientRequest earlyHintsHandler(@Nullable Handler handler) { throw new UnsupportedOperationException(); } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3Configurator.java b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3Configurator.java index c6555fe0415..43b5cd260d0 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3Configurator.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3Configurator.java @@ -221,6 +221,11 @@ public HttpClientConfigurator setKeepAliveTimeout(Duration timeout) { return this; } @Override + public HttpClientConfigurator setMaxRedirectBufferSize(int maxRedirectBufferSize) { + config.setMaxRedirectBufferSize(maxRedirectBufferSize); + return this; + } + @Override public HttpClientBuilder builder(Vertx vertx) { return vertx.httpClientBuilder().with(config).with(sslOptions); }