You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ck...@apache.org on 2020/09/14 15:15:59 UTC

[httpcomponents-core] branch master updated (9604361 -> 9f8d210)

This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git.


    omit 9604361  Update RELEASE_NOTES.txt
    omit 1d72cda  Updated release notes for HttpCore 5.1-beta1 release
    omit d05d6a9  Updated project URL
    omit f9374c0  HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)
    omit 19dea0f  Use decimal numbers for endpoint/execution IDs
    omit fa128ec  HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB
    omit 9dfd5f3  HTTPCORE-645: Chunked request streams reuse buffers between requests
    omit bf7759d  HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection
    omit ba87dac  HTTPCORE-643: Implement NullEntity for convenience (#209)
    omit 33eb3b5  RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
    omit 9885d2c  RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
    omit 4fce010  HTTPCORE-642: Implement ConnectionFactory fluent builders
     new ac73002  HTTPCORE-642: Implement ConnectionFactory fluent builders
     new 7acf66b  RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
     new 97096b6  RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
     new 1c5b283  HTTPCORE-643: Implement NullEntity for convenience (#209)
     new 5c4b08e  HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection
     new 81697f4  HTTPCORE-645: Chunked request streams reuse buffers between requests
     new adcd4cf  HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB
     new b7f1730  Use decimal numbers for endpoint/execution IDs
     new ac5754b  HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)
     new d8c3824  Updated project URL
     new 91b28d1  Updated release notes for HttpCore 5.1-beta1 release
     new 9d3339c  Update RELEASE_NOTES.txt
     new 9f8d210  Update RELEASE_NOTES.txt

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (9604361)
            \
             N -- N -- N   refs/heads/master (9f8d210)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 13 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 RELEASE_NOTES.txt | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)


[httpcomponents-core] 07/13: HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit adcd4cf0785638c63efedcf02b15979206336b7e
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:14:33 2020 -0400

    HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB
    
    This value matches the default session buffer size and avoids
    excessive memory copying between the chunk buffer and session
    buffer.
---
 .../main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java | 2 +-
 .../main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
index 95168ad..ecbb010 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
@@ -158,7 +158,7 @@ class BHttpConnectionBase implements BHttpConnection {
     private byte[] getChunkedRequestBuffer() {
         if (chunkedRequestBuffer == null) {
             final int chunkSizeHint = this.http1Config.getChunkSizeHint();
-            chunkedRequestBuffer = new byte[chunkSizeHint > 0 ? chunkSizeHint : 2048];
+            chunkedRequestBuffer = new byte[chunkSizeHint > 0 ? chunkSizeHint : 8192];
         }
         return chunkedRequestBuffer;
     }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
index b8ab5fd..23eb5e3 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
@@ -102,7 +102,7 @@ public class ChunkedOutputStream extends OutputStream {
             final OutputStream outputStream,
             final int chunkSizeHint,
             final Supplier<List<? extends Header>> trailerSupplier) {
-        this(buffer, outputStream, new byte[chunkSizeHint > 0 ? chunkSizeHint : 2048], trailerSupplier);
+        this(buffer, outputStream, new byte[chunkSizeHint > 0 ? chunkSizeHint : 8192], trailerSupplier);
     }
 
     /**


[httpcomponents-core] 11/13: Updated release notes for HttpCore 5.1-beta1 release

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 91b28d1ebe14e3cbce25bf8b1f13def03862c764
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon Sep 14 12:16:41 2020 +0200

    Updated release notes for HttpCore 5.1-beta1 release
---
 RELEASE_NOTES.txt | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 2522197..e130d97 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,3 +1,52 @@
+Release 5.1 BETA1
+------------------
+
+This is the first BETA release in the 5.1 release series that includes a number of
+new features as well performance optimizations in the classic HTTP transport.
+
+Notable changes and features included in the 5.1 series:
+
+* Conditional conformance with RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax).
+
+* Improved support for out of sequence response message handing by the the classic (blocking)
+  HTTP transport.
+
+
+Change Log
+-------------------
+
+* HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)
+  Contributed by Carter Kozak <c4kofony at gmail.com>
+
+* Use decimal numbers for endpoint/execution IDs
+  Contributed by Michael Osipov <michaelo at apache.org>
+
+* HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB.
+  Contributed by Carter Kozak <c4kofony at gmail.com>
+
+* HTTPCORE-645: Chunked request streams reuse buffers between requests.
+  Contributed by Carter Kozak <c4kofony at gmail.com>
+
+* HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection.
+  Contributed by Carter Kozak <c4kofony at gmail.com>
+
+* RFC 3986 conformance: support percent-encoded reserved characters in the host component.
+  Contributed by Oleg Kalnichevski <olegk at apache.org>
+
+* RFC 3986 conformance: revised URI parsing and formatting.
+  Contributed by Oleg Kalnichevski <olegk at apache.org>
+
+* Better parse and format methods for URIAuthority.
+  Contributed by Oleg Kalnichevski <olegk at apache.org>
+
+* HTTPCORE-628: do not encode blanks as + in URI query component.
+  Contributed by Oleg Kalnichevski <olegk at apache.org>
+
+* Async connection listeners to support passing attachments to endpoints.
+  Contributed by Oleg Kalnichevski <olegk at apache.org>
+
+
+
 Release 5.0.2
 ------------------
 


[httpcomponents-core] 01/13: HTTPCORE-642: Implement ConnectionFactory fluent builders

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit ac730022a325d3eeca60d9fbfee174817b2368bd
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:13:20 2020 -0400

    HTTPCORE-642: Implement ConnectionFactory fluent builders
    
    This closes #208
---
 .../io/DefaultBHttpClientConnectionFactory.java    | 66 +++++++++++++++++++
 .../io/DefaultBHttpServerConnectionFactory.java    | 73 ++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
index e9781df..37c2e48 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
@@ -106,4 +106,70 @@ public class DefaultBHttpClientConnectionFactory
         return conn;
     }
 
+    /**
+     * Create a new {@link Builder}.
+     *
+     * @since 5.1
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link DefaultBHttpClientConnectionFactory}.
+     *
+     * @since 5.1
+     */
+    public static final class Builder {
+        private Http1Config http1Config;
+        private CharCodingConfig charCodingConfig;
+        private ContentLengthStrategy incomingContentLengthStrategy;
+        private ContentLengthStrategy outgoingContentLengthStrategy;
+        private HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
+        private HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
+
+        private Builder() {}
+
+        public Builder http1Config(final Http1Config http1Config) {
+            this.http1Config = http1Config;
+            return this;
+        }
+
+        public Builder charCodingConfig(final CharCodingConfig charCodingConfig) {
+            this.charCodingConfig = charCodingConfig;
+            return this;
+        }
+
+        public Builder incomingContentLengthStrategy(final ContentLengthStrategy incomingContentLengthStrategy) {
+            this.incomingContentLengthStrategy = incomingContentLengthStrategy;
+            return this;
+        }
+
+        public Builder outgoingContentLengthStrategy(final ContentLengthStrategy outgoingContentLengthStrategy) {
+            this.outgoingContentLengthStrategy = outgoingContentLengthStrategy;
+            return this;
+        }
+
+        public Builder requestWriterFactory(
+                final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory) {
+            this.requestWriterFactory = requestWriterFactory;
+            return this;
+        }
+
+        public Builder responseParserFactory(
+                final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+            this.responseParserFactory = responseParserFactory;
+            return this;
+        }
+
+        public DefaultBHttpClientConnectionFactory build() {
+            return new DefaultBHttpClientConnectionFactory(
+                    http1Config,
+                    charCodingConfig,
+                    incomingContentLengthStrategy,
+                    outgoingContentLengthStrategy,
+                    requestWriterFactory,
+                    responseParserFactory);
+        }
+    }
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
index d74c528..8a67f1e 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpServerConnectionFactory.java
@@ -107,4 +107,77 @@ public class DefaultBHttpServerConnectionFactory implements HttpConnectionFactor
         return conn;
     }
 
+    /**
+     * Create a new {@link Builder}.
+     *
+     * @since 5.1
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder for {@link DefaultBHttpServerConnectionFactory}.
+     *
+     * @since 5.1
+     */
+    public static final class Builder {
+        private String scheme;
+        private Http1Config http1Config;
+        private CharCodingConfig charCodingConfig;
+        private ContentLengthStrategy incomingContentLengthStrategy;
+        private ContentLengthStrategy outgoingContentLengthStrategy;
+        private HttpMessageParserFactory<ClassicHttpRequest> requestParserFactory;
+        private HttpMessageWriterFactory<ClassicHttpResponse> responseWriterFactory;
+
+        private Builder() {}
+
+        public Builder scheme(final String scheme) {
+            this.scheme = scheme;
+            return this;
+        }
+
+        public Builder http1Config(final Http1Config http1Config) {
+            this.http1Config = http1Config;
+            return this;
+        }
+
+        public Builder charCodingConfig(final CharCodingConfig charCodingConfig) {
+            this.charCodingConfig = charCodingConfig;
+            return this;
+        }
+
+        public Builder incomingContentLengthStrategy(final ContentLengthStrategy incomingContentLengthStrategy) {
+            this.incomingContentLengthStrategy = incomingContentLengthStrategy;
+            return this;
+        }
+
+        public Builder outgoingContentLengthStrategy(final ContentLengthStrategy outgoingContentLengthStrategy) {
+            this.outgoingContentLengthStrategy = outgoingContentLengthStrategy;
+            return this;
+        }
+
+        public Builder requestParserFactory(
+                final HttpMessageParserFactory<ClassicHttpRequest> requestParserFactory) {
+            this.requestParserFactory = requestParserFactory;
+            return this;
+        }
+
+        public Builder responseWriterFactory(
+                final HttpMessageWriterFactory<ClassicHttpResponse> responseWriterFactory) {
+            this.responseWriterFactory = responseWriterFactory;
+            return this;
+        }
+
+        public DefaultBHttpServerConnectionFactory build() {
+            return new DefaultBHttpServerConnectionFactory(
+                    scheme,
+                    http1Config,
+                    charCodingConfig,
+                    incomingContentLengthStrategy,
+                    outgoingContentLengthStrategy,
+                    requestParserFactory,
+                    responseWriterFactory);
+        }
+    }
 }


