You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2026/08/16 11:21:22 UTC

(httpcomponents-client) 01/01: Header parsing optimization in cookie, authentication and caching protocol code

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

ok2c pushed a commit to branch feature/parsing_optimization
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit 3b2aeb10f6998bf4fc1d91608a88cadad04c13f1
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Aug 16 12:08:29 2026 +0200

    Header parsing optimization in cookie, authentication and caching protocol code
---
 .../http/impl/cache/CacheControlHeaderParser.java  | 54 +++++----------
 .../http/impl/cookie/RFC6265CookieSpec.java        | 78 +++++++++++++---------
 .../http/protocol/NextNonceInterceptor.java        | 13 +---
 .../http/impl/cookie/TestRFC6265CookieSpec.java    | 18 ++---
 4 files changed, 69 insertions(+), 94 deletions(-)

diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
index 6aae25450..58d9762ec 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java
@@ -43,6 +43,7 @@
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.http.message.ParserCursor;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.CharArrayBuffer;
@@ -110,38 +111,23 @@ protected CacheControlHeaderParser() {
 
     public void parse(final Iterator<Header> headerIterator, final BiConsumer<String, String> consumer) {
         while (headerIterator.hasNext()) {
-            final Header header = headerIterator.next();
-            final CharArrayBuffer buffer;
-            final Tokenizer.Cursor cursor;
-            if (header instanceof FormattedHeader) {
-                buffer = ((FormattedHeader) header).getBuffer();
-                cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length());
-            } else {
-                final String s = header.getValue();
-                if (s == null) {
-                    continue;
-                }
-                buffer = new CharArrayBuffer(s.length());
-                buffer.append(s);
-                cursor = new Tokenizer.Cursor(0, buffer.length());
-            }
-
-            // Parse the header
-            while (!cursor.atEnd()) {
-                final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
-                String value = null;
-                if (!cursor.atEnd()) {
-                    final int valueDelim = buffer.charAt(cursor.getPos());
-                    cursor.updatePos(cursor.getPos() + 1);
-                    if (valueDelim == EQUAL_CHAR) {
-                        value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
-                        if (!cursor.atEnd()) {
-                            cursor.updatePos(cursor.getPos() + 1);
+            MessageSupport.parseHeader(headerIterator.next(), (buffer, cursor) -> {
+                while (!cursor.atEnd()) {
+                    final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
+                    String value = null;
+                    if (!cursor.atEnd()) {
+                        final int valueDelim = buffer.charAt(cursor.getPos());
+                        cursor.updatePos(cursor.getPos() + 1);
+                        if (valueDelim == EQUAL_CHAR) {
+                            value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
+                            if (!cursor.atEnd()) {
+                                cursor.updatePos(cursor.getPos() + 1);
+                            }
                         }
                     }
+                    consumer.accept(name, value);
                 }
-                consumer.accept(name, value);
-            }
+            });
         }
     }
 
@@ -172,17 +158,13 @@ public final ResponseCacheControl parseResponse(final Iterator<Header> headerIte
             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) {
                 builder.setNoCache(true);
                 if (value != null) {
-                    final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length());
+                    final ParserCursor valCursor = new ParserCursor(0, value.length());
                     final Set<String> noCacheFields = new HashSet<>();
-                    while (!valCursor.atEnd()) {
-                        final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS);
+                    MessageSupport.parseTokens(value, valCursor, token -> {
                         if (!TextUtils.isBlank(token)) {
                             noCacheFields.add(token);
                         }
-                        if (!valCursor.atEnd()) {
-                            valCursor.updatePos(valCursor.getPos() + 1);
-                        }
-                    }
+                    });
                     builder.setNoCacheFields(noCacheFields);
                 }
             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/RFC6265CookieSpec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/RFC6265CookieSpec.java
index c15c78808..4a442c8d3 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/RFC6265CookieSpec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/cookie/RFC6265CookieSpec.java
@@ -45,10 +45,10 @@
 import org.apache.hc.client5.http.cookie.MalformedCookieException;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.http.FormattedHeader;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.ParseException;
 import org.apache.hc.core5.http.message.BufferedHeader;
+import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.CharArrayBuffer;
 import org.apache.hc.core5.util.Tokenizer;
@@ -109,43 +109,65 @@ static String getDefaultDomain(final CookieOrigin origin) {
     public final List<Cookie> parse(final Header header, final CookieOrigin origin) throws MalformedCookieException {
         Args.notNull(header, "Header");
         Args.notNull(origin, "Cookie origin");
-        if (!header.getName().equalsIgnoreCase("Set-Cookie")) {
-            throw new MalformedCookieException("Unrecognized cookie header: '" + header + "'");
+        final RawCookie rawCookie = MessageSupport.parserHeaderValue(header, this::parseCookie);
+        if (rawCookie == null) {
+            throw new MalformedCookieException("Cookie value is invalid");
         }
-        final CharArrayBuffer buffer;
-        final Tokenizer.Cursor cursor;
-        if (header instanceof FormattedHeader) {
-            buffer = ((FormattedHeader) header).getBuffer();
-            cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length());
-        } else {
-            final String s = header.getValue();
-            if (s == null) {
-                throw new MalformedCookieException("Header value is null");
+
+        final BasicClientCookie cookie = new BasicClientCookie(rawCookie.name, rawCookie.value);
+        cookie.setPath(getDefaultPath(origin));
+        cookie.setDomain(getDefaultDomain(origin));
+        cookie.setCreationDate(Instant.now());
+
+        for (final Map.Entry<String, String> entry: rawCookie.attribMap.entrySet()) {
+            final String paramName = entry.getKey();
+            final String paramValue = entry.getValue();
+            cookie.setAttribute(paramName, paramValue);
+            final CookieAttributeHandler handler = this.attribHandlerMap.get(paramName);
+            if (handler != null) {
+                handler.parse(cookie, paramValue);
             }
-            buffer = new CharArrayBuffer(s.length());
-            buffer.append(s);
-            cursor = new Tokenizer.Cursor(0, buffer.length());
         }
+        return Collections.singletonList(cookie);
+    }
+
+    static class RawCookie {
+
+        final String name;
+        final String value;
+        final Map<String, String> attribMap;
+
+        RawCookie(final String name, final String value, final Map<String, String> attribMap) {
+            this.name = name;
+            this.value = value;
+            this.attribMap = attribMap;
+        }
+
+        @Override
+        public String toString() {
+            return name + " = " + value + " " + attribMap;
+        }
+
+    }
+
+    private RawCookie parseCookie(final CharSequence buffer,
+                                  final Tokenizer.Cursor cursor) {
         final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
         if (name.isEmpty()) {
-            return Collections.emptyList();
+            return null;
         }
         if (cursor.atEnd()) {
-            return Collections.emptyList();
+            return null;
         }
         final int valueDelim = buffer.charAt(cursor.getPos());
         cursor.updatePos(cursor.getPos() + 1);
         if (valueDelim != '=') {
-            throw new MalformedCookieException("Cookie value is invalid: '" + header + "'");
+            return null;
         }
         final String value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
         if (!cursor.atEnd()) {
             cursor.updatePos(cursor.getPos() + 1);
         }
-        final BasicClientCookie cookie = new BasicClientCookie(name, value);
-        cookie.setPath(getDefaultPath(origin));
-        cookie.setDomain(getDefaultDomain(origin));
-        cookie.setCreationDate(Instant.now());
 
         final Map<String, String> attribMap = new LinkedHashMap<>();
         while (!cursor.atEnd()) {
@@ -162,7 +184,6 @@ public final List<Cookie> parse(final Header header, final CookieOrigin origin)
                     }
                 }
             }
-            cookie.setAttribute(paramName, paramValue);
             attribMap.put(paramName, paramValue);
         }
         // Ignore 'Expires' if 'Max-Age' is present
@@ -170,16 +191,7 @@ public final List<Cookie> parse(final Header header, final CookieOrigin origin)
             attribMap.remove(Cookie.EXPIRES_ATTR);
         }
 
-        for (final Map.Entry<String, String> entry: attribMap.entrySet()) {
-            final String paramName = entry.getKey();
-            final String paramValue = entry.getValue();
-            final CookieAttributeHandler handler = this.attribHandlerMap.get(paramName);
-            if (handler != null) {
-                handler.parse(cookie, paramValue);
-            }
-        }
-
-        return Collections.singletonList(cookie);
+        return new RawCookie(name, value, attribMap);
     }
 
     @Override
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java
index a5bb2a471..d68aed719 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/NextNonceInterceptor.java
@@ -30,10 +30,10 @@
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.EntityDetails;
-import org.apache.hc.core5.http.FormattedHeader;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.http.message.ParserCursor;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.util.Args;
@@ -101,16 +101,7 @@ public void process(final HttpResponse response, final EntityDetails entity, fin
 
         final Header header = response.getFirstHeader(AUTHENTICATION_INFO_HEADER);
         if (header != null) {
-            final String nextNonce;
-            if (header instanceof FormattedHeader) {
-                final CharSequence buf = ((FormattedHeader) header).getBuffer();
-                final ParserCursor cursor = new ParserCursor(((FormattedHeader) header).getValuePos(), buf.length());
-                nextNonce = parseNextNonce(buf, cursor);
-            } else {
-                final CharSequence headerValue = header.getValue();
-                final ParserCursor cursor = new ParserCursor(0, headerValue.length());
-                nextNonce = parseNextNonce(headerValue, cursor);
-            }
+            final String nextNonce = MessageSupport.parserHeaderValue(header, this::parseNextNonce);
             if (!TextUtils.isBlank(nextNonce)) {
                 HttpClientContext.castOrCreate(context).setNextNonce(nextNonce);
             }
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/cookie/TestRFC6265CookieSpec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/cookie/TestRFC6265CookieSpec.java
index 6bb9ad14f..56897c9b8 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/cookie/TestRFC6265CookieSpec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/cookie/TestRFC6265CookieSpec.java
@@ -85,24 +85,14 @@ void testParseCookieQuotedValue() throws Exception {
         Assertions.assertEquals("stuff", cookie.getAttribute("this"));
     }
 
-    @Test
-    void testParseCookieWrongHeader() {
-        final RFC6265CookieSpec cookiespec = new RFC6265CookieSpec();
-
-        final Header header = new BasicHeader("Set-Cookie2", "blah");
-        final CookieOrigin origin = new CookieOrigin("host", 80, "/path/", true);
-        Assertions.assertThrows(MalformedCookieException.class, () ->
-                cookiespec.parse(header, origin));
-    }
-
     @Test
     void testParseCookieMissingName() throws Exception {
         final RFC6265CookieSpec cookiespec = new RFC6265CookieSpec();
 
         final Header header = new BasicHeader("Set-Cookie", "=blah ; this = stuff;");
         final CookieOrigin origin = new CookieOrigin("host", 80, "/path/", true);
-        final List<Cookie> cookies = cookiespec.parse(header, origin);
-        Assertions.assertEquals(0, cookies.size());
+        Assertions.assertThrows(MalformedCookieException.class, () ->
+                cookiespec.parse(header, origin));
     }
 
     @Test
@@ -111,8 +101,8 @@ void testParseCookieMissingValue1() throws Exception {
 
         final Header header = new BasicHeader("Set-Cookie", "blah");
         final CookieOrigin origin = new CookieOrigin("host", 80, "/path/", true);
-        final List<Cookie> cookies = cookiespec.parse(header, origin);
-        Assertions.assertEquals(0, cookies.size());
+        Assertions.assertThrows(MalformedCookieException.class, () ->
+                cookiespec.parse(header, origin));
     }
 
     @Test