From b299c9382b58fcea61698a35ae76f5227ee139a7 Mon Sep 17 00:00:00 2001 From: Jose Date: Mon, 27 Feb 2023 16:41:15 +0100 Subject: [PATCH 1/2] Support HTTP/2 connections in REST Client Reactive Relates https://github.com/quarkusio/quarkus/issues/13969 --- .../main/asciidoc/rest-client-reactive.adoc | 21 + .../restclient/config/RestClientConfig.java | 20 + .../restclient/config/RestClientsConfig.java | 15 + .../runtime/RestClientBuilderImpl.java | 4 + .../runtime/RestClientCDIDelegateBuilder.java | 10 + .../api/QuarkusRestClientProperties.java | 10 + .../handlers/ClientSendRequestHandler.java | 25 + .../client/impl/ClientBuilderImpl.java | 21 + .../reactive/client/impl/ClientImpl.java | 2 + .../client/impl/ClientResponseImpl.java | 4 + .../impl/multipart/MultiByteHttpData.java | 5 + .../PausableHttpPostRequestEncoder.java | 2 - .../multipart/QuarkusMultipartFormUpload.java | 4 + integration-tests/pom.xml | 1 + .../rest-client-reactive-http2/pom.xml | 115 + .../quarkus/it/rest/client/http2/Client.java | 15 + .../it/rest/client/http2/Resource.java | 32 + .../http2/multipart/MultipartClient.java | 301 ++ .../http2/multipart/MultipartResource.java | 444 ++ .../src/main/resources/application.properties | 7 + .../it/rest/client/http2/ResourceIT.java | 7 + .../it/rest/client/http2/ResourceTest.java | 82 + .../http2/multipart/MultipartResourceIT.java | 7 + .../multipart/MultipartResourceTest.java | 278 ++ .../larger-than-default-form-attribute.txt | 3753 ----------------- ...ltFormAttributeMultipartFormInputTest.java | 45 - 26 files changed, 1430 insertions(+), 3800 deletions(-) create mode 100644 integration-tests/rest-client-reactive-http2/pom.xml create mode 100644 integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Client.java create mode 100644 integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Resource.java create mode 100644 integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartClient.java create mode 100644 integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartResource.java create mode 100644 integration-tests/rest-client-reactive-http2/src/main/resources/application.properties create mode 100644 integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceIT.java create mode 100644 integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceTest.java create mode 100644 integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceIT.java create mode 100644 integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceTest.java delete mode 100644 integration-tests/rest-client-reactive-multipart/src/main/resources/larger-than-default-form-attribute.txt delete mode 100644 integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 21374b9d064cb..aa9fca1d75815 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -296,6 +296,27 @@ quarkus.rest-client.extensions-api.verify-host=false This setting should not be used in production as it will disable the SSL hostname verification. ==== +=== HTTP/2 Support + +HTTP/2 is disabled by default in REST Client. If you want to enable it, you can set: + +[source, properties] +---- +// for all REST Clients: +quarkus.rest-client.http2=true +// or for a single REST Client: +quarkus.rest-client.extensions-api.http2=true +---- + +Alternatively, you can enable the Application-Layer Protocol Negotiation (alpn) TLS extension and the client will negotiate which HTTP version to use over the ones compatible by the server. By default, it will try to use HTTP/2 first and if it's not enabled, it will use HTTP/1.1. If you want to enable it, you can set: + +[source, properties] +---- +quarkus.rest-client.alpn=true +// or for a single REST Client: +quarkus.rest-client.extensions-api.alpn=true +---- + == Create the JAX-RS resource Create the `src/main/java/org/acme/rest/client/ExtensionsResource.java` file with the following content: diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index 80a0e6445a2a2..d81ad68ef7316 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -47,6 +47,8 @@ public class RestClientConfig { EMPTY.shared = Optional.empty(); EMPTY.name = Optional.empty(); EMPTY.userAgent = Optional.empty(); + EMPTY.http2 = Optional.empty(); + EMPTY.alpn = Optional.empty(); } /** @@ -245,6 +247,21 @@ public class RestClientConfig { @ConfigItem public Optional userAgent; + /** + * If this is true then HTTP/2 will be enabled. + */ + @ConfigItem + public Optional http2; + + /** + * If the Application-Layer Protocol Negotiation is enabled, the client will negotiate which protocol to use over the + * protocols exposed by the server. By default, it will try to use HTTP/2 first and if it's not enabled, it will + * use HTTP/1.1. + * When the property `http2` is enabled, this flag will be automatically enabled. + */ + @ConfigItem + public Optional alpn; + public static RestClientConfig load(String configKey) { final RestClientConfig instance = new RestClientConfig(); @@ -276,6 +293,7 @@ public static RestClientConfig load(String configKey) { instance.shared = getConfigValue(configKey, "shared", Boolean.class); instance.name = getConfigValue(configKey, "name", String.class); instance.userAgent = getConfigValue(configKey, "user-agent", String.class); + instance.http2 = getConfigValue(configKey, "http2", Boolean.class); return instance; } @@ -311,6 +329,8 @@ public static RestClientConfig load(Class interfaceClass) { instance.shared = getConfigValue(interfaceClass, "shared", Boolean.class); instance.name = getConfigValue(interfaceClass, "name", String.class); instance.userAgent = getConfigValue(interfaceClass, "user-agent", String.class); + instance.http2 = getConfigValue(interfaceClass, "http2", Boolean.class); + instance.alpn = getConfigValue(interfaceClass, "alpn", Boolean.class); return instance; } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index c6e66c40876e3..0cc9e1eca802d 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -282,6 +282,21 @@ public class RestClientsConfig { @ConfigItem public Optional keyStoreType; + /** + * If this is true then HTTP/2 will be enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean http2; + + /** + * If the Application-Layer Protocol Negotiation is enabled, the client will negotiate which protocol to use over the + * protocols exposed by the server. By default, it will try to use HTTP/2 first and if it's not enabled, it will + * use HTTP/1.1. + * When the property `http2` is enabled, this flag will be automatically enabled. + */ + @ConfigItem + public Optional alpn; + public RestClientConfig getClientConfig(String configKey) { if (configKey == null) { return RestClientConfig.EMPTY; diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 7c342766d1e74..105a88f0a3e2c 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -337,6 +337,10 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.setUserAgent(restClientsConfig.userAgent.get()); } + if (getConfiguration().hasProperty(QuarkusRestClientProperties.HTTP2)) { + clientBuilder.http2((Boolean) getConfiguration().getProperty(QuarkusRestClientProperties.HTTP2)); + } + if (proxyHost != null) { configureProxy(proxyHost, proxyPort, proxyUser, proxyPassword, nonProxyHosts); } else if (restClientsConfig.proxyAddress.isPresent()) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index c74d840829b97..88d27766b8874 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -123,6 +123,16 @@ private void configureCustomProperties(RestClientBuilder builder) { if (userAgent.isPresent()) { builder.property(QuarkusRestClientProperties.USER_AGENT, userAgent.get()); } + + Boolean http2 = oneOf(clientConfigByClassName().http2, + clientConfigByConfigKey().http2).orElse(configRoot.http2); + builder.property(QuarkusRestClientProperties.HTTP2, http2); + + Optional alpn = oneOf(clientConfigByClassName().alpn, + clientConfigByConfigKey().alpn, configRoot.alpn); + if (alpn.isPresent()) { + builder.property(QuarkusRestClientProperties.ALPN, alpn.get()); + } } private void configureProxy(RestClientBuilderImpl builder) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java index b07920d8789c6..c51ea17bc8746 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java @@ -63,4 +63,14 @@ public class QuarkusRestClientProperties { public static final String USER_AGENT = "io.quarkus.rest.client.user-agent"; + /** + * Set to true to explicitly use the HTTP/2 version. + */ + public static final String HTTP2 = "io.quarkus.rest.client.http2"; + + /** + * Set to true to explicitly use the Application-Layer Protocol Negotiation extension. + */ + public static final String ALPN = "io.quarkus.rest.client.alpn"; + } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index f4c0d51855917..6120fc1bad74f 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -58,6 +58,7 @@ import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpVersion; import io.vertx.core.http.RequestOptions; import io.vertx.core.streams.Pipe; import io.vertx.core.streams.Pump; @@ -114,6 +115,9 @@ public void handle(Void ignored) { future.subscribe().with(new Consumer<>() { @Override public void accept(HttpClientRequest httpClientRequest) { + // adapt headers to HTTP/2 depending on the underlying HTTP connection + ClientSendRequestHandler.this.adaptRequest(httpClientRequest); + if (requestContext.isMultipart()) { Promise requestPromise = Promise.promise(); QuarkusMultipartFormUpload actualEntity; @@ -458,6 +462,7 @@ private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientR throw new IllegalArgumentException( "Multipart form upload expects an entity of type MultipartForm, got: " + state.getEntity().getEntity()); } + MultivaluedMap headerMap = state.getRequestHeaders().asMap(); updateRequestHeadersFromConfig(state, headerMap); QuarkusMultipartForm multipartForm = (QuarkusMultipartForm) state.getEntity().getEntity(); @@ -470,6 +475,7 @@ private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientR } QuarkusMultipartFormUpload multipartFormUpload = new QuarkusMultipartFormUpload(Vertx.currentContext(), multipartForm, true, mode); + httpClientRequest.setChunked(multipartFormUpload.isChunked()); setEntityRelatedHeaders(headerMap, state.getEntity()); // multipart has its own headers: @@ -507,6 +513,25 @@ private Buffer setRequestHeadersAndPrepareBody(HttpClientRequest httpClientReque return actualEntity; } + private void adaptRequest(HttpClientRequest request) { + if (request.version() == HttpVersion.HTTP_2) { + // When using the protocol HTTP/2, Netty which is internally used by Vert.x will validate the headers and reject + // the requests with invalid metadata. + // When we start a new connection, the Vert.x client will automatically upgrade the first request we make to be + // valid in HTTP/2. + // The problem is that in next requests, the Vert.x client reuses the same connection within the same window time + // and hence does not upgrade the following requests. Therefore, even though the first request works fine, the + // next requests won't work. + // This has been reported in https://github.com/eclipse-vertx/vert.x/issues/4618. + // To workaround this issue, we need to "upgrade" the next requests by ourselves when the version is already set + // to HTTP/2: + if (request.path() == null || request.path().length() == 0) { + // HTTP/2 does not allow empty paths + request.setURI(request.getURI() + "/"); + } + } + } + private void updateRequestHeadersFromConfig(RestClientRequestContext state, MultivaluedMap headerMap) { Object staticHeaders = state.getConfiguration().getProperty(QuarkusRestClientProperties.STATIC_HEADERS); if (staticHeaders instanceof Map) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 90bccd8b89c26..91ce16f5d6125 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -36,6 +36,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpVersion; import io.vertx.core.net.JksOptions; import io.vertx.core.net.ProxyOptions; @@ -54,6 +55,8 @@ public class ClientBuilderImpl extends ClientBuilder { private SSLContext sslContext; private KeyStore trustStore; private char[] trustStorePassword; + private boolean http2; + private boolean alpn; private String proxyHost; private int proxyPort; @@ -134,6 +137,16 @@ public ClientBuilder readTimeout(long timeout, TimeUnit unit) { return this; } + public ClientBuilder http2(boolean http2) { + this.http2 = http2; + return this; + } + + public ClientBuilder alpn(boolean alpn) { + this.alpn = alpn; + return this; + } + public ClientBuilder proxy(String proxyHost, int proxyPort) { this.proxyPort = proxyPort; this.proxyHost = proxyHost; @@ -182,6 +195,14 @@ public ClientImpl build() { HttpClientOptions options = Optional.ofNullable(configuration.getFromContext(HttpClientOptions.class)) .orElseGet(HttpClientOptions::new); + if (http2) { + options.setProtocolVersion(HttpVersion.HTTP_2); + } + + if (http2 || alpn) { + options.setUseAlpn(true); + options.setAlpnVersions(List.of(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); + } options.setVerifyHost(verifyHost); if (trustAll) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java index d867b6848bf97..e96e589754c26 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java @@ -147,6 +147,7 @@ public Vertx get() { Object connectionTTL = configuration.getProperty(CONNECTION_TTL); if (connectionTTL != null) { options.setKeepAliveTimeout((int) connectionTTL); + options.setHttp2KeepAliveTimeout((int) connectionTTL); } Object connectionPoolSize = configuration.getProperty(CONNECTION_POOL_SIZE); @@ -156,6 +157,7 @@ public Vertx get() { log.debugf("Setting connectionPoolSize to %d", connectionPoolSize); } options.setMaxPoolSize((int) connectionPoolSize); + options.setHttp2MaxPoolSize((int) connectionPoolSize); Object keepAliveEnabled = configuration.getProperty(KEEP_ALIVE_ENABLED); if (keepAliveEnabled != null) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java index 7a31e3421077d..9f72bf9af303b 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientResponseImpl.java @@ -67,4 +67,8 @@ restClientRequestContext.properties, restClientRequestContext, getStringHeaders( throw new ProcessingException(e); } } + + public String getHttpVersion() { + return restClientRequestContext.getVertxClientResponse().version().toString(); + } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/MultiByteHttpData.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/MultiByteHttpData.java index f85b6c136b5d4..4540b412abb99 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/MultiByteHttpData.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/MultiByteHttpData.java @@ -343,6 +343,11 @@ public void setContentTransferEncoding(String contentTransferEncoding) { this.contentTransferEncoding = contentTransferEncoding; } + @Override + public long length() { + return buffer.readableBytes() + super.length(); + } + @Override public String toString() { return HttpHeaderNames.CONTENT_DISPOSITION + ": " + diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java index aa906aa19d8b6..9d59b139f767b 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java @@ -44,7 +44,6 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.multipart.Attribute; @@ -855,7 +854,6 @@ public HttpRequest finalizeRequest() throws ErrorDataEncoderException { } } } - HttpUtil.setTransferEncodingChunked(request, true); // wrap to hide the possible content return new WrappedHttpRequest(request); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java index 5e8ffaa6d4d1f..b62fdd05e50a8 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java @@ -184,6 +184,10 @@ public void run() { } } + public boolean isChunked() { + return encoder.isChunked(); + } + private void handleError(Throwable e) { ended = true; request = null; diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index afc3b40eca44b..6d41e219f8048 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -298,6 +298,7 @@ rest-client resteasy-reactive-kotlin rest-client-reactive + rest-client-reactive-http2 rest-client-reactive-kotlin-serialization rest-client-reactive-multipart rest-client-reactive-stork diff --git a/integration-tests/rest-client-reactive-http2/pom.xml b/integration-tests/rest-client-reactive-http2/pom.xml new file mode 100644 index 0000000000000..b1af9b3bec01c --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/pom.xml @@ -0,0 +1,115 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + quarkus-integration-test-rest-client-reactive-http2 + Quarkus - Integration Tests - REST Client Reactive: HTTP/2 + + + + + io.quarkus + quarkus-rest-client-reactive + + + + io.quarkus + quarkus-resteasy-reactive + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + + + io.vertx + vertx-web-client + test + + + + + io.quarkus + quarkus-rest-client-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + + diff --git a/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Client.java b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Client.java new file mode 100644 index 0000000000000..e45b7fb048ef0 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Client.java @@ -0,0 +1,15 @@ +package io.quarkus.it.rest.client.http2; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@Path("/ping") +@RegisterRestClient(configKey = "basic-client") +public interface Client { + + @GET + Response ping(); +} diff --git a/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Resource.java b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Resource.java new file mode 100644 index 0000000000000..85511e3d23ba7 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/Resource.java @@ -0,0 +1,32 @@ +package io.quarkus.it.rest.client.http2; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.client.impl.ClientResponseImpl; + +@Path("") +public class Resource { + + @RestClient + Client client; + + @GET + @Path("/client/ping") + public Response client() { + Response response = client.ping(); + if (((ClientResponseImpl) response).getHttpVersion().equals("HTTP_2")) { + return response; + } + + return Response.noContent().build(); + } + + @GET + @Path("/ping") + public String ping() { + return "pong"; + } +} diff --git a/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartClient.java b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartClient.java new file mode 100644 index 0000000000000..1b97abcb2df39 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartClient.java @@ -0,0 +1,301 @@ +package io.quarkus.it.rest.client.http2.multipart; + +import java.io.File; +import java.util.UUID; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.PartType; + +import io.smallrye.mutiny.Multi; +import io.vertx.core.buffer.Buffer; + +@Path("/echo") +@RegisterRestClient(configKey = "multipart-client") +public interface MultipartClient { + + @POST + @Path("/octet-stream") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @Produces(MediaType.TEXT_PLAIN) + String octetStreamFile(File body); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendByteArrayAsBinaryFile(WithByteArrayAsBinaryFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendByteArrayAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) byte[] file, + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendMultiByteAsBinaryFile(WithMultiByteAsBinaryFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendMultiByteAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Multi file, + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendBufferAsBinaryFile(WithBufferAsBinaryFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendBufferAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Buffer file, + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendFileAsBinaryFile(WithFileAsBinaryFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendFileAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) File file, + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendPathAsBinaryFile(WithPathAsBinaryFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/binary") + String sendPathAsBinaryFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) java.nio.file.Path file, + @FormParam("fileName") @PartType(MediaType.TEXT_PLAIN) String fileName); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendByteArrayAsTextFile(WithByteArrayAsTextFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendByteArrayAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) byte[] file, + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendBufferAsTextFile(WithBufferAsTextFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendBufferAsTextFile(@FormParam("file") @PartType(MediaType.APPLICATION_OCTET_STREAM) Buffer file, + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendFileAsTextFile(WithFileAsTextFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendFileAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) File file, + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendPathAsTextFile(WithPathAsTextFile data); + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_PLAIN) + @Path("/text") + String sendPathAsTextFile(@FormParam("file") @PartType(MediaType.TEXT_PLAIN) java.nio.file.Path file, + @FormParam("number") @PartType(MediaType.TEXT_PLAIN) int number); + + class FileWithPojo { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public byte[] file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + private String fileName; + + @FormParam("pojo") + @PartType(MediaType.APPLICATION_JSON) + private Pojo pojo; + + @FormParam("uuid") + @PartType(MediaType.TEXT_PLAIN) + private UUID uuid; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Pojo getPojo() { + return pojo; + } + + public void setPojo(Pojo pojo) { + this.pojo = pojo; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + } + + class Pojo { + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + class WithByteArrayAsBinaryFile { + + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public byte[] file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + class WithMultiByteAsBinaryFile { + + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public Multi file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + class WithBufferAsBinaryFile { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public Buffer file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + class WithFileAsBinaryFile { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + class WithPathAsBinaryFile { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public java.nio.file.Path file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + class WithPathAsTextFile { + @FormParam("file") + @PartType(MediaType.TEXT_PLAIN) + public java.nio.file.Path file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public int number; + } + + class WithByteArrayAsTextFile { + + @FormParam("file") + @PartType(MediaType.TEXT_PLAIN) + public byte[] file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public int number; + } + + class WithBufferAsTextFile { + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public Buffer file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public int number; + } + + class WithFileAsTextFile { + @FormParam("file") + @PartType(MediaType.TEXT_PLAIN) + public File file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public int number; + } +} diff --git a/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartResource.java b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartResource.java new file mode 100644 index 0000000000000..2bb0cfae2cfbb --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/main/java/io/quarkus/it/rest/client/http2/multipart/MultipartResource.java @@ -0,0 +1,444 @@ +package io.quarkus.it.rest.client.http2.multipart; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestResponse; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Multi; +import io.vertx.core.buffer.Buffer; + +@Path("") +public class MultipartResource { + + private static final Logger log = Logger.getLogger(MultipartResource.class); + + public static final String HELLO_WORLD = "HELLO WORLD"; + public static final String GREETING_TXT = "greeting.txt"; + public static final int NUMBER = 12342; + @RestClient + MultipartClient client; + + @GET + @Path("/client/octet-stream") + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendOctetStreamFile() throws IOException { + java.nio.file.Path tempFile = Files.createTempFile("dummy", ".txt"); + Files.write(tempFile, "test".getBytes(UTF_8)); + return client.octetStreamFile(tempFile.toFile()); + } + + @GET + @Path("/client/byte-array-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArray(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + MultipartClient.WithByteArrayAsBinaryFile data = new MultipartClient.WithByteArrayAsBinaryFile(); + if (!nullFile) { + data.file = HELLO_WORLD.getBytes(UTF_8); + } + data.fileName = GREETING_TXT; + return client.sendByteArrayAsBinaryFile(data); + } + + @GET + @Path("/client/params/byte-array-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + byte[] file = null; + if (!nullFile) { + file = HELLO_WORLD.getBytes(UTF_8); + } + String fileName = GREETING_TXT; + return client.sendByteArrayAsBinaryFile(file, fileName); + } + + @GET + @Path("/client/multi-byte-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendMultiByte(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + MultipartClient.WithMultiByteAsBinaryFile data = new MultipartClient.WithMultiByteAsBinaryFile(); + if (!nullFile) { + List bytes = new ArrayList<>(); + for (byte b : HELLO_WORLD.getBytes(UTF_8)) { + bytes.add(b); + } + + data.file = Multi.createFrom().iterable(bytes); + } + data.fileName = GREETING_TXT; + return client.sendMultiByteAsBinaryFile(data); + } + + @GET + @Path("/client/params/multi-byte-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendMultiByteParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + Multi file = null; + if (!nullFile) { + List bytes = new ArrayList<>(); + for (byte b : HELLO_WORLD.getBytes(UTF_8)) { + bytes.add(b); + } + + file = Multi.createFrom().iterable(bytes); + } + String fileName = GREETING_TXT; + return client.sendMultiByteAsBinaryFile(file, fileName); + } + + @GET + @Path("/client/buffer-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBuffer(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + MultipartClient.WithBufferAsBinaryFile data = new MultipartClient.WithBufferAsBinaryFile(); + if (!nullFile) { + data.file = Buffer.buffer(HELLO_WORLD); + } + data.fileName = GREETING_TXT; + return client.sendBufferAsBinaryFile(data); + } + + @GET + @Path("/client/params/buffer-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBufferParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) { + Buffer file = null; + if (!nullFile) { + file = Buffer.buffer(HELLO_WORLD); + } + String fileName = GREETING_TXT; + return client.sendBufferAsBinaryFile(file, fileName); + } + + @GET + @Path("/client/file-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsBinary(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + MultipartClient.WithFileAsBinaryFile data = new MultipartClient.WithFileAsBinaryFile(); + + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + data.file = tempFile; + } + data.fileName = GREETING_TXT; + return client.sendFileAsBinaryFile(data); + } + + @GET + @Path("/client/params/file-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsBinaryParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + File file = null; + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + file = tempFile; + } + String fileName = GREETING_TXT; + return client.sendFileAsBinaryFile(file, fileName); + } + + @GET + @Path("/client/path-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsBinary(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + MultipartClient.WithPathAsBinaryFile data = new MultipartClient.WithPathAsBinaryFile(); + + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + data.file = tempFile.toPath(); + } + data.fileName = GREETING_TXT; + return client.sendPathAsBinaryFile(data); + } + + @GET + @Path("/client/params/path-as-binary-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsBinaryParams(@QueryParam("nullFile") @DefaultValue("false") boolean nullFile) throws IOException { + java.nio.file.Path file = null; + if (!nullFile) { + File tempFile = createTempHelloWorldFile(); + + file = tempFile.toPath(); + } + String fileName = GREETING_TXT; + return client.sendPathAsBinaryFile(file, fileName); + } + + @GET + @Path("/client/byte-array-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayAsTextFile() { + MultipartClient.WithByteArrayAsTextFile data = new MultipartClient.WithByteArrayAsTextFile(); + data.file = HELLO_WORLD.getBytes(UTF_8); + data.number = NUMBER; + return client.sendByteArrayAsTextFile(data); + } + + @GET + @Path("/client/params/byte-array-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendByteArrayAsTextFileParams() { + byte[] file = HELLO_WORLD.getBytes(UTF_8); + int number = NUMBER; + return client.sendByteArrayAsTextFile(file, number); + } + + @GET + @Path("/client/buffer-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBufferAsTextFile() { + MultipartClient.WithBufferAsTextFile data = new MultipartClient.WithBufferAsTextFile(); + data.file = Buffer.buffer(HELLO_WORLD); + data.number = NUMBER; + return client.sendBufferAsTextFile(data); + } + + @GET + @Path("/client/params/buffer-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendBufferAsTextFileParams() { + Buffer file = Buffer.buffer(HELLO_WORLD); + int number = NUMBER; + return client.sendBufferAsTextFile(file, number); + } + + @GET + @Path("/client/file-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsText() throws IOException { + File tempFile = createTempHelloWorldFile(); + + MultipartClient.WithFileAsTextFile data = new MultipartClient.WithFileAsTextFile(); + data.file = tempFile; + data.number = NUMBER; + return client.sendFileAsTextFile(data); + } + + @GET + @Path("/client/params/file-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendFileAsTextParams() throws IOException { + File tempFile = createTempHelloWorldFile(); + + File file = tempFile; + int number = NUMBER; + return client.sendFileAsTextFile(file, number); + } + + @GET + @Path("/client/path-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsText() throws IOException { + File tempFile = createTempHelloWorldFile(); + + MultipartClient.WithPathAsTextFile data = new MultipartClient.WithPathAsTextFile(); + data.file = tempFile.toPath(); + data.number = NUMBER; + return client.sendPathAsTextFile(data); + } + + @GET + @Path("/client/params/path-as-text-file") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String sendPathAsTextParams() throws IOException { + File tempFile = createTempHelloWorldFile(); + + java.nio.file.Path file = tempFile.toPath(); + int number = NUMBER; + return client.sendPathAsTextFile(file, number); + } + + @POST + @Path("/echo/octet-stream") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + public String consumeOctetStream(File file) throws IOException { + return Files.readString(file.toPath()); + } + + @POST + @Path("/echo/binary") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String consumeMultipart(MultipartBodyWithBinaryFile body) { + return String.format("fileOk:%s,nameOk:%s", body.file == null ? "null" : containsHelloWorld(body.file), + GREETING_TXT.equals(body.fileName)); + } + + @POST + @Path("/echo/text") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String consumeText(MultipartBodyWithTextFile2 body) { + return String.format("fileOk:%s,numberOk:%s", containsHelloWorld(body.file), + NUMBER == Integer.parseInt(body.number[0])); + } + + @POST + @Path("/echo/with-pojo") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String consumeBinaryWithPojo(MultipartBodyWithBinaryFileAndPojo fileWithPojo) { + return String.format("fileOk:%s,nameOk:%s,pojoOk:%s,uuidNull:%s", + containsHelloWorld(fileWithPojo.file), + GREETING_TXT.equals(fileWithPojo.fileName), + fileWithPojo.pojo == null ? "null" + : "some-name".equals(fileWithPojo.pojo.getName()) && "some-value".equals(fileWithPojo.pojo.getValue()), + fileWithPojo.uuid == null); + } + + @GET + @Path("/produces/multipart") + @Produces(MediaType.MULTIPART_FORM_DATA) + public MultipartBodyWithTextFile produceMultipart() throws IOException { + File tempFile = createTempHelloWorldFile(); + + MultipartBodyWithTextFile data = new MultipartBodyWithTextFile(); + data.file = tempFile; + data.number = String.valueOf(NUMBER); + return data; + } + + @GET + @Path("/produces/input-stream-rest-response") + public RestResponse produceInputStreamRestResponse() throws IOException { + File tempFile = createTempHelloWorldFile(); + FileInputStream is = new FileInputStream(tempFile); + return RestResponse.ResponseBuilder + .ok(is) + .type(MediaType.TEXT_PLAIN_TYPE) + .build(); + } + + private File createTempHelloWorldFile() throws IOException { + File tempFile = File.createTempFile("quarkus-test", ".bin"); + tempFile.deleteOnExit(); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) { + fileOutputStream.write(HELLO_WORLD.getBytes()); + } + return tempFile; + } + + private boolean containsHelloWorld(File file) { + try { + String actual = new String(Files.readAllBytes(file.toPath())); + return HELLO_WORLD.equals(actual); + } catch (IOException e) { + log.error("Failed to contents of uploaded file " + file.getAbsolutePath()); + return false; + } + } + + public static class MultipartBodyWithBinaryFile { + + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + } + + public static class MultipartBodyWithTextFile { + + @FormParam("file") + @PartType(MediaType.TEXT_PLAIN) + public File file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public String number; + } + + public static class MultipartBodyWithTextFile2 { + + @FormParam("file") + @PartType(MediaType.TEXT_PLAIN) + public File file; + + @FormParam("number") + @PartType(MediaType.TEXT_PLAIN) + public String[] number; + } + + public static class MultipartBodyWithBinaryFileAndPojo { + + @FormParam("file") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + + @FormParam("fileName") + @PartType(MediaType.TEXT_PLAIN) + public String fileName; + + @FormParam("pojo") + @PartType(MediaType.APPLICATION_JSON) + public MultipartClient.Pojo pojo; + + @FormParam("uuid") + @PartType(MediaType.TEXT_PLAIN) + public String uuid; + } + +} diff --git a/integration-tests/rest-client-reactive-http2/src/main/resources/application.properties b/integration-tests/rest-client-reactive-http2/src/main/resources/application.properties new file mode 100644 index 0000000000000..b9da60f180313 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/main/resources/application.properties @@ -0,0 +1,7 @@ +quarkus.rest-client.basic-client.url=${test.url} +quarkus.rest-client.basic-client.http2=true + +quarkus.rest-client.multipart-client.url=${test.url} +quarkus.rest-client.multipart-client.http2=true + +quarkus.http.http2=true \ No newline at end of file diff --git a/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceIT.java b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceIT.java new file mode 100644 index 0000000000000..dd5f7a4d56757 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.rest.client.http2; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ResourceIT extends ResourceTest { +} diff --git a/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceTest.java b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceTest.java new file mode 100644 index 0000000000000..6ca48e8e5eb0d --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/ResourceTest.java @@ -0,0 +1,82 @@ +package io.quarkus.it.rest.client.http2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.JdkSSLEngineOptions; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +@QuarkusTest +public class ResourceTest { + @TestHTTPResource(value = "/ping") + URL sslUrl; + + @TestHTTPResource(value = "/client/ping") + URL clientUrl; + + private WebClient webClient; + + @BeforeEach + public void setup() { + webClient = createWebClient(); + } + + @AfterEach + public void tearDown() { + if (webClient != null) { + webClient.close(); + } + } + + @Test + public void shouldReturnPongFromServer() throws Exception { + HttpResponse response = call(sslUrl); + assertEquals("pong", response.bodyAsString()); + assertEquals("HTTP_2", response.version().name()); + } + + @Test + public void shouldReturnPongFromClient() throws Exception { + HttpResponse response = call(clientUrl); + // if it's empty, it's because the REST Client is not using the HTTP/2 version + assertEquals("pong", response.bodyAsString()); + } + + private HttpResponse call(URL url) throws Exception { + CompletableFuture> result = new CompletableFuture<>(); + webClient.get(url.getPort(), url.getHost(), url.getPath()) + .send(ar -> { + if (ar.succeeded()) { + result.complete(ar.result()); + } else { + result.completeExceptionally(ar.cause()); + } + }); + + return result.get(); + } + + private WebClient createWebClient() { + Assumptions.assumeTrue(JdkSSLEngineOptions.isAlpnAvailable()); //don't run on JDK8 + Vertx vertx = Vertx.vertx(); + WebClientOptions options = new WebClientOptions() + .setUseAlpn(true) + .setProtocolVersion(HttpVersion.HTTP_2); + + return WebClient.create(vertx, options); + } +} diff --git a/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceIT.java b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceIT.java new file mode 100644 index 0000000000000..4b129b3db54a7 --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.rest.client.http2.multipart; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class MultipartResourceIT extends MultipartResourceTest { +} diff --git a/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceTest.java b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceTest.java new file mode 100644 index 0000000000000..8ee0d766b482e --- /dev/null +++ b/integration-tests/rest-client-reactive-http2/src/test/java/io/quarkus/it/rest/client/http2/multipart/MultipartResourceTest.java @@ -0,0 +1,278 @@ +package io.quarkus.it.rest.client.http2.multipart; + +import static io.quarkus.it.rest.client.http2.multipart.MultipartResource.HELLO_WORLD; +import static io.quarkus.it.rest.client.http2.multipart.MultipartResource.NUMBER; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; + +import jakarta.ws.rs.core.MediaType; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +@QuarkusTest +public class MultipartResourceTest { + + private static final String EXPECTED_CONTENT_DISPOSITION_PART = "Content-Disposition: form-data; name=\"%s\""; + private static final String EXPECTED_CONTENT_TYPE_PART = "Content-Type: %s"; + + @Test + public void shouldSendByteArrayAsBinaryFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/byte-array-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/byte-array-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + } + + @Test + public void shouldSendNullByteArrayAsBinaryFile() { + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/byte-array-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/params/byte-array-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + } + + @Test + public void shouldSendBufferAsBinaryFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/buffer-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/buffer-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + } + + @Test + public void shouldSendNullBufferAsBinaryFile() { + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/buffer-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/params/buffer-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + } + + @Test + public void shouldSendFileAsBinaryFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/file-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/file-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + } + + @Test + public void shouldMultiAsBinaryFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/multi-byte-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/multi-byte-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + } + + @Test + public void shouldSendNullFileAsBinaryFile() { + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/file-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/params/file-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + } + + @Test + public void shouldSendPathAsBinaryFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/path-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/path-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,nameOk:true")); + } + + @Test + public void shouldSendNullPathAsBinaryFile() { + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/path-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + given() + .queryParam("nullFile", "true") + .header("Content-Type", "text/plain") + .when().get("/client/params/path-as-binary-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:null,nameOk:true")); + } + + @Test + public void shouldSendByteArrayAsTextFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/byte-array-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/byte-array-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + } + + @Test + public void shouldSendBufferAsTextFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/buffer-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/buffer-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + } + + @Test + public void shouldSendFileAsTextFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/file-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/file-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + } + + @Test + public void shouldSendPathAsTextFile() { + given() + .header("Content-Type", "text/plain") + .when().get("/client/path-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + given() + .header("Content-Type", "text/plain") + .when().get("/client/params/path-as-text-file") + .then() + .statusCode(200) + .body(equalTo("fileOk:true,numberOk:true")); + } + + @Test + public void shouldProducesMultipartForm() { + String response = RestAssured.get("/produces/multipart") + .then() + .contentType(ContentType.MULTIPART) + .statusCode(200) + .extract().asString(); + + assertMultipartResponseContains(response, "number", MediaType.TEXT_PLAIN, NUMBER); + assertMultipartResponseContains(response, "file", MediaType.TEXT_PLAIN, HELLO_WORLD); + } + + @Test + public void shouldProperlyHandleOctetStreamFile() { + // @formatter:off + given() + .header("Content-Type", "text/plain") + .when().get("/client/octet-stream") + .then() + .statusCode(200) + .body(equalTo("test")); + // @formatter:on + } + + @Test + public void shouldProducesInputStreamRestResponse() { + RestAssured.get("/produces/input-stream-rest-response") + .then() + .contentType(ContentType.TEXT) + .statusCode(200) + .body(equalTo("HELLO WORLD")); + } + + private void assertMultipartResponseContains(String response, String name, String contentType, Object value) { + String[] lines = response.split("--"); + assertThat(lines).anyMatch(line -> line.contains(String.format(EXPECTED_CONTENT_DISPOSITION_PART, name)) + && line.contains(String.format(EXPECTED_CONTENT_TYPE_PART, contentType)) + && line.contains(value.toString())); + } +} diff --git a/integration-tests/rest-client-reactive-multipart/src/main/resources/larger-than-default-form-attribute.txt b/integration-tests/rest-client-reactive-multipart/src/main/resources/larger-than-default-form-attribute.txt deleted file mode 100644 index f1c8fd758d7d6..0000000000000 --- a/integration-tests/rest-client-reactive-multipart/src/main/resources/larger-than-default-form-attribute.txt +++ /dev/null @@ -1,3753 +0,0 @@ -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi -%% -Type: language -Subtag: an -Description: Aragonese -Added: 2005-10-16 -%% -Type: language -Subtag: ar -Description: Arabic -Added: 2005-10-16 -Suppress-Script: Arab -Scope: macrolanguage -%% -Type: language -Subtag: as -Description: Assamese -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: av -Description: Avaric -Added: 2005-10-16 -%% -Type: language -Subtag: ay -Description: Aymara -Added: 2005-10-16 -Suppress-Script: Latn -Scope: macrolanguage -%% -Type: language -Subtag: az -Description: Azerbaijani -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: ba -Description: Bashkir -Added: 2005-10-16 -%% -Type: language -Subtag: be -Description: Belarusian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bg -Description: Bulgarian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: bh -Description: Bihari languages -Added: 2005-10-16 -Scope: collection -%% -Type: language -Subtag: bi -Description: Bislama -Added: 2005-10-16 -%% -Type: language -Subtag: bm -Description: Bambara -Added: 2005-10-16 -%% -Type: language -Subtag: bn -Description: Bengali -Description: Bangla -Added: 2005-10-16 -Suppress-Script: Beng -%% -Type: language -Subtag: bo -Description: Tibetan -Added: 2005-10-16 -%% -Type: language -Subtag: br -Description: Breton -Added: 2005-10-16 -%% -Type: language -Subtag: bs -Description: Bosnian -Added: 2005-10-16 -Suppress-Script: Latn -Macrolanguage: sh -%% -Type: language -Subtag: ca -Description: Catalan -Description: Valencian -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ce -Description: -%% -Type: language -Subtag: ce -Description: -File-Date: 2018-04-23 -%% -Type: language -Subtag: aa -Description: Afar -Added: 2005-10-16 -%% -Type: language -Subtag: ab -Description: Abkhazian -Added: 2005-10-16 -Suppress-Script: Cyrl -%% -Type: language -Subtag: ae -Description: Avestan -Added: 2005-10-16 -%% -Type: language -Subtag: af -Description: Afrikaans -Added: 2005-10-16 -Suppress-Script: Latn -%% -Type: language -Subtag: ak -Description: Akan -Added: 2005-10-16 -Scope: macrolanguage -%% -Type: language -Subtag: am -Description: Amharic -Added: 2005-10-16 -Suppress-Script: Ethi diff --git a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java deleted file mode 100644 index 7c4bc1557480e..0000000000000 --- a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.quarkus.it.rest.client.multipart; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.equalTo; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.http.ContentType; -import io.vertx.core.http.HttpServerOptions; - -@Disabled -@QuarkusTest -public class LargerThanDefaultFormAttributeMultipartFormInputTest { - private final File FILE = new File("./src/main/resources/larger-than-default-form-attribute.txt"); - - @Test - public void test() throws IOException { - String fileContents = new String(Files.readAllBytes(FILE.toPath()), StandardCharsets.UTF_8); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 10; ++i) { - sb.append(fileContents); - } - fileContents = sb.toString(); - - Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); - given() - .multiPart("text", fileContents) - .accept("text/plain") - .when() - .post("/file") - .then() - .statusCode(200) - .contentType(ContentType.TEXT) - .body(equalTo(fileContents)); - } - -} From a9a6c97dc068d3007902817984a025c0dceacca6 Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 23 Feb 2023 11:22:50 +0100 Subject: [PATCH 2/2] Avoid to send an extra request with empty buffer --- .../multipart/QuarkusMultipartFormUpload.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java index b62fdd05e50a8..03514b349e572 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java @@ -2,7 +2,6 @@ import java.io.File; import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicInteger; import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; @@ -11,6 +10,7 @@ import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.MemoryFileUpload; @@ -148,25 +148,24 @@ public void run() { throw new IllegalArgumentException("Wrong Vert.x context used for multipart upload. Expected: " + context + ", actual: " + Vertx.currentContext()); } - AtomicInteger counter = new AtomicInteger(); while (!ended) { if (encoder.isChunked()) { try { HttpContent chunk = encoder.readChunk(ALLOC); if (chunk == PausableHttpPostRequestEncoder.WAIT_MARKER) { return; // resumption will be scheduled by encoder - } - ByteBuf content = chunk.content(); - Buffer buff = Buffer.buffer(content); - counter.incrementAndGet(); - boolean writable = pending.write(buff); - if (encoder.isEndOfInput()) { + } else if (chunk == LastHttpContent.EMPTY_LAST_CONTENT || encoder.isEndOfInput()) { ended = true; request = null; encoder = null; pending.write(InboundBuffer.END_SENTINEL); - } else if (!writable) { - break; + } else { + ByteBuf content = chunk.content(); + Buffer buff = Buffer.buffer(content); + boolean writable = pending.write(buff); + if (!writable) { + break; + } } } catch (Exception e) { handleError(e);