[httpcomponents-core] 04/13: HTTPCORE-643: Implement NullEntity for convenience (#209)

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 1c5b283e4350921fdaf83cda42c917c13bef39a0
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:13:36 2020 -0400

    HTTPCORE-643: Implement NullEntity for convenience (#209)
---
 .../hc/core5/http/impl/io/BHttpConnectionBase.java |   1 +
 .../hc/core5/http/impl/io/EmptyInputStream.java    |   2 +
 .../hc/core5/http/impl/io/IncomingHttpEntity.java  |   1 +
 .../hc/core5/http/io/entity/BasicHttpEntity.java   |   1 -
 .../{impl/io => io/entity}/EmptyInputStream.java   |   4 +-
 .../entity/NullEntity.java}                        | 104 ++++++++-------------
 .../core5/http/io/entity/TestBasicHttpEntity.java  |   1 -
 .../http/io/entity/TestInputStreamEntity.java      |   1 -
 .../hc/core5/http/io/entity/TestNullEntity.java    |  90 ++++++++++++++++++
 .../http/protocol/TestStandardInterceptors.java    |   2 +-
 10 files changed, 134 insertions(+), 73 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
index 4d8a80c..10683a5 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
@@ -58,6 +58,7 @@ import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
 import org.apache.hc.core5.http.io.BHttpConnection;
 import org.apache.hc.core5.http.io.SessionInputBuffer;
 import org.apache.hc.core5.http.io.SessionOutputBuffer;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.io.CloseMode;
 import org.apache.hc.core5.io.Closer;
 import org.apache.hc.core5.net.InetAddressUtils;
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
index 8508245..1c3d2e4 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
@@ -31,7 +31,9 @@ import java.io.InputStream;
 
 /**
  * @since 4.4
+ * @deprecated Please use {@link org.apache.hc.core5.http.io.entity.EmptyInputStream}
  */
+@Deprecated
 public final class EmptyInputStream extends InputStream {
 
     public static final EmptyInputStream INSTANCE = new EmptyInputStream();
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
index e831325..ee1228f 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
@@ -38,6 +38,7 @@ import org.apache.hc.core5.function.Supplier;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.io.Closer;
 
 class IncomingHttpEntity implements HttpEntity {
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
index 1d9407e..730ac91 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/BasicHttpEntity.java
@@ -33,7 +33,6 @@ import java.io.InputStream;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.apache.hc.core5.io.Closer;
 import org.apache.hc.core5.util.Args;
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
similarity index 97%
copy from httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
copy to httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
index 8508245..294952b 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/EmptyInputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EmptyInputStream.java
@@ -25,12 +25,12 @@
  *
  */
 
-package org.apache.hc.core5.http.impl.io;
+package org.apache.hc.core5.http.io.entity;
 
 import java.io.InputStream;
 
 /**
- * @since 4.4
+ * @since 5.1
  */
 public final class EmptyInputStream extends InputStream {
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
similarity index 53%
copy from httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
copy to httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
index e831325..b4b1805 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/IncomingHttpEntity.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/NullEntity.java
@@ -25,7 +25,13 @@
  *
  */
 
-package org.apache.hc.core5.http.impl.io;
+package org.apache.hc.core5.http.io.entity;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -34,103 +40,67 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
-import org.apache.hc.core5.function.Supplier;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpEntity;
-import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
-import org.apache.hc.core5.io.Closer;
-
-class IncomingHttpEntity implements HttpEntity {
-
-    private final InputStream content;
-    private final long len;
-    private final boolean chunked;
-    private final Header contentType;
-    private final Header contentEncoding;
-
-    IncomingHttpEntity(final InputStream content, final long len, final boolean chunked, final Header contentType, final Header contentEncoding) {
-        this.content = content;
-        this.len = len;
-        this.chunked = chunked;
-        this.contentType = contentType;
-        this.contentEncoding = contentEncoding;
-    }
+/**
+ * An empty entity with no content-type. This type may be used for convenience
+ * in place of an empty {@link ByteArrayEntity}.
+ *
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class NullEntity implements HttpEntity {
 
-    @Override
-    public boolean isRepeatable() {
-        return false;
-    }
+    public static final NullEntity INSTANCE = new NullEntity();
+
+    private NullEntity() {}
 
     @Override
-    public boolean isChunked() {
-        return chunked;
+    public boolean isRepeatable() {
+        return true;
     }
 
     @Override
-    public long getContentLength() {
-        return len;
+    public InputStream getContent() throws IOException, UnsupportedOperationException {
+        return EmptyInputStream.INSTANCE;
     }
 
     @Override
-    public String getContentType() {
-        return contentType != null ? contentType.getValue() : null;
-    }
+    public void writeTo(final OutputStream outStream) throws IOException {}
 
     @Override
-    public String getContentEncoding() {
-        return contentEncoding != null ? contentEncoding.getValue() : null;
+    public boolean isStreaming() {
+        return false;
     }
 
     @Override
-    public InputStream getContent() throws IOException, IllegalStateException {
-        return content;
+    public Supplier<List<? extends Header>> getTrailers() {
+        return null;
     }
 
     @Override
-    public boolean isStreaming() {
-        return content != null && content != EmptyInputStream.INSTANCE;
-    }
+    public void close() throws IOException {}
 
     @Override
-    public void writeTo(final OutputStream outStream) throws IOException {
-        AbstractHttpEntity.writeTo(this, outStream);
+    public long getContentLength() {
+        return 0;
     }
 
     @Override
-    public Supplier<List<? extends Header>> getTrailers() {
+    public String getContentType() {
         return null;
     }
 
     @Override
-    public Set<String> getTrailerNames() {
-        return Collections.emptySet();
+    public String getContentEncoding() {
+        return null;
     }
 
     @Override
-    public void close() throws IOException {
-        Closer.close(content);
+    public boolean isChunked() {
+        return false;
     }
 
     @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append('[');
-        sb.append("Content-Type: ");
-        sb.append(getContentType());
-        sb.append(',');
-        sb.append("Content-Encoding: ");
-        sb.append(getContentEncoding());
-        sb.append(',');
-        final long len = getContentLength();
-        if (len >= 0) {
-            sb.append("Content-Length: ");
-            sb.append(len);
-            sb.append(',');
-        }
-        sb.append("Chunked: ");
-        sb.append(isChunked());
-        sb.append(']');
-        return sb.toString();
+    public Set<String> getTrailerNames() {
+        return Collections.emptySet();
     }
-
 }
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
index b2ac16e..8012585 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestBasicHttpEntity.java
@@ -32,7 +32,6 @@ import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
index 04f937b..35e0e17 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestInputStreamEntity.java
@@ -33,7 +33,6 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 
 import org.apache.hc.core5.http.ContentType;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java
new file mode 100644
index 0000000..c2abce3
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestNullEntity.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.io.entity;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+public class TestNullEntity {
+
+    @Test
+    public void testLength() {
+        assertEquals(0, NullEntity.INSTANCE.getContentLength());
+    }
+
+    @Test
+    public void testContentType() {
+        assertNull(NullEntity.INSTANCE.getContentType());
+    }
+
+    @Test
+    public void testContentEncoding() {
+        assertNull(NullEntity.INSTANCE.getContentEncoding());
+    }
+
+    @Test
+    public void testTrailerNames() {
+        assertEquals(Collections.emptySet(), NullEntity.INSTANCE.getTrailerNames());
+    }
+
+    @Test
+    public void testContentStream() throws IOException {
+        try (InputStream content = NullEntity.INSTANCE.getContent()) {
+            assertEquals(-1, content.read());
+        }
+        // Closing the resource should have no impact
+        try (InputStream content = NullEntity.INSTANCE.getContent()) {
+            assertEquals(-1, content.read());
+        }
+    }
+
+    @Test
+    public void testWriteTo() throws IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        NullEntity.INSTANCE.writeTo(baos);
+        assertEquals(0, baos.size());
+    }
+
+    @Test
+    public void testIsStreaming() {
+        assertFalse(NullEntity.INSTANCE.isStreaming());
+    }
+
+    @Test
+    public void testIsChunked() {
+        assertFalse(NullEntity.INSTANCE.isChunked());
+    }
+}
\ No newline at end of file
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
index 8ed0f1e..cef1f8a 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestStandardInterceptors.java
@@ -38,7 +38,7 @@ import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.Method;
 import org.apache.hc.core5.http.ProtocolException;
-import org.apache.hc.core5.http.impl.io.EmptyInputStream;
+import org.apache.hc.core5.http.io.entity.EmptyInputStream;
 import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
 import org.apache.hc.core5.http.io.entity.HttpEntities;
 import org.apache.hc.core5.http.io.entity.StringEntity;


[httpcomponents-core] 10/13: Updated project URL

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit d8c3824a8d105808695b97485c74d66fb731c8b5
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sat Sep 12 14:33:27 2020 +0200

    Updated project URL
---
 httpcore5-h2/pom.xml       | 2 +-
 httpcore5-reactive/pom.xml | 2 +-
 httpcore5-testing/pom.xml  | 2 +-
 httpcore5/pom.xml          | 2 +-
 pom.xml                    | 4 ++--
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/httpcore5-h2/pom.xml b/httpcore5-h2/pom.xml
index 5ce7ed5..004d5e2 100644
--- a/httpcore5-h2/pom.xml
+++ b/httpcore5-h2/pom.xml
@@ -33,7 +33,7 @@
   <artifactId>httpcore5-h2</artifactId>
   <name>Apache HttpComponents Core HTTP/2</name>
   <description>Apache HttpComponents HTTP/2 Core Components</description>
-  <url>https://hc.apache.org/httpcomponents-core-5.0.x/</url>
+  <url>https://hc.apache.org/httpcomponents-core-5.1.x/</url>
   <packaging>jar</packaging>
 
   <properties>
diff --git a/httpcore5-reactive/pom.xml b/httpcore5-reactive/pom.xml
index bb102d4..4fe7aa4 100644
--- a/httpcore5-reactive/pom.xml
+++ b/httpcore5-reactive/pom.xml
@@ -34,7 +34,7 @@
   <artifactId>httpcore5-reactive</artifactId>
   <name>Apache HttpComponents Core Reactive Extensions</name>
   <description>Apache HttpComponents Reactive Streams Bindings</description>
-  <url>https://hc.apache.org/httpcomponents-core-5.0.x/</url>
+  <url>https://hc.apache.org/httpcomponents-core-5.1.x/</url>
   <packaging>jar</packaging>
 
   <properties>
diff --git a/httpcore5-testing/pom.xml b/httpcore5-testing/pom.xml
index 52e05f3..2d11b5a 100644
--- a/httpcore5-testing/pom.xml
+++ b/httpcore5-testing/pom.xml
@@ -33,7 +33,7 @@
   <artifactId>httpcore5-testing</artifactId>
   <name>Apache HttpComponents Core Integration Tests</name>
   <description>Apache HttpComponents HTTP/2 and HTTP/1.1 core component integration tests</description>
-  <url>https://hc.apache.org/httpcomponents-core-5.0.x/</url>
+  <url>https://hc.apache.org/httpcomponents-core-5.1.x/</url>
   <packaging>jar</packaging>
 
   <properties>
diff --git a/httpcore5/pom.xml b/httpcore5/pom.xml
index d80cd46..3cfe0b9 100644
--- a/httpcore5/pom.xml
+++ b/httpcore5/pom.xml
@@ -34,7 +34,7 @@
   <name>Apache HttpComponents Core HTTP/1.1</name>
   <inceptionYear>2005</inceptionYear>
   <description>Apache HttpComponents HTTP/1.1 core components</description>
-  <url>https://hc.apache.org/httpcomponents-core-5.0.x/</url>
+  <url>https://hc.apache.org/httpcomponents-core-5.1.x/</url>
   <packaging>jar</packaging>
 
   <properties>
diff --git a/pom.xml b/pom.xml
index 893ab85..414c99b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
   <name>Apache HttpComponents Core Parent</name>
   <version>5.1-alpha1-SNAPSHOT</version>
   <description>Apache HttpComponents Core is a library of components for building HTTP enabled services</description>
-  <url>https://hc.apache.org/httpcomponents-core-5.0.x/</url>
+  <url>https://hc.apache.org/httpcomponents-core-5.1.x/</url>
   <inceptionYear>2005</inceptionYear>
   <packaging>pom</packaging>
 
@@ -48,7 +48,7 @@
     <connection>scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-core.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/httpcomponents-core.git</developerConnection>
     <url>https://github.com/apache/httpcomponents-core/tree/${project.scm.tag}</url>
-    <tag>5.1-alpha1-SNAPSHOT</tag>
+    <tag>master</tag>
   </scm>
 
   <modules>


[httpcomponents-core] 03/13: RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 97096b6da2d213c092481f3853041f4e8945b92c
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sat Jul 25 12:47:27 2020 +0200

    RFC 3986 conformance: support percent-encoded reserved characters in the host component; host component can be empty
---
 .../main/java/org/apache/hc/core5/net/URIAuthority.java    | 10 +---------
 .../src/main/java/org/apache/hc/core5/net/URIBuilder.java  | 14 ++++++++++++--
 .../java/org/apache/hc/core5/net/TestURIAuthority.java     | 14 ++++----------
 .../test/java/org/apache/hc/core5/net/TestURIBuilder.java  | 14 ++++++++++++++
 4 files changed, 31 insertions(+), 21 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
index 1cae817..920c426 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIAuthority.java
@@ -34,7 +34,6 @@ import java.util.Locale;
 
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.LangUtils;
 import org.apache.hc.core5.util.TextUtils;
 import org.apache.hc.core5.util.Tokenizer;
@@ -104,9 +103,6 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
         } else {
             hostName = token;
         }
-        if (TextUtils.isBlank(hostName)) {
-            throw createException(s, cursor, "Authority host is empty");
-        }
         final int port;
         if (!TextUtils.isBlank(portText)) {
             try {
@@ -162,12 +158,8 @@ public final class URIAuthority implements NamedEndpoint, Serializable {
      */
     public URIAuthority(final String userInfo, final String hostname, final int port) {
         super();
-        Args.containsNoBlanks(hostname, "Host name");
-        if (userInfo != null) {
-            Args.containsNoBlanks(userInfo, "User info");
-        }
         this.userInfo = userInfo;
-        this.hostname = hostname.toLowerCase(Locale.ROOT);
+        this.hostname = hostname != null ? hostname.toLowerCase(Locale.ROOT) : null;
         this.port = Ports.checkWithDefault(port);
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
index 70358e2..c8f3fa7 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
@@ -292,7 +292,7 @@ public class URIBuilder {
                 if (InetAddressUtils.isIPv6Address(this.host)) {
                     sb.append("[").append(this.host).append("]");
                 } else {
-                    sb.append(this.host);
+                    sb.append(PercentCodec.encode(this.host, this.charset));
                 }
                 if (this.port >= 0) {
                     sb.append(":").append(this.port);
@@ -336,6 +336,16 @@ public class URIBuilder {
         this.port = uri.getPort();
         this.encodedUserInfo = uri.getRawUserInfo();
         this.userInfo = uri.getUserInfo();
+        if (this.encodedAuthority != null && this.host == null) {
+            try {
+                final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
+                this.encodedUserInfo = uriAuthority.getUserInfo();
+                this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
+                this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
+                this.port = uriAuthority.getPort();
+            } catch (final URISyntaxException ignore) {
+            }
+        }
         this.encodedPath = uri.getRawPath();
         this.pathSegments = parsePath(uri.getRawPath(), charset);
         this.pathRootless = uri.getRawPath() != null && !uri.getRawPath().startsWith("/");
@@ -447,7 +457,7 @@ public class URIBuilder {
      * @return this.
      */
     public URIBuilder setHost(final String host) {
-        this.host = !TextUtils.isBlank(host) ? host : null;
+        this.host = host;
         this.encodedSchemeSpecificPart = null;
         this.encodedAuthority = null;
         return this;
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
index 76c3b41..3aa1986 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIAuthority.java
@@ -129,16 +129,10 @@ public class TestURIAuthority {
                 CoreMatchers.equalTo(new URIAuthority("somehost", -1)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost#blah"),
                 CoreMatchers.equalTo(new URIAuthority("somehost", -1)));
-        try {
-            URIAuthority.create("aaaa@:8080");
-            Assert.fail("URISyntaxException expected");
-        } catch (final URISyntaxException expected) {
-        }
-        try {
-            URIAuthority.create("@:");
-            Assert.fail("URISyntaxException expected");
-        } catch (final URISyntaxException expected) {
-        }
+        MatcherAssert.assertThat(URIAuthority.parse("aaaa@:8080"),
+                CoreMatchers.equalTo(new URIAuthority("aaaa", "", 8080)));
+        MatcherAssert.assertThat(URIAuthority.parse("@:"),
+                CoreMatchers.equalTo(new URIAuthority(null, "", -1)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost:8080"),
                 CoreMatchers.equalTo(new URIAuthority("somehost", 8080)));
         MatcherAssert.assertThat(URIAuthority.parse("somehost:8080/blah"),
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
index ea153cf..8e5b334 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
@@ -512,6 +512,20 @@ public class TestURIBuilder {
     }
 
     @Test
+    public void testSetHostWithReservedChars() throws Exception {
+        final URIBuilder uribuilder = new URIBuilder();
+        uribuilder.setScheme("http").setHost("!example!.com");
+        Assert.assertEquals(URI.create("http://%21example%21.com"), uribuilder.build());
+    }
+
+    @Test
+    public void testGetHostWithReservedChars() throws Exception {
+        final URIBuilder uribuilder = new URIBuilder("http://someuser%21@%21example%21.com/");
+        Assert.assertEquals("!example!.com", uribuilder.getHost());
+        Assert.assertEquals("someuser!", uribuilder.getUserInfo());
+    }
+
+    @Test
     public void testMultipleLeadingPathSlashes() throws Exception {
         final URI uri = new URIBuilder()
                 .setScheme("ftp")


[httpcomponents-core] 13/13: Update RELEASE_NOTES.txt

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 9f8d21067f114a9a5501d9a4d783ddc54096f693
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 10:48:45 2020 -0400

    Update RELEASE_NOTES.txt
    
    Use updated ckozak email address
---
 RELEASE_NOTES.txt | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index ed9f8bb..1f1108a 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -16,19 +16,19 @@ Change Log
 -------------------
 
 * HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)
-  Contributed by Carter Kozak <c4kofony at gmail.com>
+  Contributed by Carter Kozak <ckozak at apache.org>
 
 * Use decimal numbers for endpoint/execution IDs
   Contributed by Michael Osipov <michaelo at apache.org>
 
 * HTTPCORE-645: Increase blocking default chunk size from 2 KiB to 8 KiB.
-  Contributed by Carter Kozak <c4kofony at gmail.com>
+  Contributed by Carter Kozak <ckozak at apache.org>
 
 * HTTPCORE-645: Chunked request streams reuse buffers between requests.
-  Contributed by Carter Kozak <c4kofony at gmail.com>
+  Contributed by Carter Kozak <ckozak at apache.org>
 
 * HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection.
-  Contributed by Carter Kozak <c4kofony at gmail.com>
+  Contributed by Carter Kozak <ckozak at apache.org>
 
 * RFC 3986 conformance: Support percent-encoded reserved characters in the host component.
   Contributed by Oleg Kalnichevski <olegk at apache.org>


[httpcomponents-core] 06/13: HTTPCORE-645: Chunked request streams reuse buffers between requests

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 81697f46635f24576e84582bf9e65b99470717d1
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:14:13 2020 -0400

    HTTPCORE-645: Chunked request streams reuse buffers between requests
    
    This adds a lazily initialized reusable buffer to the connection
    which is reused for all chunked request entities to avoid expensive
    array allocations each time a chunked request entity is sent.
---
 .../hc/core5/http/impl/io/BHttpConnectionBase.java | 13 +++++++++--
 .../hc/core5/http/impl/io/ChunkedOutputStream.java | 26 ++++++++++++++++++----
 2 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
index 10683a5..95168ad 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/BHttpConnectionBase.java
@@ -72,6 +72,8 @@ class BHttpConnectionBase implements BHttpConnection {
     final SessionOutputBufferImpl outbuffer;
     final BasicHttpConnectionMetrics connMetrics;
     final AtomicReference<SocketHolder> socketHolderRef;
+    // Lazily initialized chunked request buffer provided to ChunkedOutputStream.
+    private byte[] chunkedRequestBuffer;
 
     volatile ProtocolVersion version;
     volatile EndpointDetails endpointDetails;
@@ -147,13 +149,20 @@ class BHttpConnectionBase implements BHttpConnection {
         if (len >= 0) {
             return new ContentLengthOutputStream(buffer, outputStream, len);
         } else if (len == ContentLengthStrategy.CHUNKED) {
-            final int chunkSizeHint = http1Config.getChunkSizeHint() >= 0 ? http1Config.getChunkSizeHint() : 2048;
-            return new ChunkedOutputStream(buffer, outputStream, chunkSizeHint, trailers);
+            return new ChunkedOutputStream(buffer, outputStream, getChunkedRequestBuffer(), trailers);
         } else {
             return new IdentityOutputStream(buffer, outputStream);
         }
     }
 
+    private byte[] getChunkedRequestBuffer() {
+        if (chunkedRequestBuffer == null) {
+            final int chunkSizeHint = this.http1Config.getChunkSizeHint();
+            chunkedRequestBuffer = new byte[chunkSizeHint > 0 ? chunkSizeHint : 2048];
+        }
+        return chunkedRequestBuffer;
+    }
+
     protected InputStream createContentInputStream(
             final long len,
             final SessionInputBuffer buffer,
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
index 91a0790..b8ab5fd 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ChunkedOutputStream.java
@@ -69,25 +69,43 @@ public class ChunkedOutputStream extends OutputStream {
      *
      * @param buffer Session output buffer
      * @param outputStream Output stream
-     * @param chunkSizeHint minimal chunk size hint
+     * @param chunkCache Buffer used to aggregate smaller writes into chunks.
      * @param trailerSupplier Trailer supplier. May be {@code null}
      *
-     * @since 5.0
+     * @since 5.1
      */
     public ChunkedOutputStream(
             final SessionOutputBuffer buffer,
             final OutputStream outputStream,
-            final int chunkSizeHint,
+            final byte[] chunkCache,
             final Supplier<List<? extends Header>> trailerSupplier) {
         super();
         this.buffer = Args.notNull(buffer, "Session output buffer");
         this.outputStream = Args.notNull(outputStream, "Output stream");
-        this.cache = new byte[chunkSizeHint > 0 ? chunkSizeHint : 2048];
+        this.cache = Args.notNull(chunkCache, "Chunk cache");
         this.lineBuffer = new CharArrayBuffer(32);
         this.trailerSupplier = trailerSupplier;
     }
 
     /**
+     * Constructor taking an integer chunk size hint.
+     *
+     * @param buffer Session output buffer
+     * @param outputStream Output stream
+     * @param chunkSizeHint minimal chunk size hint
+     * @param trailerSupplier Trailer supplier. May be {@code null}
+     *
+     * @since 5.0
+     */
+    public ChunkedOutputStream(
+            final SessionOutputBuffer buffer,
+            final OutputStream outputStream,
+            final int chunkSizeHint,
+            final Supplier<List<? extends Header>> trailerSupplier) {
+        this(buffer, outputStream, new byte[chunkSizeHint > 0 ? chunkSizeHint : 2048], trailerSupplier);
+    }
+
+    /**
      * Constructor with no trailers.
      *
      * @param buffer Session output buffer


[httpcomponents-core] 02/13: RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 7acf66b68f7b5db64378eaaf34bacee1c4080da9
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon Jul 20 17:30:22 2020 +0200

    RFC 3986 conformance: revised URI parsing and formatting; URLEncodedUtils deprecated in favor of WWWFormCodec
---
 .../hc/core5/http2/examples/H2GreetingServer.java  |   4 +-
 .../hc/core5/testing/framework/FrameworkTest.java  |   5 +-
 .../framework/TestingFrameworkRequestHandler.java  |   7 +-
 .../hc/core5/http/io/entity/EntityUtils.java       |   4 +-
 .../hc/core5/http/io/entity/HttpEntities.java      |   4 +-
 .../http/nio/entity/AsyncEntityProducers.java      |   4 +-
 .../http/nio/support/AsyncRequestBuilder.java      |   4 +-
 .../java/org/apache/hc/core5/net/PercentCodec.java | 163 +++++++++
 .../java/org/apache/hc/core5/net/URIBuilder.java   | 233 +++++++++---
 .../org/apache/hc/core5/net/URLEncodedUtils.java   | 303 ++--------------
 .../java/org/apache/hc/core5/net/WWWFormCodec.java |  83 +++++
 .../hc/core5/http/NameValuePairListMatcher.java    |  85 +++++
 .../hc/core5/http/io/entity/TestEntityUtils.java   |   4 +-
 .../org/apache/hc/core5/net/TestPercentCodec.java  |  70 ++++
 .../org/apache/hc/core5/net/TestURIBuilder.java    | 206 ++++++++---
 .../apache/hc/core5/net/TestURLEncodedUtils.java   | 394 ---------------------
 .../org/apache/hc/core5/net/TestWWWFormCodec.java  | 123 +++++++
 17 files changed, 913 insertions(+), 783 deletions(-)

diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
index b1dc3e1..551be6f 100644
--- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java
@@ -63,7 +63,7 @@ import org.apache.hc.core5.http2.HttpVersionPolicy;
 import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap;
 import org.apache.hc.core5.io.CloseMode;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.reactor.IOReactorConfig;
 import org.apache.hc.core5.reactor.ListenerEndpoint;
 import org.apache.hc.core5.util.TimeValue;
@@ -174,7 +174,7 @@ public class H2GreetingServer {
             if (contentType != null && contentType.isSameMimeType(ContentType.APPLICATION_FORM_URLENCODED)) {
 
                 // decoding the form entity into key/value pairs:
-                final List<NameValuePair> args = URLEncodedUtils.parse(httpEntity, contentType.getCharset());
+                final List<NameValuePair> args = WWWFormCodec.parse(httpEntity, contentType.getCharset());
                 if (!args.isEmpty()) {
                     name = args.get(0).getValue();
                 }
diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
index 326a8f1..9685487 100644
--- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
+++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/FrameworkTest.java
@@ -46,7 +46,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.URIBuilder;
 
 public class FrameworkTest {
 
@@ -112,7 +112,8 @@ public class FrameworkTest {
             if (path != null) {
                 final URI uri = path.startsWith("/") ? new URI("http://localhost:8080" + path) :
                                                  new URI("http://localhost:8080/");
-                final List<NameValuePair> params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
+                final URIBuilder uriBuilder = new URIBuilder(uri, StandardCharsets.UTF_8);
+                final List<NameValuePair> params = uriBuilder.getQueryParams();
                 @SuppressWarnings("unchecked")
                 final Map<String, Object> queryMap = (Map<String, Object>) request.get(QUERY);
                 for (final NameValuePair param : params) {
diff --git a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
index 07bd7d5..e757b62 100644
--- a/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
+++ b/httpcore5-testing/src/main/java/org/apache/hc/core5/testing/framework/TestingFrameworkRequestHandler.java
@@ -45,17 +45,17 @@ import java.util.Map.Entry;
 
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.ProtocolVersion;
 import org.apache.hc.core5.http.io.HttpRequestHandler;
-import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.URIBuilder;
 
 public class TestingFrameworkRequestHandler implements HttpRequestHandler {
     protected Throwable thrown;
@@ -137,7 +137,8 @@ public class TestingFrameworkRequestHandler implements HttpRequestHandler {
             final Map<String, String> expectedQuery = (Map<String, String>) requestExpectations.get(QUERY);
             if (expectedQuery != null) {
                 final URI uri = request.getUri();
-                final List<NameValuePair> actualParams = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
+                final URIBuilder uriBuilder = new URIBuilder(uri, StandardCharsets.UTF_8);
+                final List<NameValuePair> actualParams = uriBuilder.getQueryParams();
                 final Map<String, String> actualParamsMap = new HashMap<>();
                 for (final NameValuePair actualParam : actualParams) {
                     actualParamsMap.put(actualParam.getName(), actualParam.getValue());
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
index 0e6d70a..f4b44a3 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/EntityUtils.java
@@ -45,7 +45,7 @@ import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.ParseException;
 import org.apache.hc.core5.io.Closer;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.ByteArrayBuffer;
 import org.apache.hc.core5.util.CharArrayBuffer;
@@ -412,7 +412,7 @@ public final class EntityUtils {
         if (buf.isEmpty()) {
             return Collections.emptyList();
         }
-        return URLEncodedUtils.parse(buf, charset, '&');
+        return WWWFormCodec.parse(buf, charset);
     }
 
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
index 597e1ea..6331733 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/entity/HttpEntities.java
@@ -46,7 +46,7 @@ import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.io.IOCallback;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -88,7 +88,7 @@ public final class HttpEntities {
         final ContentType contentType = charset != null ?
                 ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) :
                 ContentType.APPLICATION_FORM_URLENCODED;
-        return create(URLEncodedUtils.format(parameters, contentType.getCharset()), contentType);
+        return create(WWWFormCodec.format(parameters, contentType.getCharset()), contentType);
     }
 
     public static HttpEntity create(final IOCallback<OutputStream> callback, final ContentType contentType) {
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
index 3fd20d1..b34438d 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/AsyncEntityProducers.java
@@ -45,7 +45,7 @@ import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
 import org.apache.hc.core5.http.nio.DataStreamChannel;
 import org.apache.hc.core5.http.nio.StreamChannel;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 
 /**
  * {AsyncEntityProducer} factory methods.
@@ -82,7 +82,7 @@ public final class AsyncEntityProducers {
         final ContentType contentType = charset != null ?
                 ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset) :
                 ContentType.APPLICATION_FORM_URLENCODED;
-        return create(URLEncodedUtils.format(parameters, contentType.getCharset()), contentType);
+        return create(WWWFormCodec.format(parameters, contentType.getCharset()), contentType);
     }
 
     public static AsyncEntityProducer createBinary(
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
index 81149b2..720980e 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncRequestBuilder.java
@@ -50,7 +50,7 @@ import org.apache.hc.core5.http.nio.AsyncRequestProducer;
 import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
 import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
 import org.apache.hc.core5.net.URIBuilder;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -368,7 +368,7 @@ public class AsyncRequestBuilder {
         AsyncEntityProducer entityProducerCopy = entityProducer;
         if (parameters != null && !parameters.isEmpty()) {
             if (entityProducerCopy == null && (Method.POST.isSame(method) || Method.PUT.isSame(method))) {
-                final String content = URLEncodedUtils.format(
+                final String content = WWWFormCodec.format(
                         parameters,
                         charset != null ? charset : ContentType.APPLICATION_FORM_URLENCODED.getCharset());
                 entityProducerCopy = new StringAsyncEntityProducer(
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
new file mode 100644
index 0000000..2782282
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
@@ -0,0 +1,163 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.net;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+
+/**
+ * Percent-encoding mechanism defined in RFC 3986
+ *
+ * @since 5.1
+ */
+public class PercentCodec {
+
+    static final BitSet GEN_DELIMS = new BitSet(256);
+    static final BitSet SUB_DELIMS = new BitSet(256);
+    static final BitSet UNRESERVED = new BitSet(256);
+    static final BitSet URIC = new BitSet(256);
+
+    static {
+        GEN_DELIMS.set(':');
+        GEN_DELIMS.set('/');
+        GEN_DELIMS.set('?');
+        GEN_DELIMS.set('#');
+        GEN_DELIMS.set('[');
+        GEN_DELIMS.set(']');
+        GEN_DELIMS.set('@');
+
+        SUB_DELIMS.set('!');
+        SUB_DELIMS.set('$');
+        SUB_DELIMS.set('&');
+        SUB_DELIMS.set('\'');
+        SUB_DELIMS.set('(');
+        SUB_DELIMS.set(')');
+        SUB_DELIMS.set('*');
+        SUB_DELIMS.set('+');
+        SUB_DELIMS.set(',');
+        SUB_DELIMS.set(';');
+        SUB_DELIMS.set('=');
+
+        for (int i = 'a'; i <= 'z'; i++) {
+            UNRESERVED.set(i);
+        }
+        for (int i = 'A'; i <= 'Z'; i++) {
+            UNRESERVED.set(i);
+        }
+        // numeric characters
+        for (int i = '0'; i <= '9'; i++) {
+            UNRESERVED.set(i);
+        }
+        UNRESERVED.set('-');
+        UNRESERVED.set('.');
+        UNRESERVED.set('_');
+        UNRESERVED.set('~');
+        URIC.or(SUB_DELIMS);
+        URIC.or(UNRESERVED);
+    }
+
+    private static final int RADIX = 16;
+
+    static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
+                       final BitSet safechars, final boolean blankAsPlus) {
+        if (content == null) {
+            return;
+        }
+        final CharBuffer cb = CharBuffer.wrap(content);
+        final ByteBuffer bb = (charset != null ? charset : StandardCharsets.UTF_8).encode(cb);
+        while (bb.hasRemaining()) {
+            final int b = bb.get() & 0xff;
+            if (safechars.get(b)) {
+                buf.append((char) b);
+            } else if (blankAsPlus && b == ' ') {
+                buf.append("+");
+            } else {
+                buf.append("%");
+                final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
+                final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+                buf.append(hex1);
+                buf.append(hex2);
+            }
+        }
+    }
+
+    static void encode(final StringBuilder buf, final CharSequence content, final Charset charset, final boolean blankAsPlus) {
+        encode(buf, content, charset, UNRESERVED, blankAsPlus);
+    }
+
+    public static void encode(final StringBuilder buf, final CharSequence content, final Charset charset) {
+        encode(buf, content, charset, UNRESERVED, false);
+    }
+
+    public static String encode(final CharSequence content, final Charset charset) {
+        if (content == null) {
+            return null;
+        }
+        final StringBuilder buf = new StringBuilder();
+        encode(buf, content, charset, UNRESERVED, false);
+        return buf.toString();
+    }
+
+    static String decode(final CharSequence content, final Charset charset, final boolean plusAsBlank) {
+        if (content == null) {
+            return null;
+        }
+        final ByteBuffer bb = ByteBuffer.allocate(content.length());
+        final CharBuffer cb = CharBuffer.wrap(content);
+        while (cb.hasRemaining()) {
+            final char c = cb.get();
+            if (c == '%' && cb.remaining() >= 2) {
+                final char uc = cb.get();
+                final char lc = cb.get();
+                final int u = Character.digit(uc, RADIX);
+                final int l = Character.digit(lc, RADIX);
+                if (u != -1 && l != -1) {
+                    bb.put((byte) ((u << 4) + l));
+                } else {
+                    bb.put((byte) '%');
+                    bb.put((byte) uc);
+                    bb.put((byte) lc);
+                }
+            } else if (plusAsBlank && c == '+') {
+                bb.put((byte) ' ');
+            } else {
+                bb.put((byte) c);
+            }
+        }
+        bb.flip();
+        return (charset != null ? charset : StandardCharsets.UTF_8).decode(bb).toString();
+    }
+
+    public static String decode(final CharSequence content, final Charset charset) {
+        return decode(content, charset, false);
+    }
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
index 3fbe2d7..70358e2 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URIBuilder.java
@@ -34,6 +34,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -41,7 +42,9 @@ import java.util.List;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.apache.hc.core5.http.message.ParserCursor;
 import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Tokenizer;
 
 /**
  * Builder for {@link URI} instances.
@@ -75,6 +78,7 @@ public class URIBuilder {
     private String host;
     private int port;
     private String encodedPath;
+    private boolean pathRootless;
     private List<String> pathSegments;
     private String encodedQuery;
     private List<NameValuePair> queryParams;
@@ -98,7 +102,7 @@ public class URIBuilder {
      * @throws URISyntaxException if the input is not a valid URI
      */
     public URIBuilder(final String string) throws URISyntaxException {
-        this(new URI(string), null);
+        this(new URI(string), StandardCharsets.UTF_8);
     }
 
     /**
@@ -106,7 +110,7 @@ public class URIBuilder {
      * @param uri
      */
     public URIBuilder(final URI uri) {
-        this(uri, null);
+        this(uri, StandardCharsets.UTF_8);
     }
 
     /**
@@ -125,8 +129,7 @@ public class URIBuilder {
      */
     public URIBuilder(final URI uri, final Charset charset) {
         super();
-        setCharset(charset);
-        digestURI(uri);
+        digestURI(uri, charset);
     }
 
     public URIBuilder setCharset(final Charset charset) {
@@ -138,18 +141,118 @@ public class URIBuilder {
         return charset;
     }
 
-    private List <NameValuePair> parseQuery(final String query, final Charset charset) {
-        if (query != null && !query.isEmpty()) {
-            return URLEncodedUtils.parse(query, charset);
+    private static final char QUERY_PARAM_SEPARATOR = '&';
+    private static final char PARAM_VALUE_SEPARATOR = '=';
+    private static final char PATH_SEPARATOR = '/';
+
+    private static final BitSet QUERY_PARAM_SEPARATORS = new BitSet(256);
+    private static final BitSet QUERY_VALUE_SEPARATORS = new BitSet(256);
+    private static final BitSet PATH_SEPARATORS = new BitSet(256);
+
+    static {
+        QUERY_PARAM_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
+        QUERY_PARAM_SEPARATORS.set(PARAM_VALUE_SEPARATOR);
+        QUERY_VALUE_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
+        PATH_SEPARATORS.set(PATH_SEPARATOR);
+    }
+
+    static List<NameValuePair> parseQuery(final CharSequence s, final Charset charset, final boolean plusAsBlank) {
+        if (s == null) {
+            return null;
+        }
+        final Tokenizer tokenParser = Tokenizer.INSTANCE;
+        final ParserCursor cursor = new ParserCursor(0, s.length());
+        final List<NameValuePair> list = new ArrayList<>();
+        while (!cursor.atEnd()) {
+            final String name = tokenParser.parseToken(s, cursor, QUERY_PARAM_SEPARATORS);
+            String value = null;
+            if (!cursor.atEnd()) {
+                final int delim = s.charAt(cursor.getPos());
+                cursor.updatePos(cursor.getPos() + 1);
+                if (delim == PARAM_VALUE_SEPARATOR) {
+                    value = tokenParser.parseToken(s, cursor, QUERY_VALUE_SEPARATORS);
+                    if (!cursor.atEnd()) {
+                        cursor.updatePos(cursor.getPos() + 1);
+                    }
+                }
+            }
+            if (!name.isEmpty()) {
+                list.add(new BasicNameValuePair(
+                        PercentCodec.decode(name, charset, plusAsBlank),
+                        PercentCodec.decode(value, charset, plusAsBlank)));
+            }
         }
-        return null;
+        return list;
     }
 
-    private List <String> parsePath(final String path, final Charset charset) {
-        if (path != null && !path.isEmpty()) {
-            return URLEncodedUtils.parsePathSegments(path, charset);
+    static List<String> splitPath(final CharSequence s) {
+        if (s == null) {
+            return null;
+        }
+        final ParserCursor cursor = new ParserCursor(0, s.length());
+        // Skip leading separator
+        if (cursor.atEnd()) {
+            return new ArrayList<>(0);
+        }
+        if (PATH_SEPARATORS.get(s.charAt(cursor.getPos()))) {
+            cursor.updatePos(cursor.getPos() + 1);
+        }
+        final List<String> list = new ArrayList<>();
+        final StringBuilder buf = new StringBuilder();
+        for (;;) {
+            if (cursor.atEnd()) {
+                list.add(buf.toString());
+                break;
+            }
+            final char current = s.charAt(cursor.getPos());
+            if (PATH_SEPARATORS.get(current)) {
+                list.add(buf.toString());
+                buf.setLength(0);
+            } else {
+                buf.append(current);
+            }
+            cursor.updatePos(cursor.getPos() + 1);
+        }
+        return list;
+    }
+
+    static List<String> parsePath(final CharSequence s, final Charset charset) {
+        if (s == null) {
+            return null;
+        }
+        final List<String> segments = splitPath(s);
+        final List<String> list = new ArrayList<>(segments.size());
+        for (final String segment: segments) {
+            list.add(PercentCodec.decode(segment, charset));
+        }
+        return list;
+    }
+
+    static void formatPath(final StringBuilder buf, final Iterable<String> segments, final boolean rootless, final Charset charset) {
+        int i = 0;
+        for (final String segment : segments) {
+            if (i > 0 || !rootless) {
+                buf.append(PATH_SEPARATOR);
+            }
+            PercentCodec.encode(buf, segment, charset);
+            i++;
+        }
+    }
+
+    static void formatQuery(final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset,
+                            final boolean blankAsPlus) {
+        int i = 0;
+        for (final NameValuePair parameter : params) {
+            if (i > 0) {
+                buf.append(QUERY_PARAM_SEPARATOR);
+            }
+            PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus);
+            if (parameter.getValue() != null) {
+                buf.append(PARAM_VALUE_SEPARATOR);
+                PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus);
+            }
+            i++;
         }
-        return null;
     }
 
     /**
@@ -167,14 +270,23 @@ public class URIBuilder {
         if (this.encodedSchemeSpecificPart != null) {
             sb.append(this.encodedSchemeSpecificPart);
         } else {
+            final boolean authoritySpecified;
             if (this.encodedAuthority != null) {
                 sb.append("//").append(this.encodedAuthority);
+                authoritySpecified = true;
             } else if (this.host != null) {
                 sb.append("//");
                 if (this.encodedUserInfo != null) {
                     sb.append(this.encodedUserInfo).append("@");
                 } else if (this.userInfo != null) {
-                    encodeUserInfo(sb, this.userInfo);
+                    final int idx = this.userInfo.indexOf(':');
+                    if (idx != -1) {
+                        PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset);
+                        sb.append(':');
+                        PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset);
+                    } else {
+                        PercentCodec.encode(sb, this.userInfo, this.charset);
+                    }
                     sb.append("@");
                 }
                 if (InetAddressUtils.isIPv6Address(this.host)) {
@@ -185,43 +297,38 @@ public class URIBuilder {
                 if (this.port >= 0) {
                     sb.append(":").append(this.port);
                 }
+                authoritySpecified = true;
+            } else {
+                authoritySpecified = false;
             }
             if (this.encodedPath != null) {
-                sb.append(normalizePath(this.encodedPath, sb.length() == 0));
+                if (authoritySpecified && !TextUtils.isEmpty(this.encodedPath) && !this.encodedPath.startsWith("/")) {
+                    sb.append('/');
+                }
+                sb.append(this.encodedPath);
             } else if (this.pathSegments != null) {
-                encodePath(sb, this.pathSegments);
+                formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset);
             }
             if (this.encodedQuery != null) {
                 sb.append("?").append(this.encodedQuery);
             } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
                 sb.append("?");
-                encodeUrlForm(sb, this.queryParams);
+                formatQuery(sb, this.queryParams, this.charset, false);
             } else if (this.query != null) {
                 sb.append("?");
-                encodeUric(sb, this.query);
+                PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false);
             }
         }
         if (this.encodedFragment != null) {
             sb.append("#").append(this.encodedFragment);
         } else if (this.fragment != null) {
             sb.append("#");
-            encodeUric(sb, this.fragment);
+            PercentCodec.encode(sb, this.fragment, this.charset);
         }
         return sb.toString();
     }
 
-    private static String normalizePath(final String path, final boolean relative) {
-        String s = path;
-        if (TextUtils.isBlank(s)) {
-            return "";
-        }
-        if (!relative && !s.startsWith("/")) {
-            s = "/" + s;
-        }
-        return s;
-    }
-
-    private void digestURI(final URI uri) {
+    private void digestURI(final URI uri, final Charset charset) {
         this.scheme = uri.getScheme();
         this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
         this.encodedAuthority = uri.getRawAuthority();
@@ -230,27 +337,13 @@ public class URIBuilder {
         this.encodedUserInfo = uri.getRawUserInfo();
         this.userInfo = uri.getUserInfo();
         this.encodedPath = uri.getRawPath();
-        this.pathSegments = parsePath(uri.getRawPath(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.pathSegments = parsePath(uri.getRawPath(), charset);
+        this.pathRootless = uri.getRawPath() != null && !uri.getRawPath().startsWith("/");
         this.encodedQuery = uri.getRawQuery();
-        this.queryParams = parseQuery(uri.getRawQuery(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
         this.encodedFragment = uri.getRawFragment();
         this.fragment = uri.getFragment();
-    }
-
-    private void encodeUserInfo(final StringBuilder buf, final String userInfo) {
-        URLEncodedUtils.encUserInfo(buf, userInfo, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodePath(final StringBuilder buf, final List<String> pathSegments) {
-        URLEncodedUtils.formatSegments(buf, pathSegments, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodeUrlForm(final StringBuilder buf, final List<NameValuePair> params) {
-        URLEncodedUtils.formatParameters(buf, params, this.charset != null ? this.charset : StandardCharsets.UTF_8);
-    }
-
-    private void encodeUric(final StringBuilder buf, final String fragment) {
-        URLEncodedUtils.encUric(buf, fragment, this.charset != null ? this.charset : StandardCharsets.UTF_8);
+        this.charset = charset;
     }
 
     /**
@@ -301,7 +394,7 @@ public class URIBuilder {
             final StringBuilder sb = new StringBuilder(schemeSpecificPart);
             if (nvps != null && !nvps.isEmpty()) {
                 sb.append("?");
-                encodeUrlForm(sb, nvps);
+                formatQuery(sb, nvps, this.charset, false);
             }
             this.encodedSchemeSpecificPart = sb.toString();
         }
@@ -327,7 +420,11 @@ public class URIBuilder {
      * be unescaped and may contain non ASCII characters.
      *
      * @return this.
+     *
+     * @deprecated The use of clear-text passwords in {@link URI}s has been deprecated and is strongly
+     * discouraged.
      */
+    @Deprecated
     public URIBuilder setUserInfo(final String username, final String password) {
         return setUserInfo(username + ':' + password);
     }
@@ -387,7 +484,9 @@ public class URIBuilder {
      * @return this.
      */
     public URIBuilder setPath(final String path) {
-        return setPathSegments(path != null ? URLEncodedUtils.splitPathSegments(path) : null);
+        setPathSegments(path != null ? splitPath(path) : null);
+        this.pathRootless = path != null && !path.startsWith("/");
+        return this;
     }
 
     /**
@@ -399,6 +498,23 @@ public class URIBuilder {
         this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
         this.encodedSchemeSpecificPart = null;
         this.encodedPath = null;
+        this.pathRootless = false;
+        return this;
+    }
+
+    /**
+     * Sets rootless URI path (the first segment does not start with a /).
+     * The value is expected to be unescaped and may contain non ASCII characters.
+     *
+     * @return this.
+     *
+     * @since 5.1
+     */
+    public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
+        this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
+        this.encodedSchemeSpecificPart = null;
+        this.encodedPath = null;
+        this.pathRootless = true;
         return this;
     }
 
@@ -411,6 +527,23 @@ public class URIBuilder {
         this.pathSegments = pathSegments != null && pathSegments.size() > 0 ? new ArrayList<>(pathSegments) : null;
         this.encodedSchemeSpecificPart = null;
         this.encodedPath = null;
+        this.pathRootless = false;
+        return this;
+    }
+
+    /**
+     * Sets rootless URI path (the first segment does not start with a /).
+     * The value is expected to be unescaped and may contain non ASCII characters.
+     *
+     * @return this.
+     *
+     * @since 5.1
+     */
+    public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
+        this.pathSegments = pathSegments != null && pathSegments.size() > 0 ? new ArrayList<>(pathSegments) : null;
+        this.encodedSchemeSpecificPart = null;
+        this.encodedPath = null;
+        this.pathRootless = true;
         return this;
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
index c2131ea..a3e3676 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/URLEncodedUtils.java
@@ -28,14 +28,11 @@
 package org.apache.hc.core5.net;
 
 import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
-import java.util.Collections;
 import java.util.List;
 
 import org.apache.hc.core5.http.NameValuePair;
@@ -47,18 +44,15 @@ import org.apache.hc.core5.util.Tokenizer;
  * A collection of utilities for encoding URLs.
  *
  * @since 4.0
+ *
+ * @deprecated Use {@link URIBuilder} to parse and format {@link URI}s and
+ * {@link WWWFormCodec} to parse and format {@code application/x-www-form-urlencoded} forms.
  */
+@Deprecated
 public class URLEncodedUtils {
 
     private static final char QP_SEP_A = '&';
     private static final char QP_SEP_S = ';';
-    private static final String NAME_VALUE_SEPARATOR = "=";
-    private static final char PATH_SEPARATOR = '/';
-
-    private static final BitSet PATH_SEPARATORS     = new BitSet(256);
-    static {
-        PATH_SEPARATORS.set(PATH_SEPARATOR);
-    }
 
     /**
      * Returns a list of {@link NameValuePair}s URI query parameters.
@@ -76,7 +70,7 @@ public class URLEncodedUtils {
         if (query != null && !query.isEmpty()) {
             return parse(query, charset);
         }
-        return createEmptyList();
+        return new ArrayList<>(0);
     }
 
     /**
@@ -91,7 +85,7 @@ public class URLEncodedUtils {
      */
     public static List<NameValuePair> parse(final CharSequence s, final Charset charset) {
         if (s == null) {
-            return createEmptyList();
+            return new ArrayList<>(0);
         }
         return parse(s, charset, QP_SEP_A, QP_SEP_S);
     }
@@ -133,45 +127,13 @@ public class URLEncodedUtils {
             }
             if (!name.isEmpty()) {
                 list.add(new BasicNameValuePair(
-                        decodeFormFields(name, charset),
-                        decodeFormFields(value, charset)));
-            }
-        }
-        return list;
-    }
-
-    static List<String> splitSegments(final CharSequence s, final BitSet separators) {
-        final Tokenizer.Cursor cursor = new Tokenizer.Cursor(0, s.length());
-        // Skip leading separator
-        if (cursor.atEnd()) {
-            return Collections.emptyList();
-        }
-        if (separators.get(s.charAt(cursor.getPos()))) {
-            cursor.updatePos(cursor.getPos() + 1);
-        }
-        final List<String> list = new ArrayList<>();
-        final StringBuilder buf = new StringBuilder();
-        for (;;) {
-            if (cursor.atEnd()) {
-                list.add(buf.toString());
-                break;
-            }
-            final char current = s.charAt(cursor.getPos());
-            if (separators.get(current)) {
-                list.add(buf.toString());
-                buf.setLength(0);
-            } else {
-                buf.append(current);
+                        PercentCodec.decode(name, charset, true),
+                        PercentCodec.decode(value, charset, true)));
             }
-            cursor.updatePos(cursor.getPos() + 1);
         }
         return list;
     }
 
-    static List<String> splitPathSegments(final CharSequence s) {
-        return splitSegments(s, PATH_SEPARATORS);
-    }
-
     /**
      * Returns a list of URI path segments.
      *
@@ -182,12 +144,7 @@ public class URLEncodedUtils {
      * @since 4.5
      */
     public static List<String> parsePathSegments(final CharSequence s, final Charset charset) {
-        Args.notNull(s, "Char sequence");
-        final List<String> list = splitPathSegments(s);
-        for (int i = 0; i < list.size(); i++) {
-            list.set(i, urlDecode(list.get(i), charset != null ? charset : StandardCharsets.UTF_8, false));
-        }
-        return list;
+        return URIBuilder.parsePath(s, charset);
     }
 
     /**
@@ -202,13 +159,6 @@ public class URLEncodedUtils {
         return parsePathSegments(s, StandardCharsets.UTF_8);
     }
 
-    static void formatSegments(final StringBuilder buf, final Iterable<String> segments, final Charset charset) {
-        for (final String segment : segments) {
-            buf.append(PATH_SEPARATOR);
-            urlEncode(buf, segment, charset, PATHSAFE);
-        }
-    }
-
     /**
      * Returns a string consisting of joint encoded path segments.
      *
@@ -221,7 +171,7 @@ public class URLEncodedUtils {
     public static String formatSegments(final Iterable<String> segments, final Charset charset) {
         Args.notNull(segments, "Segments");
         final StringBuilder buf = new StringBuilder();
-        formatSegments(buf, segments, charset);
+        URIBuilder.formatPath(buf, segments, false, charset);
         return buf.toString();
     }
 
@@ -237,32 +187,6 @@ public class URLEncodedUtils {
         return formatSegments(Arrays.asList(segments), StandardCharsets.UTF_8);
     }
 
-    static void formatNameValuePairs(
-            final StringBuilder buf,
-            final Iterable<? extends NameValuePair> parameters,
-            final char parameterSeparator,
-            final Charset charset) {
-        int i = 0;
-        for (final NameValuePair parameter : parameters) {
-            if (i > 0) {
-                buf.append(parameterSeparator);
-            }
-            encodeFormFields(buf, parameter.getName(), charset);
-            if (parameter.getValue() != null) {
-                buf.append(NAME_VALUE_SEPARATOR);
-                encodeFormFields(buf, parameter.getValue(), charset);
-            }
-            i++;
-        }
-    }
-
-    static void formatParameters(
-            final StringBuilder buf,
-            final Iterable<? extends NameValuePair> parameters,
-            final Charset charset) {
-        formatNameValuePairs(buf, parameters, QP_SEP_A, charset);
-    }
-
     /**
      * Returns a String that is suitable for use as an {@code application/x-www-form-urlencoded}
      * list of parameters in an HTTP PUT or HTTP POST.
@@ -280,7 +204,18 @@ public class URLEncodedUtils {
             final Charset charset) {
         Args.notNull(parameters, "Parameters");
         final StringBuilder buf = new StringBuilder();
-        formatNameValuePairs(buf, parameters, parameterSeparator, charset);
+        int i = 0;
+        for (final NameValuePair parameter : parameters) {
+            if (i > 0) {
+                buf.append(parameterSeparator);
+            }
+            PercentCodec.encode(buf, parameter.getName(), charset, URLENCODER, true);
+            if (parameter.getValue() != null) {
+                buf.append('=');
+                PercentCodec.encode(buf, parameter.getValue(), charset, URLENCODER, true);
+            }
+            i++;
+        }
         return buf.toString();
     }
 
@@ -300,205 +235,25 @@ public class URLEncodedUtils {
         return format(parameters, QP_SEP_A, charset);
     }
 
-    /**
-     * Unreserved characters, i.e. alphanumeric, plus: {@code _ - ! . ~ ' ( ) *}
-     * <p>
-     *  This list is the same as the {@code unreserved} list in
-     *  <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
-     */
-    private static final BitSet UNRESERVED   = new BitSet(256);
-    /**
-     * Punctuation characters: , ; : $ & + =
-     * <p>
-     * These are the additional characters allowed by userinfo.
-     */
-    private static final BitSet PUNCT        = new BitSet(256);
-    /** Characters which are safe to use in userinfo,
-     * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation */
-    private static final BitSet USERINFO     = new BitSet(256);
-    /** Characters which are safe to use in a path,
-     * i.e. {@link #UNRESERVED} plus {@link #PUNCT}uation plus / @ */
-    private static final BitSet PATHSAFE     = new BitSet(256);
-    /** Characters which are safe to use in a query or a fragment,
-     * i.e. {@link #RESERVED} plus {@link #UNRESERVED} */
-    private static final BitSet URIC     = new BitSet(256);
-
-    /**
-     * Reserved characters, i.e. {@code ;/?:@&=+$,[]}
-     * <p>
-     *  This list is the same as the {@code reserved} list in
-     *  <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>
-     *  as augmented by
-     *  <a href="http://www.ietf.org/rfc/rfc2732.txt">RFC 2732</a>
-     */
-    private static final BitSet RESERVED     = new BitSet(256);
-
-
-    /**
-     * Safe characters for x-www-form-urlencoded data, as per java.net.URLEncoder and browser behaviour,
-     * i.e. alphanumeric plus {@code "-", "_", ".", "*"}
-     */
     private static final BitSet URLENCODER   = new BitSet(256);
 
-    private static final BitSet PATH_SPECIAL = new BitSet(256);
-
     static {
         // unreserved chars
         // alpha characters
         for (int i = 'a'; i <= 'z'; i++) {
-            UNRESERVED.set(i);
+            URLENCODER.set(i);
         }
         for (int i = 'A'; i <= 'Z'; i++) {
-            UNRESERVED.set(i);
+            URLENCODER.set(i);
         }
         // numeric characters
         for (int i = '0'; i <= '9'; i++) {
-            UNRESERVED.set(i);
-        }
-        UNRESERVED.set('_'); // these are the charactes of the "mark" list
-        UNRESERVED.set('-');
-        UNRESERVED.set('.');
-        UNRESERVED.set('*');
-        URLENCODER.or(UNRESERVED); // skip remaining unreserved characters
-        UNRESERVED.set('!');
-        UNRESERVED.set('~');
-        UNRESERVED.set('\'');
-        UNRESERVED.set('(');
-        UNRESERVED.set(')');
-        // punct chars
-        PUNCT.set(',');
-        PUNCT.set(';');
-        PUNCT.set(':');
-        PUNCT.set('$');
-        PUNCT.set('&');
-        PUNCT.set('+');
-        PUNCT.set('=');
-        // Safe for userinfo
-        USERINFO.or(UNRESERVED);
-        USERINFO.or(PUNCT);
-
-        // URL path safe
-        PATHSAFE.or(UNRESERVED);
-        PATHSAFE.set(';'); // param separator
-        PATHSAFE.set(':'); // RFC 2396
-        PATHSAFE.set('@');
-        PATHSAFE.set('&');
-        PATHSAFE.set('=');
-        PATHSAFE.set('+');
-        PATHSAFE.set('$');
-        PATHSAFE.set(',');
-
-        PATH_SPECIAL.or(PATHSAFE);
-        PATH_SPECIAL.set('/');
-
-        RESERVED.set(';');
-        RESERVED.set('/');
-        RESERVED.set('?');
-        RESERVED.set(':');
-        RESERVED.set('@');
-        RESERVED.set('&');
-        RESERVED.set('=');
-        RESERVED.set('+');
-        RESERVED.set('$');
-        RESERVED.set(',');
-        RESERVED.set('['); // added by RFC 2732
-        RESERVED.set(']'); // added by RFC 2732
-
-        URIC.or(RESERVED);
-        URIC.or(UNRESERVED);
-    }
-
-    private static final int RADIX = 16;
-
-    private static List<NameValuePair> createEmptyList() {
-        return new ArrayList<>(0);
-    }
-
-    private static void urlEncode(
-            final StringBuilder buf,
-            final String content,
-            final Charset charset,
-            final BitSet safechars) {
-        if (content == null) {
-            return;
-        }
-        final ByteBuffer bb = charset.encode(content);
-        while (bb.hasRemaining()) {
-            final int b = bb.get() & 0xff;
-            if (safechars.get(b)) {
-                buf.append((char) b);
-            } else {
-                buf.append("%");
-                final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
-                final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
-                buf.append(hex1);
-                buf.append(hex2);
-            }
-        }
-    }
-
-    private static String urlDecode(
-            final String content,
-            final Charset charset,
-            final boolean plusAsBlank) {
-        if (content == null) {
-            return null;
-        }
-        final ByteBuffer bb = ByteBuffer.allocate(content.length());
-        final CharBuffer cb = CharBuffer.wrap(content);
-        while (cb.hasRemaining()) {
-            final char c = cb.get();
-            if (c == '%' && cb.remaining() >= 2) {
-                final char uc = cb.get();
-                final char lc = cb.get();
-                final int u = Character.digit(uc, 16);
-                final int l = Character.digit(lc, 16);
-                if (u != -1 && l != -1) {
-                    bb.put((byte) ((u << 4) + l));
-                } else {
-                    bb.put((byte) '%');
-                    bb.put((byte) uc);
-                    bb.put((byte) lc);
-                }
-            } else if (plusAsBlank && c == '+') {
-                bb.put((byte) ' ');
-            } else {
-                bb.put((byte) c);
-            }
-        }
-        bb.flip();
-        return charset.decode(bb).toString();
-    }
-
-    static String decodeFormFields(final String content, final Charset charset) {
-        if (content == null) {
-            return null;
+            URLENCODER.set(i);
         }
-        return urlDecode(content, charset != null ? charset : StandardCharsets.UTF_8, true);
-    }
-
-    static void encodeFormFields(final StringBuilder buf, final String content, final Charset charset) {
-        if (content == null) {
-            return;
-        }
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URLENCODER);
-    }
-
-    static String encodeFormFields(final String content, final Charset charset) {
-        if (content == null) {
-            return null;
-        }
-        final StringBuilder buf = new StringBuilder();
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URLENCODER);
-        return buf.toString();
-    }
-
-    static void encUserInfo(final StringBuilder buf, final String content, final Charset charset) {
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, USERINFO);
-    }
-
-    static void encUric(final StringBuilder buf, final String content, final Charset charset) {
-        urlEncode(buf, content, charset != null ? charset : StandardCharsets.UTF_8, URIC);
+        URLENCODER.set('_'); // these are the characters of the "mark" list
+        URLENCODER.set('-');
+        URLENCODER.set('.');
+        URLENCODER.set('*');
     }
 
 }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java
new file mode 100644
index 0000000..900918e
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/WWWFormCodec.java
@@ -0,0 +1,83 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.net;
+
+import java.nio.charset.Charset;
+import java.util.List;
+
+import org.apache.hc.core5.http.NameValuePair;
+
+/**
+ * {@code application/x-www-form-urlencoded} codec.
+ *
+ * @since 5.1
+ */
+public class WWWFormCodec {
+
+    private static final char QP_SEP_A = '&';
+
+    /**
+     * Returns a list of {@link NameValuePair} parameters parsed
+     * from the {@code application/x-www-form-urlencoded} content.
+     *
+     * @param s input text.
+     * @param charset parameter charset.
+     * @return list of form parameters.
+     */
+    public static List<NameValuePair> parse(final CharSequence s, final Charset charset) {
+        return URIBuilder.parseQuery(s, charset, true);
+    }
+
+    /**
+     * Formats the list of {@link NameValuePair} parameters into a {@code application/x-www-form-urlencoded}
+     * content.
+     *
+     * @param buf the content buffer
+     * @param params  The from parameters.
+     * @param charset The encoding to use.
+     */
+    public static void format(
+            final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset) {
+        URIBuilder.formatQuery(buf, params, charset, true);
+    }
+
+    /**
+     * Formats the list of {@link NameValuePair} parameters into a {@code application/x-www-form-urlencoded}
+     * content string.
+     *
+     * @param params  The from parameters.
+     * @param charset The encoding to use.
+     * @return content string
+     */
+    public static String format(final Iterable<? extends NameValuePair> params, final Charset charset) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatQuery(buf, params, charset, true);
+        return buf.toString();
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java b/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java
new file mode 100644
index 0000000..7ecf449
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/NameValuePairListMatcher.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.core5.http;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hc.core5.util.LangUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+
+public class NameValuePairListMatcher extends BaseMatcher<List<NameValuePair>> {
+
+    private List<? extends NameValuePair> nvps;
+
+    public NameValuePairListMatcher(final List<? extends NameValuePair> nvps) {
+        this.nvps = nvps;
+    }
+
+    @Override
+    public boolean matches(final Object item) {
+        if (item instanceof List<?>) {
+            final List<?> objects = (List<?>) item;
+            if (objects.size() != nvps.size()) {
+                return false;
+            }
+            for (int i = 1; i < objects.size(); i++) {
+                final Object obj = objects.get(i);
+                if (obj instanceof NameValuePair) {
+                    final NameValuePair nvp = (NameValuePair) obj;
+                    final NameValuePair expected = nvps.get(i);
+                    if (!LangUtils.equals(nvp.getName(), expected.getName())
+                            || !LangUtils.equals(nvp.getValue(), expected.getValue())) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("equals ").appendValueList("[", ";", "]", nvps);
+    }
+
+    @Factory
+    public static Matcher<List<NameValuePair>> equalsTo(final NameValuePair... nvps) {
+        return new NameValuePairListMatcher(Arrays.asList(nvps));
+    }
+
+    @Factory
+    public static Matcher<List<NameValuePair>> isEmpty() {
+        return new NameValuePairListMatcher(Collections.<NameValuePair>emptyList());
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
index d50131c..f83ac7f 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/io/entity/TestEntityUtils.java
@@ -42,7 +42,7 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
-import org.apache.hc.core5.net.URLEncodedUtils;
+import org.apache.hc.core5.net.WWWFormCodec;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -228,7 +228,7 @@ public class TestEntityUtils {
         parameters.add(new BasicNameValuePair("russian", ru_hello));
         parameters.add(new BasicNameValuePair("swiss", ch_hello));
 
-        final String s = URLEncodedUtils.format(parameters, StandardCharsets.UTF_8);
+        final String s = WWWFormCodec.format(parameters, StandardCharsets.UTF_8);
 
         Assert.assertEquals("russian=%D0%92%D1%81%D0%B5%D0%BC_%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82" +
                 "&swiss=Gr%C3%BCezi_z%C3%A4m%C3%A4", s);
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
new file mode 100644
index 0000000..a19b617
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
@@ -0,0 +1,70 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.net;
+
+import java.nio.charset.StandardCharsets;
+
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link PercentCodec}.
+ */
+public class TestPercentCodec {
+
+    @Test
+    public void testCoding() {
+        final StringBuilder buf = new StringBuilder();
+        PercentCodec.encode(buf, "blah!", StandardCharsets.UTF_8);
+        PercentCodec.encode(buf, " ~ ", StandardCharsets.UTF_8);
+        PercentCodec.encode(buf, "huh?", StandardCharsets.UTF_8);
+        MatcherAssert.assertThat(buf.toString(), CoreMatchers.equalTo("blah%21%20~%20huh%3F"));
+    }
+
+    @Test
+    public void testDecoding() {
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20~%20huh%3F", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! ~ huh?"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21+~%20huh%3F", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah!+~ huh?"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21+~%20huh%3F", StandardCharsets.UTF_8, true),
+                CoreMatchers.equalTo("blah! ~ huh?"));
+    }
+
+    @Test
+    public void testDecodingPartialContent() {
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%a", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %a"));
+        MatcherAssert.assertThat(PercentCodec.decode("blah%21%20%wa", StandardCharsets.UTF_8),
+                CoreMatchers.equalTo("blah! %wa"));
+    }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
index f86d345..ea153cf 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURIBuilder.java
@@ -32,17 +32,138 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.NameValuePairListMatcher;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
 import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 
 public class TestURIBuilder {
 
+    private static final String CH_HELLO = "\u0047\u0072\u00FC\u0065\u007A\u0069\u005F\u007A\u00E4\u006D\u00E4";
+    private static final String RU_HELLO = "\u0412\u0441\u0435\u043C\u005F\u043F\u0440\u0438\u0432\u0435\u0442";
+
+    static List<String> parsePath(final CharSequence s) {
+        return URIBuilder.parsePath(s, null);
+    }
+
+    @Test
+    public void testParseSegments() throws Exception {
+        MatcherAssert.assertThat(parsePath("/this/that"), CoreMatchers.equalTo(Arrays.asList("this", "that")));
+        MatcherAssert.assertThat(parsePath("this/that"), CoreMatchers.equalTo(Arrays.asList("this", "that")));
+        MatcherAssert.assertThat(parsePath("this//that"), CoreMatchers.equalTo(Arrays.asList("this", "", "that")));
+        MatcherAssert.assertThat(parsePath("this//that/"), CoreMatchers.equalTo(Arrays.asList("this", "", "that", "")));
+        MatcherAssert.assertThat(parsePath("this//that/%2fthis%20and%20that"),
+                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "/this and that")));
+        MatcherAssert.assertThat(parsePath("this///that//"),
+                CoreMatchers.equalTo(Arrays.asList("this", "", "", "that", "", "")));
+        MatcherAssert.assertThat(parsePath("/"), CoreMatchers.equalTo(Collections.singletonList("")));
+        MatcherAssert.assertThat(parsePath(""), CoreMatchers.equalTo(Collections.<String>emptyList()));
+    }
+
+    static String formatPath(final String... pathSegments) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatPath(buf, Arrays.asList(pathSegments), false, null);
+        return buf.toString();
+    }
+
+    @Test
+    public void testFormatSegments() throws Exception {
+        MatcherAssert.assertThat(formatPath("this", "that"), CoreMatchers.equalTo("/this/that"));
+        MatcherAssert.assertThat(formatPath("this", "", "that"), CoreMatchers.equalTo("/this//that"));
+        MatcherAssert.assertThat(formatPath("this", "", "that", "/this and that"),
+                CoreMatchers.equalTo("/this//that/%2Fthis%20and%20that"));
+        MatcherAssert.assertThat(formatPath("this", "", "", "that", "", ""),
+                CoreMatchers.equalTo("/this///that//"));
+        MatcherAssert.assertThat(formatPath(""), CoreMatchers.equalTo("/"));
+        MatcherAssert.assertThat(formatPath(), CoreMatchers.equalTo(""));
+    }
+
+    static List<NameValuePair> parseQuery(final CharSequence s) {
+        return URIBuilder.parseQuery(s, null, false);
+    }
+
+    @Test
+    public void testParseQuery() throws Exception {
+        MatcherAssert.assertThat(parseQuery(""), NameValuePairListMatcher.isEmpty());
+        MatcherAssert.assertThat(parseQuery("Name0"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name0", null)));
+        MatcherAssert.assertThat(parseQuery("Name1=Value1"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name1", "Value1")));
+        MatcherAssert.assertThat(parseQuery("Name2="),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name2", "")));
+        MatcherAssert.assertThat(parseQuery(" Name3  "),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name3", null)));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%204%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4!")));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%2B4%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value+4!")));
+        MatcherAssert.assertThat(parseQuery("Name4=Value%204%21%20%214"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4! !4")));
+        MatcherAssert.assertThat(parseQuery("Name5=aaa&Name6=bbb"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name5", "aaa"),
+                        new BasicNameValuePair("Name6", "bbb")));
+        MatcherAssert.assertThat(parseQuery("Name7=aaa&Name7=b%2Cb&Name7=ccc"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name7", "aaa"),
+                        new BasicNameValuePair("Name7", "b,b"),
+                        new BasicNameValuePair("Name7", "ccc")));
+        MatcherAssert.assertThat(parseQuery("Name8=xx%2C%20%20yy%20%20%2Czz"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name8", "xx,  yy  ,zz")));
+        MatcherAssert.assertThat(parseQuery("price=10%20%E2%82%AC"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("price", "10 \u20AC")));
+        MatcherAssert.assertThat(parseQuery("a=b\"c&d=e"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("a", "b\"c"),
+                        new BasicNameValuePair("d", "e")));
+        MatcherAssert.assertThat(parseQuery("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("russian", RU_HELLO),
+                        new BasicNameValuePair("swiss", CH_HELLO)));
+    }
+
+    static String formatQuery(final NameValuePair... params) {
+        final StringBuilder buf = new StringBuilder();
+        URIBuilder.formatQuery(buf, Arrays.asList(params), null, false);
+        return buf.toString();
+    }
+
+    @Test
+    public void testFormatQuery() throws Exception {
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name0", null)), CoreMatchers.equalTo("Name0"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name1", "Value1")), CoreMatchers.equalTo("Name1=Value1"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name2", "")), CoreMatchers.equalTo("Name2="));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value 4&")),
+                CoreMatchers.equalTo("Name4=Value%204%26"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value+4&")),
+                CoreMatchers.equalTo("Name4=Value%2B4%26"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name4", "Value 4& =4")),
+                CoreMatchers.equalTo("Name4=Value%204%26%20%3D4"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("Name5", "aaa"),
+                new BasicNameValuePair("Name6", "bbb")), CoreMatchers.equalTo("Name5=aaa&Name6=bbb"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("Name7", "aaa"),
+                new BasicNameValuePair("Name7", "b,b"),
+                new BasicNameValuePair("Name7", "ccc")
+        ), CoreMatchers.equalTo("Name7=aaa&Name7=b%2Cb&Name7=ccc"));
+        MatcherAssert.assertThat(formatQuery(new BasicNameValuePair("Name8", "xx,  yy  ,zz")),
+                CoreMatchers.equalTo("Name8=xx%2C%20%20yy%20%20%2Czz"));
+        MatcherAssert.assertThat(formatQuery(
+                new BasicNameValuePair("russian", RU_HELLO),
+                new BasicNameValuePair("swiss", CH_HELLO)),
+                CoreMatchers.equalTo("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)));
+    }
+
     @Test
     public void testHierarchicalUri() throws Exception {
         final URI uri = new URI("http", "stuff", "localhost", 80, "/some stuff", "param=stuff", "fragment");
@@ -169,14 +290,6 @@ public class TestURIBuilder {
     }
 
     @Test
-    public void testSetUserInfo() throws Exception {
-        final URI uri = new URI("http", null, "localhost", 80, "/", "param=stuff", null);
-        final URIBuilder uribuilder = new URIBuilder(uri).setUserInfo("user", "password");
-        final URI result = uribuilder.build();
-        Assert.assertEquals(new URI("http://user:password@localhost:80/?param=stuff"), result);
-    }
-
-    @Test
     public void testRemoveParameters() throws Exception {
         final URI uri = new URI("http", null, "localhost", 80, "/", "param=stuff", null);
         final URIBuilder uribuilder = new URIBuilder(uri).removeQuery();
@@ -290,35 +403,6 @@ public class TestURIBuilder {
     }
 
     @Test
-    public void testAgainstURIEncoded() throws Exception {
-        // Check that the encoded URI generated by URI builder agrees with that generated by using URI directly
-        final String scheme="https";
-        final String host="localhost";
-        final String specials="/ abcd!$&*()_-+.,=:;'~<>/@[]|#^%\"{}\\`xyz"; // N.B. excludes \u00a3\u00ac\u00a6
-        final URI uri = new URI(scheme, specials, host, 80, specials, specials, specials);
-
-        final URI bld = new URIBuilder()
-                .setScheme(scheme)
-                .setHost(host)
-                .setUserInfo(specials)
-                .setPath(specials)
-                .setCustomQuery(specials)
-                .setFragment(specials)
-                .build();
-
-        Assert.assertEquals(uri.getHost(), bld.getHost());
-
-        Assert.assertEquals(uri.getRawUserInfo(), bld.getRawUserInfo());
-
-        Assert.assertEquals(uri.getRawPath(), bld.getRawPath());
-
-        Assert.assertEquals(uri.getRawQuery(), bld.getRawQuery());
-
-        Assert.assertEquals(uri.getRawFragment(), bld.getRawFragment());
-
-    }
-
-    @Test
     public void testBuildAddParametersUTF8() throws Exception {
         assertAddParameters(StandardCharsets.UTF_8);
     }
@@ -355,8 +439,8 @@ public class TestURIBuilder {
     }
 
     public void assertBuild(final Charset charset, final URI uri) throws Exception {
-        final String encodedData1 = URLEncodedUtils.encodeFormFields("\"1\u00aa position\"", charset);
-        final String encodedData2 = URLEncodedUtils.encodeFormFields("Jos\u00e9 Abra\u00e3o", charset);
+        final String encodedData1 = PercentCodec.encode("\"1\u00aa position\"", charset);
+        final String encodedData2 = PercentCodec.encode("Jos\u00e9 Abra\u00e3o", charset);
 
         final String uriExpected = String.format("https://somehost.com/stuff?parameter1=value1&parameter2=%s&parameter3=%s", encodedData1, encodedData2);
 
@@ -392,7 +476,7 @@ public class TestURIBuilder {
 
     @Test
     public void testTolerateNullInput() throws Exception {
-        Assert.assertThat(new URIBuilder()
+        MatcherAssert.assertThat(new URIBuilder()
                         .setScheme(null)
                         .setHost("localhost")
                         .setUserInfo(null)
@@ -406,7 +490,7 @@ public class TestURIBuilder {
 
     @Test
     public void testTolerateBlankInput() throws Exception {
-        Assert.assertThat(new URIBuilder()
+        MatcherAssert.assertThat(new URIBuilder()
                         .setScheme("")
                         .setHost("localhost")
                         .setUserInfo("")
@@ -424,8 +508,7 @@ public class TestURIBuilder {
         final HttpHost httpHost = new HttpHost("http", "example.com", 1234);
         final URIBuilder uribuilder = new URIBuilder();
         uribuilder.setHttpHost(httpHost);
-        final URI result = uribuilder.build();
-        Assert.assertEquals(URI.create(httpHost.toURI()), result);
+        Assert.assertEquals(URI.create("http://example.com:1234"), uribuilder.build());
     }
 
     @Test
@@ -435,23 +518,50 @@ public class TestURIBuilder {
                 .setHost("somehost")
                 .setPath("//blah//blah")
                 .build();
-        Assert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp://somehost//blah//blah")));
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp://somehost//blah//blah")));
     }
 
     @Test
-    public void testPathNoLeadingSlash() throws Exception {
+    public void testNoAuthorityAndPath() throws Exception {
         final URI uri = new URIBuilder()
-                .setScheme("ftp")
+                .setScheme("file")
+                .setPath("/blah")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:/blah")));
+    }
+
+    @Test
+    public void testNoAuthorityAndPathSegments() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
+                .setPathSegments("this", "that")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:/this/that")));
+    }
+
+    @Test
+    public void testNoAuthorityAndRootlessPath() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
                 .setPath("blah")
                 .build();
-        Assert.assertThat(uri, CoreMatchers.equalTo(URI.create("ftp:/blah")));
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:blah")));
+    }
+
+    @Test
+    public void testNoAuthorityAndRootlessPathSegments() throws Exception {
+        final URI uri = new URIBuilder()
+                .setScheme("file")
+                .setPathSegmentsRootless("this", "that")
+                .build();
+        MatcherAssert.assertThat(uri, CoreMatchers.equalTo(URI.create("file:this/that")));
     }
 
     @Test
     public void testOpaque() throws Exception {
         final URIBuilder uriBuilder = new URIBuilder("http://host.com");
         final URI uri = uriBuilder.build();
-        Assert.assertThat(uriBuilder.isOpaque(), CoreMatchers.equalTo(uri.isOpaque()));
+        MatcherAssert.assertThat(uriBuilder.isOpaque(), CoreMatchers.equalTo(uri.isOpaque()));
     }
 
     @Test
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java
deleted file mode 100644
index e7687a4..0000000
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestURLEncodedUtils.java
+++ /dev/null
@@ -1,394 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-
-package org.apache.hc.core5.net;
-
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.hc.core5.http.NameValuePair;
-import org.apache.hc.core5.http.message.BasicNameValuePair;
-import org.hamcrest.CoreMatchers;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class TestURLEncodedUtils {
-
-    @Test
-    public void testParseURLCodedContent() throws Exception {
-        List <NameValuePair> result;
-
-        result = parse("");
-        Assert.assertTrue(result.isEmpty());
-
-        result = parse("Name0");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name0", null);
-
-        result = parse("Name1=Value1");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name1", "Value1");
-
-        result = parse("Name2=");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name2", "");
-
-        result = parse("Name3");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name3", null);
-
-        result = parse("Name4=Value%204%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4!");
-
-        result = parse("Name4=Value%2B4%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value+4!");
-
-        result = parse("Name4=Value%204%21%20%214");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4! !4");
-
-        result = parse("Name5=aaa&Name6=bbb");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "Name5", "aaa");
-        assertNameValuePair(result.get(1), "Name6", "bbb");
-
-        result = parse("Name7=aaa&Name7=b%2Cb&Name7=ccc");
-        Assert.assertEquals(3, result.size());
-        assertNameValuePair(result.get(0), "Name7", "aaa");
-        assertNameValuePair(result.get(1), "Name7", "b,b");
-        assertNameValuePair(result.get(2), "Name7", "ccc");
-
-        result = parse("Name8=xx%2C%20%20yy%20%20%2Czz");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name8", "xx,  yy  ,zz");
-
-        result = parse("price=10%20%E2%82%AC");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "price", "10 \u20AC");
-    }
-
-    @Test
-    public void testParseSegments() throws Exception {
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("/this/that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this/that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that/"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this//that/%2fthis%20and%20that"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "that", "/this and that")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("this///that//"),
-                CoreMatchers.equalTo(Arrays.asList("this", "", "", "that", "", "")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments("/"),
-                CoreMatchers.equalTo(Collections.singletonList("")));
-        Assert.assertThat(URLEncodedUtils.parsePathSegments(""),
-                CoreMatchers.equalTo(Collections.<String>emptyList()));
-    }
-
-    @Test
-    public void testFormatSegments() throws Exception {
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "that"),
-                CoreMatchers.equalTo("/this/that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "that"),
-                CoreMatchers.equalTo("/this//that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "that", "/this and that"),
-                CoreMatchers.equalTo("/this//that/%2Fthis%20and%20that"));
-        Assert.assertThat(URLEncodedUtils.formatSegments("this", "", "", "that", "", ""),
-                CoreMatchers.equalTo("/this///that//"));
-        Assert.assertThat(URLEncodedUtils.formatSegments(""),
-                CoreMatchers.equalTo("/"));
-        Assert.assertThat(URLEncodedUtils.formatSegments(),
-                CoreMatchers.equalTo(""));
-    }
-
-    @Test
-    public void testParseURLCodedContentString() throws Exception {
-        List <NameValuePair> result;
-
-        result = parseString("");
-        Assert.assertTrue(result.isEmpty());
-
-        result = parseString("Name0");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name0", null);
-
-        result = parseString("Name1=Value1");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name1", "Value1");
-
-        result = parseString("Name2=");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name2", "");
-
-        result = parseString("Name3");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name3", null);
-
-        result = parseString("Name4=Value%204%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4!");
-
-        result = parseString("Name4=Value%2B4%21");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value+4!");
-
-        result = parseString("Name4=Value%204%21%20%214");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name4", "Value 4! !4");
-
-        result = parseString("Name5=aaa&Name6=bbb");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "Name5", "aaa");
-        assertNameValuePair(result.get(1), "Name6", "bbb");
-
-        result = parseString("Name7=aaa&Name7=b%2Cb&Name7=ccc");
-        Assert.assertEquals(3, result.size());
-        assertNameValuePair(result.get(0), "Name7", "aaa");
-        assertNameValuePair(result.get(1), "Name7", "b,b");
-        assertNameValuePair(result.get(2), "Name7", "ccc");
-
-        result = parseString("Name8=xx%2C%20%20yy%20%20%2Czz");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "Name8", "xx,  yy  ,zz");
-
-        result = parseString("price=10%20%E2%82%AC");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "price", "10 \u20AC");
-
-        result = parse("a=b\"c&d=e");
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "a", "b\"c");
-        assertNameValuePair(result.get(1), "d", "e");
-    }
-
-    @Test
-    public void testParseInvalidURLCodedContent() throws Exception {
-        List <NameValuePair> result;
-
-        result = parse("name=%");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%");
-
-        result = parse("name=%a");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%a");
-
-        result = parse("name=%wa%20");
-        Assert.assertEquals(1, result.size());
-        assertNameValuePair(result.get(0), "name", "%wa ");
-    }
-
-    private static final int SWISS_GERMAN_HELLO [] = {
-        0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4
-    };
-
-    private static final int RUSSIAN_HELLO [] = {
-        0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438,
-        0x432, 0x435, 0x442
-    };
-
-    private static String constructString(final int [] unicodeChars) {
-        final StringBuilder buffer = new StringBuilder();
-        if (unicodeChars != null) {
-            for (final int unicodeChar : unicodeChars) {
-                buffer.append((char)unicodeChar);
-            }
-        }
-        return buffer.toString();
-    }
-
-    @Test
-    public void testParseUTF8Ampersand1String() throws Exception {
-        final String ru_hello = constructString(RUSSIAN_HELLO);
-        final String ch_hello = constructString(SWISS_GERMAN_HELLO);
-        final List <NameValuePair> parameters = new ArrayList<>();
-        parameters.add(new BasicNameValuePair("russian", ru_hello));
-        parameters.add(new BasicNameValuePair("swiss", ch_hello));
-
-        final String s = URLEncodedUtils.format(parameters, StandardCharsets.UTF_8);
-
-        final List <NameValuePair> result = URLEncodedUtils.parse(s, StandardCharsets.UTF_8);
-        Assert.assertEquals(2, result.size());
-        assertNameValuePair(result.get(0), "russian", ru_hello);
-        assertNameValuePair(result.get(1), "swiss", ch_hello);
-    }
-
-    @Test
-    public void testParseUTF8Ampersand2String() throws Exception {
-        testParseUTF8String('&');
-    }
-
-    @Test
-    public void testParseUTF8SemicolonString() throws Exception {
-        testParseUTF8String(';');
-    }
-
-    private void testParseUTF8String(final char parameterSeparator) throws Exception {
-        final String ru_hello = constructString(RUSSIAN_HELLO);
-        final String ch_hello = constructString(SWISS_GERMAN_HELLO);
-        final List <NameValuePair> parameters = new ArrayList<>();
-        parameters.add(new BasicNameValuePair("russian", ru_hello));
-        parameters.add(new BasicNameValuePair("swiss", ch_hello));
-
-        final String s = URLEncodedUtils.format(parameters, parameterSeparator, StandardCharsets.UTF_8);
-
-        final List<NameValuePair> result1 = URLEncodedUtils.parse(s, StandardCharsets.UTF_8);
-        Assert.assertEquals(2, result1.size());
-        assertNameValuePair(result1.get(0), "russian", ru_hello);
-        assertNameValuePair(result1.get(1), "swiss", ch_hello);
-
-        final List<NameValuePair> result2 = URLEncodedUtils.parse(s, StandardCharsets.UTF_8, parameterSeparator);
-        Assert.assertEquals(2, result2.size());
-        assertNameValuePair(result2.get(0), "russian", ru_hello);
-        assertNameValuePair(result2.get(1), "swiss", ch_hello);
-    }
-
-    @Test
-    public void testEmptyQuery() throws Exception {
-        final List<NameValuePair> result = URLEncodedUtils.parse("", StandardCharsets.UTF_8);
-        Assert.assertEquals(0, result.size());
-        // [HTTPCLIENT-1889]:
-        result.add(new BasicNameValuePair("key", "value"));
-    }
-
-    @Test
-    public void testFormat() throws Exception {
-        final List <NameValuePair> params = new ArrayList<>();
-        Assert.assertEquals(0, URLEncodedUtils.format(params, StandardCharsets.US_ASCII).length());
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name0", null));
-        Assert.assertEquals("Name0", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name1", "Value1"));
-        Assert.assertEquals("Name1=Value1", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name2", ""));
-        Assert.assertEquals("Name2=", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4&"));
-        Assert.assertEquals("Name4=Value%204%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value+4&"));
-        Assert.assertEquals("Name4=Value%2B4%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4& =4"));
-        Assert.assertEquals("Name4=Value%204%26%20%3D4", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name5", "aaa"));
-        params.add(new BasicNameValuePair("Name6", "bbb"));
-        Assert.assertEquals("Name5=aaa&Name6=bbb", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name7", "aaa"));
-        params.add(new BasicNameValuePair("Name7", "b,b"));
-        params.add(new BasicNameValuePair("Name7", "ccc"));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, '&', StandardCharsets.US_ASCII));
-        Assert.assertEquals("Name7=aaa;Name7=b%2Cb;Name7=ccc", URLEncodedUtils.format(params, ';', StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name8", "xx,  yy  ,zz"));
-        Assert.assertEquals("Name8=xx%2C%20%20yy%20%20%2Czz", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-    }
-
-    @Test
-    public void testFormatString() throws Exception { // as above, using String
-        final List <NameValuePair> params = new ArrayList<>();
-        Assert.assertEquals(0, URLEncodedUtils.format(params, StandardCharsets.US_ASCII).length());
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name0", null));
-        Assert.assertEquals("Name0", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name1", "Value1"));
-        Assert.assertEquals("Name1=Value1", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name2", ""));
-        Assert.assertEquals("Name2=", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4&"));
-        Assert.assertEquals("Name4=Value%204%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value+4&"));
-        Assert.assertEquals("Name4=Value%2B4%26", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name4", "Value 4& =4"));
-        Assert.assertEquals("Name4=Value%204%26%20%3D4", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name5", "aaa"));
-        params.add(new BasicNameValuePair("Name6", "bbb"));
-        Assert.assertEquals("Name5=aaa&Name6=bbb", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name7", "aaa"));
-        params.add(new BasicNameValuePair("Name7", "b,b"));
-        params.add(new BasicNameValuePair("Name7", "ccc"));
-        Assert.assertEquals("Name7=aaa&Name7=b%2Cb&Name7=ccc", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-
-        params.clear();
-        params.add(new BasicNameValuePair("Name8", "xx,  yy  ,zz"));
-        Assert.assertEquals("Name8=xx%2C%20%20yy%20%20%2Czz", URLEncodedUtils.format(params, StandardCharsets.US_ASCII));
-    }
-
-    private List <NameValuePair> parse (final String params) {
-        return URLEncodedUtils.parse(params, StandardCharsets.UTF_8);
-    }
-
-    private List <NameValuePair> parseString (final String uri) throws Exception {
-        return URLEncodedUtils.parse(new URI("?"+uri), StandardCharsets.UTF_8);
-    }
-
-    private static void assertNameValuePair (
-            final NameValuePair parameter,
-            final String expectedName,
-            final String expectedValue) {
-        Assert.assertEquals(parameter.getName(), expectedName);
-        Assert.assertEquals(parameter.getValue(), expectedValue);
-    }
-
-}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java
new file mode 100644
index 0000000..88d4362
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestWWWFormCodec.java
@@ -0,0 +1,123 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.net;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.NameValuePairListMatcher;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+public class TestWWWFormCodec {
+
+    private static final String CH_HELLO = "\u0047\u0072\u00FC\u0065\u007A\u0069\u005F\u007A\u00E4\u006D\u00E4";
+    private static final String RU_HELLO = "\u0412\u0441\u0435\u043C\u005F\u043F\u0440\u0438\u0432\u0435\u0442";
+
+    private static List<NameValuePair> parse(final String params) {
+        return WWWFormCodec.parse(params, StandardCharsets.UTF_8);
+    }
+
+    @Test
+    public void testParse() throws Exception {
+        MatcherAssert.assertThat(parse(""), NameValuePairListMatcher.isEmpty());
+        MatcherAssert.assertThat(parse("Name0"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name0", null)));
+        MatcherAssert.assertThat(parse("Name1=Value1"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name1", "Value1")));
+        MatcherAssert.assertThat(parse("Name2="),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name2", "")));
+        MatcherAssert.assertThat(parse(" Name3  "),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name3", null)));
+        MatcherAssert.assertThat(parse("Name4=Value%204%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4!")));
+        MatcherAssert.assertThat(parse("Name4=Value%2B4%21"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value+4!")));
+        MatcherAssert.assertThat(parse("Name4=Value%204%21%20%214"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name4", "Value 4! !4")));
+        MatcherAssert.assertThat(parse("Name5=aaa&Name6=bbb"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name5", "aaa"),
+                        new BasicNameValuePair("Name6", "bbb")));
+        MatcherAssert.assertThat(parse("Name7=aaa&Name7=b%2Cb&Name7=ccc"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("Name7", "aaa"),
+                        new BasicNameValuePair("Name7", "b,b"),
+                        new BasicNameValuePair("Name7", "ccc")));
+        MatcherAssert.assertThat(parse("Name8=xx%2C%20%20yy%20%20%2Czz"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("Name8", "xx,  yy  ,zz")));
+        MatcherAssert.assertThat(parse("price=10%20%E2%82%AC"),
+                NameValuePairListMatcher.equalsTo(new BasicNameValuePair("price", "10 \u20AC")));
+        MatcherAssert.assertThat(parse("a=b\"c&d=e"),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("a", "b\"c"),
+                        new BasicNameValuePair("d", "e")));
+        MatcherAssert.assertThat(parse("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)),
+                NameValuePairListMatcher.equalsTo(
+                        new BasicNameValuePair("russian", RU_HELLO),
+                        new BasicNameValuePair("swiss", CH_HELLO)));
+    }
+
+    private static String format(final NameValuePair... nvps) {
+        return WWWFormCodec.format(Arrays.asList(nvps), StandardCharsets.UTF_8);
+    }
+
+    @Test
+    public void testFormat() throws Exception {
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name0", null)), CoreMatchers.equalTo("Name0"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name1", "Value1")), CoreMatchers.equalTo("Name1=Value1"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name2", "")), CoreMatchers.equalTo("Name2="));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value 4&")),
+                CoreMatchers.equalTo("Name4=Value+4%26"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value+4&")),
+                CoreMatchers.equalTo("Name4=Value%2B4%26"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name4", "Value 4& =4")),
+                CoreMatchers.equalTo("Name4=Value+4%26+%3D4"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("Name5", "aaa"),
+                new BasicNameValuePair("Name6", "bbb")), CoreMatchers.equalTo("Name5=aaa&Name6=bbb"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("Name7", "aaa"),
+                new BasicNameValuePair("Name7", "b,b"),
+                new BasicNameValuePair("Name7", "ccc")
+        ), CoreMatchers.equalTo("Name7=aaa&Name7=b%2Cb&Name7=ccc"));
+        MatcherAssert.assertThat(format(new BasicNameValuePair("Name8", "xx,  yy  ,zz")),
+                CoreMatchers.equalTo("Name8=xx%2C++yy++%2Czz"));
+        MatcherAssert.assertThat(format(
+                new BasicNameValuePair("russian", RU_HELLO),
+                new BasicNameValuePair("swiss", CH_HELLO)),
+                CoreMatchers.equalTo("russian=" + PercentCodec.encode(RU_HELLO, StandardCharsets.UTF_8) +
+                        "&swiss=" + PercentCodec.encode(CH_HELLO, StandardCharsets.UTF_8)));
+    }
+
+}


[httpcomponents-core] 05/13: HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 5c4b08eee3476ab77bfeffd3b237d2bdcd47ddfd
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:13:58 2020 -0400

    HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection
    
    This adds a configurable ResponseOutOfOrderStrategy in place of the
    previous always-enabled behavior, and uses the no-op
    NoResponseOutOfOrderStrategy implementation by default.
    
    The previous behavior can be used by selecting the
    MonitoringResponseOutOfOrderStrategy, which has been updated
    to support more flexible behavior. Note that this strategy
    results in a 1 ms pause for every chunk transferred, limiting
    upload speed using the default 8 KiB chunk size to at most
    8 MiB/second.
    
    The original discussion can be found on the mailing list:
    https://www.mail-archive.com/httpclient-users@hc.apache.org/msg09911.html
    
    This closes #206
---
 ...gResponseOutOfOrderStrategyIntegrationTest.java | 215 +++++++++++++++++++++
 .../http/impl/io/DefaultBHttpClientConnection.java | 105 +++++++++-
 .../io/DefaultBHttpClientConnectionFactory.java    |  32 ++-
 .../io/MonitoringResponseOutOfOrderStrategy.java   | 111 +++++++++++
 .../http/impl/io/NoResponseOutOfOrderStrategy.java |  61 ++++++
 .../http/impl/io/ResponseOutOfOrderException.java  |  41 ++++
 .../core5/http/io/ResponseOutOfOrderStrategy.java  |  66 +++++++
 .../java/org/apache/hc/core5/util/Timeout.java     |   5 +
 .../TestMonitoringResponseOutOfOrderStrategy.java  | 133 +++++++++++++
 9 files changed, 764 insertions(+), 5 deletions(-)

diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java
new file mode 100644
index 0000000..fad4491
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/MonitoringResponseOutOfOrderStrategyIntegrationTest.java
@@ -0,0 +1,215 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.testing.classic;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.bootstrap.HttpRequester;
+import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap;
+import org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnectionFactory;
+import org.apache.hc.core5.http.impl.io.MonitoringResponseOutOfOrderStrategy;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.testing.SSLTestContexts;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class MonitoringResponseOutOfOrderStrategyIntegrationTest {
+
+    // Use a 16k buffer for consistent results across systems
+    private static final int BUFFER_SIZE = 16 * 1024;
+    private static final Timeout TIMEOUT = Timeout.ofSeconds(3);
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> protocols() {
+        return Arrays.asList(new Object[][]{
+                { URIScheme.HTTP },
+                { URIScheme.HTTPS }
+        });
+    }
+
+    private final URIScheme scheme;
+    private ClassicTestServer server;
+    private HttpRequester requester;
+
+    public MonitoringResponseOutOfOrderStrategyIntegrationTest(final URIScheme scheme) {
+        this.scheme = scheme;
+    }
+
+    @Rule
+    public ExternalResource serverResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            server = new ClassicTestServer(
+                    scheme == URIScheme.HTTPS ? SSLTestContexts.createServerSSLContext() : null,
+                    SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .setSndBufSize(BUFFER_SIZE)
+                            .setRcvBufSize(BUFFER_SIZE)
+                            .setSoKeepAlive(false)
+                            .build());
+        }
+
+        @Override
+        protected void after() {
+            if (server != null) {
+                try {
+                    server.shutdown(CloseMode.IMMEDIATE);
+                    server = null;
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    @Rule
+    public ExternalResource requesterResource = new ExternalResource() {
+
+        @Override
+        protected void before() throws Throwable {
+            requester = RequesterBootstrap.bootstrap()
+                    .setSslContext(scheme == URIScheme.HTTPS  ? SSLTestContexts.createClientSSLContext() : null)
+                    .setSocketConfig(SocketConfig.custom()
+                            .setSoTimeout(TIMEOUT)
+                            .setRcvBufSize(BUFFER_SIZE)
+                            .setSndBufSize(BUFFER_SIZE)
+                            .setSoKeepAlive(false)
+                            .build())
+                    .setStreamListener(LoggingHttp1StreamListener.INSTANCE)
+                    .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
+                    .setConnectionFactory(DefaultBHttpClientConnectionFactory.builder()
+                            .responseOutOfOrderStrategy(MonitoringResponseOutOfOrderStrategy.INSTANCE)
+                            .build())
+                    .create();
+        }
+
+        @Override
+        protected void after() {
+            if (requester != null) {
+                try {
+                    requester.close(CloseMode.IMMEDIATE);
+                    requester = null;
+                } catch (final Exception ignore) {
+                }
+            }
+        }
+
+    };
+
+    @Test(timeout = 5000) // Failures may hang
+    public void testResponseOutOfOrderWithDefaultStrategy() throws Exception {
+        this.server.registerHandler("*", new HttpRequestHandler() {
+
+            @Override
+            public void handle(
+                    final ClassicHttpRequest request,
+                    final ClassicHttpResponse response,
+                    final HttpContext context) throws IOException {
+                response.setCode(400);
+                response.setEntity(new AllOnesHttpEntity(200000));
+            }
+
+        });
+
+        this.server.start(null, null, null);
+
+        final HttpCoreContext context = HttpCoreContext.create();
+        final HttpHost host = new HttpHost(scheme.id, "localhost", this.server.getPort());
+
+        final ClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/");
+        post.setEntity(new AllOnesHttpEntity(200000));
+
+        try (final ClassicHttpResponse response = requester.execute(host, post, TIMEOUT, context)) {
+            Assert.assertEquals(400, response.getCode());
+            EntityUtils.consumeQuietly(response.getEntity());
+        }
+    }
+
+    private static final class AllOnesHttpEntity extends AbstractHttpEntity {
+        private long remaining;
+
+        protected AllOnesHttpEntity(final long length) {
+            super(ContentType.APPLICATION_OCTET_STREAM, null, true);
+            this.remaining = length;
+        }
+
+        @Override
+        public InputStream getContent() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void writeTo(final OutputStream outStream) throws IOException {
+            final byte[] buf = new byte[1024];
+            while (remaining > 0) {
+                final int writeLength = (int) Math.min(remaining, buf.length);
+                outStream.write(buf, 0, writeLength);
+                outStream.flush();
+                remaining -= writeLength;
+            }
+        }
+
+        @Override
+        public boolean isStreaming() {
+            return true;
+        }
+
+        @Override
+        public void close() {
+        }
+
+        @Override
+        public long getContentLength() {
+            return -1L;
+        }
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
index dd5c1e3..d905bab 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnection.java
@@ -28,6 +28,7 @@
 package org.apache.hc.core5.http.impl.io;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.Socket;
 import java.nio.charset.CharsetDecoder;
@@ -51,6 +52,7 @@ import org.apache.hc.core5.http.io.HttpMessageParser;
 import org.apache.hc.core5.http.io.HttpMessageParserFactory;
 import org.apache.hc.core5.http.io.HttpMessageWriter;
 import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -65,6 +67,7 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
     private final HttpMessageWriter<ClassicHttpRequest> requestWriter;
     private final ContentLengthStrategy incomingContentStrategy;
     private final ContentLengthStrategy outgoingContentStrategy;
+    private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
     private volatile boolean consistent;
 
     /**
@@ -80,6 +83,8 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
      * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param responseOutOfOrderStrategy response out of order strategy. If {@code null}
+     *   {@link NoResponseOutOfOrderStrategy#INSTANCE} will be used.
      * @param requestWriterFactory request writer factory. If {@code null}
      *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
      * @param responseParserFactory response parser factory. If {@code null}
@@ -91,6 +96,7 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
             final CharsetEncoder charEncoder,
             final ContentLengthStrategy incomingContentStrategy,
             final ContentLengthStrategy outgoingContentStrategy,
+            final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
         super(http1Config, charDecoder, charEncoder);
@@ -99,12 +105,51 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
         this.responseParser = (responseParserFactory != null ? responseParserFactory :
             DefaultHttpResponseParserFactory.INSTANCE).create(http1Config);
         this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
-                DefaultContentLengthStrategy.INSTANCE;
+            DefaultContentLengthStrategy.INSTANCE;
         this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
-                DefaultContentLengthStrategy.INSTANCE;
+            DefaultContentLengthStrategy.INSTANCE;
+        this.responseOutOfOrderStrategy = responseOutOfOrderStrategy != null ? responseOutOfOrderStrategy :
+            NoResponseOutOfOrderStrategy.INSTANCE;
         this.consistent = true;
     }
 
+    /**
+     * Creates new instance of DefaultBHttpClientConnection.
+     *
+     * @param http1Config Message http1Config. If {@code null}
+     *   {@link Http1Config#DEFAULT} will be used.
+     * @param charDecoder decoder to be used for decoding HTTP protocol elements.
+     *   If {@code null} simple type cast will be used for byte to char conversion.
+     * @param charEncoder encoder to be used for encoding HTTP protocol elements.
+     *   If {@code null} simple type cast will be used for char to byte conversion.
+     * @param incomingContentStrategy incoming content length strategy. If {@code null}
+     *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
+     *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
+     * @param requestWriterFactory request writer factory. If {@code null}
+     *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
+     * @param responseParserFactory response parser factory. If {@code null}
+     *   {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
+     */
+    public DefaultBHttpClientConnection(
+            final Http1Config http1Config,
+            final CharsetDecoder charDecoder,
+            final CharsetEncoder charEncoder,
+            final ContentLengthStrategy incomingContentStrategy,
+            final ContentLengthStrategy outgoingContentStrategy,
+            final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
+            final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+        this(
+                http1Config,
+                charDecoder,
+                charEncoder,
+                incomingContentStrategy,
+                outgoingContentStrategy,
+                null,
+                requestWriterFactory,
+                responseParserFactory);
+    }
+
     public DefaultBHttpClientConnection(
             final Http1Config http1Config,
             final CharsetDecoder charDecoder,
@@ -149,8 +194,62 @@ public class DefaultBHttpClientConnection extends BHttpConnectionBase
         if (len == ContentLengthStrategy.UNDEFINED) {
             throw new LengthRequiredException();
         }
-        try (final OutputStream outStream = createContentOutputStream(len, this.outbuffer, socketHolder.getOutputStream(), entity.getTrailers())) {
+        try (final OutputStream outStream = createContentOutputStream(
+                len, this.outbuffer, new OutputStream() {
+
+                    final OutputStream socketOutputStream = socketHolder.getOutputStream();
+                    final InputStream socketInputStream = socketHolder.getInputStream();
+
+                    long totalBytes = 0;
+
+                    void checkForEarlyResponse(final long totalBytesSent, final int nextWriteSize) throws IOException {
+                        if (responseOutOfOrderStrategy.isEarlyResponseDetected(
+                                request,
+                                DefaultBHttpClientConnection.this,
+                                socketInputStream,
+                                totalBytesSent,
+                                nextWriteSize)) {
+                            throw new ResponseOutOfOrderException();
+                        }
+                    }
+
+                    @Override
+                    public void write(final byte[] b) throws IOException {
+                        checkForEarlyResponse(totalBytes, b.length);
+                        totalBytes += b.length;
+                        socketOutputStream.write(b);
+                    }
+
+                    @Override
+                    public void write(final byte[] b, final int off, final int len) throws IOException {
+                        checkForEarlyResponse(totalBytes, len);
+                        totalBytes += len;
+                        socketOutputStream.write(b, off, len);
+                    }
+
+                    @Override
+                    public void write(final int b) throws IOException {
+                        checkForEarlyResponse(totalBytes, 1);
+                        totalBytes++;
+                        socketOutputStream.write(b);
+                    }
+
+                    @Override
+                    public void flush() throws IOException {
+                        socketOutputStream.flush();
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                        socketOutputStream.close();
+                    }
+
+                }, entity.getTrailers())) {
             entity.writeTo(outStream);
+        } catch (final ResponseOutOfOrderException ex) {
+            if (len > 0) {
+                this.consistent = false;
+            }
         }
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
index 37c2e48..c2becd2 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/DefaultBHttpClientConnectionFactory.java
@@ -41,6 +41,7 @@ import org.apache.hc.core5.http.impl.CharCodingSupport;
 import org.apache.hc.core5.http.io.HttpConnectionFactory;
 import org.apache.hc.core5.http.io.HttpMessageParserFactory;
 import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
 
 /**
  * Default factory for {@link org.apache.hc.core5.http.io.HttpClientConnection}s.
@@ -55,21 +56,23 @@ public class DefaultBHttpClientConnectionFactory
     private final CharCodingConfig charCodingConfig;
     private final ContentLengthStrategy incomingContentStrategy;
     private final ContentLengthStrategy outgoingContentStrategy;
+    private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
     private final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
     private final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
 
-    public DefaultBHttpClientConnectionFactory(
+    private DefaultBHttpClientConnectionFactory(
             final Http1Config http1Config,
             final CharCodingConfig charCodingConfig,
             final ContentLengthStrategy incomingContentStrategy,
             final ContentLengthStrategy outgoingContentStrategy,
+            final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
-        super();
         this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT;
         this.charCodingConfig = charCodingConfig != null ? charCodingConfig : CharCodingConfig.DEFAULT;
         this.incomingContentStrategy = incomingContentStrategy;
         this.outgoingContentStrategy = outgoingContentStrategy;
+        this.responseOutOfOrderStrategy = responseOutOfOrderStrategy;
         this.requestWriterFactory = requestWriterFactory;
         this.responseParserFactory = responseParserFactory;
     }
@@ -77,6 +80,23 @@ public class DefaultBHttpClientConnectionFactory
     public DefaultBHttpClientConnectionFactory(
             final Http1Config http1Config,
             final CharCodingConfig charCodingConfig,
+            final ContentLengthStrategy incomingContentStrategy,
+            final ContentLengthStrategy outgoingContentStrategy,
+            final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
+            final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
+        this(
+                http1Config,
+                charCodingConfig,
+                incomingContentStrategy,
+                outgoingContentStrategy,
+                null,
+                requestWriterFactory,
+                responseParserFactory);
+    }
+
+    public DefaultBHttpClientConnectionFactory(
+            final Http1Config http1Config,
+            final CharCodingConfig charCodingConfig,
             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
         this(http1Config, charCodingConfig, null, null, requestWriterFactory, responseParserFactory);
@@ -100,6 +120,7 @@ public class DefaultBHttpClientConnectionFactory
                 CharCodingSupport.createEncoder(this.charCodingConfig),
                 this.incomingContentStrategy,
                 this.outgoingContentStrategy,
+                this.responseOutOfOrderStrategy,
                 this.requestWriterFactory,
                 this.responseParserFactory);
         conn.bind(socket);
@@ -125,6 +146,7 @@ public class DefaultBHttpClientConnectionFactory
         private CharCodingConfig charCodingConfig;
         private ContentLengthStrategy incomingContentLengthStrategy;
         private ContentLengthStrategy outgoingContentLengthStrategy;
+        private ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
         private HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory;
         private HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory;
 
@@ -150,6 +172,11 @@ public class DefaultBHttpClientConnectionFactory
             return this;
         }
 
+        public Builder responseOutOfOrderStrategy(final ResponseOutOfOrderStrategy responseOutOfOrderStrategy) {
+            this.responseOutOfOrderStrategy = responseOutOfOrderStrategy;
+            return this;
+        }
+
         public Builder requestWriterFactory(
                 final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory) {
             this.requestWriterFactory = requestWriterFactory;
@@ -168,6 +195,7 @@ public class DefaultBHttpClientConnectionFactory
                     charCodingConfig,
                     incomingContentLengthStrategy,
                     outgoingContentLengthStrategy,
+                    responseOutOfOrderStrategy,
                     requestWriterFactory,
                     responseParserFactory);
         }
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..303e61b
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/MonitoringResponseOutOfOrderStrategy.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.impl.io;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Timeout;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link ResponseOutOfOrderStrategy} implementation which checks for premature responses every {@link #chunkSize}
+ * bytes. An 8 KiB chunk size is used by default based on testing using values between 4 KiB and 128 KiB. This is
+ * optimized for correctness and results in a maximum upload speed of 8 MiB/s until {@link #maxChunksToCheck} is
+ * reached.
+ *
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class MonitoringResponseOutOfOrderStrategy implements ResponseOutOfOrderStrategy {
+
+    private static final int DEFAULT_CHUNK_SIZE = 8 * 1024;
+
+    public static final MonitoringResponseOutOfOrderStrategy INSTANCE = new MonitoringResponseOutOfOrderStrategy();
+
+    private final long chunkSize;
+    private final long maxChunksToCheck;
+
+    /**
+     * Instantiates a default {@link MonitoringResponseOutOfOrderStrategy}. {@link #INSTANCE} may be used instead.
+     */
+    public MonitoringResponseOutOfOrderStrategy() {
+        this(DEFAULT_CHUNK_SIZE);
+    }
+
+    /**
+     * Instantiates a {@link MonitoringResponseOutOfOrderStrategy} with unlimited {@link #maxChunksToCheck}.
+     *
+     * @param chunkSize The chunk size after which a response check is executed.
+     */
+    public MonitoringResponseOutOfOrderStrategy(final long chunkSize) {
+        this(chunkSize, Long.MAX_VALUE);
+    }
+
+    /**
+     * Instantiates a {@link MonitoringResponseOutOfOrderStrategy}.
+     *
+     * @param chunkSize The chunk size after which a response check is executed.
+     * @param maxChunksToCheck The maximum number of chunks to check, allowing expensive checks to be avoided
+     *                         after a sufficient portion of the request entity has been transferred.
+     */
+    public MonitoringResponseOutOfOrderStrategy(final long chunkSize, final long maxChunksToCheck) {
+        this.chunkSize = Args.positive(chunkSize, "chunkSize");
+        this.maxChunksToCheck = Args.positive(maxChunksToCheck, "maxChunksToCheck");
+    }
+
+    @Override
+    public boolean isEarlyResponseDetected(
+            final ClassicHttpRequest request,
+            final HttpClientConnection connection,
+            final InputStream inputStream,
+            final long totalBytesSent,
+            final long nextWriteSize) throws IOException {
+        if (nextWriteStartsNewChunk(totalBytesSent, nextWriteSize)) {
+            final boolean ssl = connection.getSSLSession() != null;
+            return ssl ? connection.isDataAvailable(Timeout.ONE_MILLISECOND) : (inputStream.available() > 0);
+        }
+        return false;
+    }
+
+    private boolean nextWriteStartsNewChunk(final long totalBytesSent, final long nextWriteSize) {
+        final long currentChunkIndex = Math.min(totalBytesSent / chunkSize, maxChunksToCheck);
+        final long newChunkIndex = Math.min((totalBytesSent + nextWriteSize) / chunkSize, maxChunksToCheck);
+        return currentChunkIndex < newChunkIndex;
+    }
+
+    @Override
+    public String toString() {
+        return "DefaultResponseOutOfOrderStrategy{chunkSize=" + chunkSize + ", maxChunksToCheck=" + maxChunksToCheck + '}';
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..94606e2
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/NoResponseOutOfOrderStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.impl.io;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+
+import java.io.InputStream;
+
+/**
+ * An implementation of {@link ResponseOutOfOrderStrategy} which does not check for early responses.
+ *
+ * Early response detection requires 1ms blocking reads and incurs a hefty performance cost for
+ * large uploads.
+ *
+ * @see MonitoringResponseOutOfOrderStrategy
+ * @since 5.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class NoResponseOutOfOrderStrategy implements ResponseOutOfOrderStrategy {
+
+    public static final NoResponseOutOfOrderStrategy INSTANCE = new NoResponseOutOfOrderStrategy();
+
+    @Override
+    public boolean isEarlyResponseDetected(
+            final ClassicHttpRequest request,
+            final HttpClientConnection connection,
+            final InputStream inputStream,
+            final long totalBytesSent,
+            final long nextWriteSize) {
+        return false;
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ResponseOutOfOrderException.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ResponseOutOfOrderException.java
new file mode 100644
index 0000000..5744342
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/io/ResponseOutOfOrderException.java
@@ -0,0 +1,41 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.impl.io;
+
+import java.io.IOException;
+
+/**
+ * Signals an early (out of order) response.
+ */
+class ResponseOutOfOrderException extends IOException {
+
+    public ResponseOutOfOrderException() {
+        super();
+    }
+
+}
\ No newline at end of file
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..ff03f68
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/ResponseOutOfOrderStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.io;
+
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Represents a strategy to determine how frequently the client should check for an out of order response.
+ * An out of order response is sent before the server has read the full request. If the client fails to
+ * check for an early response then a {@link java.net.SocketException} or {@link java.net.SocketTimeoutException}
+ * may be thrown while writing the request entity after a timeout is reached on either the client or server.
+ *
+ * @since 5.1
+ */
+@Internal
+public interface ResponseOutOfOrderStrategy {
+
+    /**
+     * Called before each write to the to a socket {@link java.io.OutputStream} with the number of
+     * bytes that have already been sent, and the size of the write that will occur if this check
+     * does not encounter an out of order response.
+     *
+     * @param request The current request.
+     * @param connection The connection used to send the current request.
+     * @param inputStream The response stream, this may be used to check for an early response.
+     * @param totalBytesSent Number of bytes that have already been sent.
+     * @param nextWriteSize The size of a socket write operation that will follow this check.
+     * @return True if an early response was detected, otherwise false.
+     * @throws IOException in case of a network failure while checking for an early response.
+     */
+    boolean isEarlyResponseDetected(
+            ClassicHttpRequest request,
+            HttpClientConnection connection,
+            InputStream inputStream,
+            long totalBytesSent,
+            long nextWriteSize) throws IOException;
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/Timeout.java b/httpcore5/src/main/java/org/apache/hc/core5/util/Timeout.java
index f6a835a..77619f6 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/util/Timeout.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/util/Timeout.java
@@ -47,6 +47,11 @@ public class Timeout extends TimeValue {
     public static final Timeout ZERO_MILLISECONDS = Timeout.of(0, TimeUnit.MILLISECONDS);
 
     /**
+     * A one milliseconds {@link Timeout}.
+     */
+    public static final Timeout ONE_MILLISECOND = Timeout.of(1, TimeUnit.MILLISECONDS);
+
+    /**
      * A disabled timeout represented as 0 {@code MILLISECONDS}.
      */
     public static final Timeout DISABLED = ZERO_MILLISECONDS;
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java
new file mode 100644
index 0000000..4c8966e
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestMonitoringResponseOutOfOrderStrategy.java
@@ -0,0 +1,133 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.http.impl.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.net.ssl.SSLSession;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.HttpClientConnection;
+import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+public class TestMonitoringResponseOutOfOrderStrategy {
+
+    private static final ClassicHttpRequest REQUEST = new BasicClassicHttpRequest("POST", "/path");
+
+    @Test
+    public void testFirstByteIsNotCheckedSsl() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                // SSLSocket streams report zero bytes available
+                socketInputStream(0),
+                0,
+                1);
+        Assert.assertFalse(earlyResponse);
+    }
+
+    @Test
+    public void testFirstByteIsNotCheckedPlain() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, false),
+                socketInputStream(1),
+                0,
+                1);
+        Assert.assertFalse(earlyResponse);
+    }
+
+    @Test
+    public void testWritesWithinChunkAreNotChecked() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                socketInputStream(0),
+                1,
+                8190);
+        Assert.assertFalse(
+                "There is data available, but checks shouldn't occur until just prior to the 8192nd byte",
+                earlyResponse);
+    }
+
+    @Test
+    public void testWritesAcrossChunksAreChecked() throws IOException {
+        final boolean earlyResponse = MonitoringResponseOutOfOrderStrategy.INSTANCE.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                socketInputStream(0),
+                8191,
+                1);
+        Assert.assertTrue(earlyResponse);
+    }
+
+    @Test
+    public void testMaximumChunks() throws IOException {
+        final ResponseOutOfOrderStrategy strategy = new MonitoringResponseOutOfOrderStrategy(1, 2);
+        Assert.assertTrue(strategy.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                socketInputStream(0),
+                0,
+                1));
+        Assert.assertTrue(strategy.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                socketInputStream(0),
+                1,
+                2));
+        Assert.assertFalse(strategy.isEarlyResponseDetected(
+                REQUEST,
+                connection(true, true),
+                socketInputStream(0),
+                2,
+                3));
+    }
+
+    private static InputStream socketInputStream(final int available) throws IOException {
+        final InputStream stream = Mockito.mock(InputStream.class);
+        Mockito.when(stream.available()).thenReturn(available);
+        return stream;
+    }
+
+    private static HttpClientConnection connection(final boolean dataAvailable, final boolean ssl) throws IOException {
+        final HttpClientConnection connection = Mockito.mock(HttpClientConnection.class);
+        Mockito.when(connection.isDataAvailable(ArgumentMatchers.any(Timeout.class))).thenReturn(dataAvailable);
+        if (ssl) {
+            Mockito.when(connection.getSSLSession()).thenReturn(Mockito.mock(SSLSession.class));
+        }
+        return connection;
+    }
+}


[httpcomponents-core] 08/13: Use decimal numbers for endpoint/execution IDs

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit b7f17306b207378cb4f6046eba9bd6a8e8d1f01a
Author: Michael Osipov <mi...@apache.org>
AuthorDate: Tue Aug 18 12:19:02 2020 +0200

    Use decimal numbers for endpoint/execution IDs
    
    This closes #219
---
 httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java  | 2 +-
 .../org/apache/hc/core5/http/examples/AsyncReverseProxyExample.java     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java
index 3cffb58..e7e1e7e 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java
@@ -71,7 +71,7 @@ class IOSessionImpl implements IOSession {
         this.commandQueue = new ConcurrentLinkedDeque<>();
         this.lock = new ReentrantLock();
         this.socketTimeout = Timeout.DISABLED;
-        this.id = String.format(type + "-%08X", COUNT.getAndIncrement());
+        this.id = String.format(type + "-%010d", COUNT.getAndIncrement());
         this.handlerRef = new AtomicReference<>();
         this.status = new AtomicReference<>(Status.ACTIVE);
         final long currentTimeMillis = System.currentTimeMillis();
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncReverseProxyExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncReverseProxyExample.java
index 8136cab..5771d97 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncReverseProxyExample.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncReverseProxyExample.java
@@ -251,7 +251,7 @@ public class AsyncReverseProxyExample {
         AsyncClientEndpoint clientEndpoint;
 
         ProxyExchangeState() {
-            this.id = String.format("%08X", COUNT.getAndIncrement());
+            this.id = String.format("%010d", COUNT.getAndIncrement());
         }
 
     }


[httpcomponents-core] 09/13: HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit ac5754bedc16c9331771dfbfa641842910d600e9
Author: Carter Kozak <ck...@apache.org>
AuthorDate: Mon Sep 14 11:14:48 2020 -0400

    HTTPCORE-649: Implement ByteArrayBuffer.append(ByteBuffer)
---
 .../apache/hc/core5/http2/hpack/HPackDecoder.java  | 18 ++---
 .../apache/hc/core5/http2/hpack/HPackEncoder.java  | 11 +---
 .../http/nio/entity/BasicAsyncEntityConsumer.java  | 11 +---
 .../org/apache/hc/core5/util/ByteArrayBuffer.java  | 16 +++++
 .../entity/TestAbstractBinAsyncEntityConsumer.java | 11 +---
 .../apache/hc/core5/util/TestByteArrayBuffer.java  | 76 ++++++++++++++++++++++
 6 files changed, 101 insertions(+), 42 deletions(-)

diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java
index dd9379c..1df29a2 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackDecoder.java
@@ -125,21 +125,15 @@ public final class HPackDecoder {
     }
 
     static void decodePlainString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
-
         final int strLen = decodeInt(src, 7);
-        if (strLen > src.remaining()) {
+        final int remaining = src.remaining();
+        if (strLen > remaining) {
             throw new HPackException(UNEXPECTED_EOS);
         }
-        if (src.hasArray()) {
-            final byte[] b = src.array();
-            final int off = src.position();
-            buffer.append(b, src.arrayOffset() + off, strLen);
-            src.position(off + strLen);
-        } else {
-            for (int i = 0; i < strLen; i++) {
-                buffer.append(src.get());
-            }
-        }
+        final int originalLimit = src.limit();
+        src.limit(originalLimit - (remaining - strLen));
+        buffer.append(src);
+        src.limit(originalLimit);
     }
 
     static void decodeHuffman(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java
index 2824760..621bdf4 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/hpack/HPackEncoder.java
@@ -110,16 +110,7 @@ public final class HPackEncoder {
         } else {
             dst.ensureCapacity(strLen + 8);
             encodeInt(dst, 7, strLen, 0x0);
-            if (src.hasArray()) {
-                final byte[] b = src.array();
-                final int off = src.position();
-                dst.append(b, src.arrayOffset() + off, strLen);
-                src.position(off + strLen);
-            } else {
-                while (src.hasRemaining()) {
-                    dst.append(src.get());
-                }
-            }
+            dst.append(src);
         }
     }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityConsumer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityConsumer.java
index 26487ec..648bde6 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityConsumer.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/entity/BasicAsyncEntityConsumer.java
@@ -60,16 +60,7 @@ public class BasicAsyncEntityConsumer extends AbstractBinAsyncEntityConsumer<byt
 
     @Override
     protected void data(final ByteBuffer src, final boolean endOfStream) throws IOException {
-        if (src == null) {
-            return;
-        }
-        if (src.hasArray()) {
-            buffer.append(src.array(), src.arrayOffset() + src.position(), src.remaining());
-        } else {
-            while (src.hasRemaining()) {
-                buffer.append(src.get());
-            }
-        }
+        buffer.append(src);
     }
 
     @Override
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java b/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java
index 573faa5..c9479c3 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/util/ByteArrayBuffer.java
@@ -28,6 +28,7 @@
 package org.apache.hc.core5.util;
 
 import java.io.Serializable;
+import java.nio.ByteBuffer;
 
 /**
  * A resizable byte array.
@@ -171,6 +172,21 @@ public final class ByteArrayBuffer implements Serializable {
         append(b.array(), off, len);
     }
 
+    public void append(final ByteBuffer buffer) {
+        if (buffer == null) {
+            return;
+        }
+        final int bufferLength = buffer.remaining();
+        if (bufferLength > 0) {
+            final int newLength = this.len + bufferLength;
+            if (newLength > this.array.length) {
+                expand(newLength);
+            }
+            buffer.get(this.array, this.len, bufferLength);
+            this.len = newLength;
+        }
+    }
+
     /**
      * Clears content of the buffer. The underlying byte array is not resized.
      */
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/entity/TestAbstractBinAsyncEntityConsumer.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/entity/TestAbstractBinAsyncEntityConsumer.java
index 7c7b12a..ea7609e 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/entity/TestAbstractBinAsyncEntityConsumer.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/entity/TestAbstractBinAsyncEntityConsumer.java
@@ -62,16 +62,7 @@ public class TestAbstractBinAsyncEntityConsumer {
 
         @Override
         protected void data(final ByteBuffer src, final boolean endOfStream) throws IOException {
-            if (src == null) {
-                return;
-            }
-            if (src.hasArray()) {
-                buffer.append(src.array(), src.arrayOffset() + src.position(), src.remaining());
-            } else {
-                while (src.hasRemaining()) {
-                    buffer.append(src.get());
-                }
-            }
+            buffer.append(src);
         }
 
         @Override
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/util/TestByteArrayBuffer.java b/httpcore5/src/test/java/org/apache/hc/core5/util/TestByteArrayBuffer.java
index 204426a..6d8d6f5 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/util/TestByteArrayBuffer.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/util/TestByteArrayBuffer.java
@@ -31,6 +31,7 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 
 import org.junit.Assert;
@@ -109,6 +110,73 @@ public class TestByteArrayBuffer {
     }
 
     @Test
+    public void testAppendHeapByteBuffer() {
+        final ByteArrayBuffer buffer = new ByteArrayBuffer(4);
+        Assert.assertEquals(4, buffer.capacity());
+
+        final ByteBuffer tmp = ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5, 6});
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(8, buffer.capacity());
+        Assert.assertEquals(6, buffer.length());
+
+        tmp.clear();
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(16, buffer.capacity());
+        Assert.assertEquals(12, buffer.length());
+        Assert.assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6}, buffer.toByteArray());
+    }
+
+    @Test
+    public void testAppendHeapByteBufferWithOffset() {
+        final ByteArrayBuffer buffer = new ByteArrayBuffer(4);
+        Assert.assertEquals(4, buffer.capacity());
+
+        final ByteBuffer tmp = ByteBuffer.wrap(new byte[] { 7, 7, 1, 2, 3, 4, 5, 6, 7, 7}, 2, 6).slice();
+        Assert.assertTrue("Validate this is testing a buffer with an array offset", tmp.arrayOffset() > 0);
+
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(8, buffer.capacity());
+        Assert.assertEquals(6, buffer.length());
+
+        tmp.clear();
+        Assert.assertEquals(6, tmp.remaining());
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(16, buffer.capacity());
+        Assert.assertEquals(12, buffer.length());
+        Assert.assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6}, buffer.toByteArray());
+    }
+
+    @Test
+    public void testAppendDirectByteBuffer() {
+        final ByteArrayBuffer buffer = new ByteArrayBuffer(4);
+        Assert.assertEquals(4, buffer.capacity());
+
+        final ByteBuffer tmp = ByteBuffer.allocateDirect(6);
+        tmp.put(new byte[] { 1, 2, 3, 4, 5, 6}).flip();
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(8, buffer.capacity());
+        Assert.assertEquals(6, buffer.length());
+
+        tmp.clear();
+        buffer.append(tmp);
+
+        Assert.assertFalse("The input buffer should be drained", tmp.hasRemaining());
+        Assert.assertEquals(16, buffer.capacity());
+        Assert.assertEquals(12, buffer.length());
+        Assert.assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6}, buffer.toByteArray());
+    }
+
+    @Test
     public void testInvalidAppend() throws Exception {
         final ByteArrayBuffer buffer = new ByteArrayBuffer(4);
         buffer.append((byte[])null, 0, 0);
@@ -251,6 +319,14 @@ public class TestByteArrayBuffer {
     }
 
     @Test
+    public void testAppendNullByteBuffer() throws Exception {
+        final ByteArrayBuffer buffer = new ByteArrayBuffer(8);
+        final ByteBuffer nullBuffer = null;
+        buffer.append(nullBuffer);
+        Assert.assertEquals(0, buffer.length());
+    }
+
+    @Test
     public void testInvalidAppendCharArrayAsAscii() throws Exception {
         final ByteArrayBuffer buffer = new ByteArrayBuffer(4);
         buffer.append((char[])null, 0, 0);


[httpcomponents-core] 12/13: Update RELEASE_NOTES.txt

Posted by ck...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ckozak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git

commit 9d3339cacd4cd62695e070f473fe4e66f227bbe5
Author: Gary Gregory <ga...@users.noreply.github.com>
AuthorDate: Mon Sep 14 08:50:31 2020 -0400

    Update RELEASE_NOTES.txt
    
    Start sentences with a capital letter.
---
 RELEASE_NOTES.txt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index e130d97..ed9f8bb 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -30,16 +30,16 @@ Change Log
 * HTTPCORE-639: Add a configurable ResponseOutOfOrder strategy for DefaultBHttpClientConnection.
   Contributed by Carter Kozak <c4kofony at gmail.com>
 
-* RFC 3986 conformance: support percent-encoded reserved characters in the host component.
+* RFC 3986 conformance: Support percent-encoded reserved characters in the host component.
   Contributed by Oleg Kalnichevski <olegk at apache.org>
 
-* RFC 3986 conformance: revised URI parsing and formatting.
+* RFC 3986 conformance: Revised URI parsing and formatting.
   Contributed by Oleg Kalnichevski <olegk at apache.org>
 
 * Better parse and format methods for URIAuthority.
   Contributed by Oleg Kalnichevski <olegk at apache.org>
 
-* HTTPCORE-628: do not encode blanks as + in URI query component.
+* HTTPCORE-628: Do not encode blanks as + in URI query component.
   Contributed by Oleg Kalnichevski <olegk at apache.org>
 
 * Async connection listeners to support passing attachments to endpoints.