You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2026/09/15 03:53:48 UTC

(httpcomponents-core) branch master updated: RFC 9651: Structured Field Values for HTTP

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0863485ae RFC 9651: Structured Field Values for HTTP
0863485ae is described below

commit 0863485ae85927df1e9a7f163bfbd7d7753806ca
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Wed Sep 2 18:48:28 2026 +0200

    RFC 9651: Structured Field Values for HTTP
---
 .../http/structured/StructuredFieldBareItem.java   |   265 +
 .../http/structured/StructuredFieldDictionary.java |   193 +
 .../http/structured/StructuredFieldHeaders.java    |   170 +
 .../http/structured/StructuredFieldInnerList.java  |   159 +
 .../core5/http/structured/StructuredFieldItem.java |   177 +
 .../core5/http/structured/StructuredFieldList.java |   138 +
 .../http/structured/StructuredFieldMember.java     |    42 +
 .../http/structured/StructuredFieldParameters.java |   194 +
 .../http/structured/StructuredFieldParser.java     |   639 +
 .../http/structured/StructuredFieldRules.java      |   110 +
 .../http/structured/StructuredFieldSerializer.java |   348 +
 .../core5/http/structured/StructuredFieldType.java |    52 +
 .../http/structured/StructuredFieldValue.java      |    36 +
 .../hc/core5/http/structured/package-info.java     |    32 +
 .../java/org/apache/hc/core5/net/PercentCodec.java |    31 +-
 .../http/examples/StructuredFieldsExample.java     |   106 +
 .../structured/TestStructuredFieldConformance.java |   558 +
 .../http/structured/TestStructuredFieldParser.java |   135 +
 .../TestStructuredFieldRealWorldHeaders.java       |    99 +
 .../structured/TestStructuredFieldSerializer.java  |   191 +
 .../structured/TestStructuredFieldStrictness.java  |   106 +
 .../org/apache/hc/core5/net/TestPercentCodec.java  |    10 +
 .../hc/core5/http/structured/rfc9651/binary.json   |   106 +
 .../hc/core5/http/structured/rfc9651/boolean.json  |    75 +
 .../hc/core5/http/structured/rfc9651/date.json     |   107 +
 .../core5/http/structured/rfc9651/dictionary.json  |   169 +
 .../http/structured/rfc9651/display-string.json    |   137 +
 .../hc/core5/http/structured/rfc9651/examples.json |   208 +
 .../hc/core5/http/structured/rfc9651/item.json     |    34 +
 .../http/structured/rfc9651/key-generated.json     |  7013 +++++
 .../http/structured/rfc9651/large-generated.json   | 28819 +++++++++++++++++++
 .../hc/core5/http/structured/rfc9651/list.json     |    74 +
 .../hc/core5/http/structured/rfc9651/listlist.json |    76 +
 .../http/structured/rfc9651/number-generated.json  |  2374 ++
 .../hc/core5/http/structured/rfc9651/number.json   |   255 +
 .../core5/http/structured/rfc9651/param-dict.json  |   113 +
 .../core5/http/structured/rfc9651/param-list.json  |   151 +
 .../http/structured/rfc9651/param-listlist.json    |    36 +
 .../rfc9651/serialisation-tests/key-generated.json |  6239 ++++
 .../rfc9651/serialisation-tests/number.json        |    57 +
 .../serialisation-tests/string-generated.json      |   299 +
 .../serialisation-tests/token-generated.json       |  1490 +
 .../http/structured/rfc9651/string-generated.json  |  2335 ++
 .../hc/core5/http/structured/rfc9651/string.json   |    88 +
 .../http/structured/rfc9651/token-generated.json   |  2862 ++
 .../hc/core5/http/structured/rfc9651/token.json    |    38 +
 46 files changed, 56938 insertions(+), 8 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java
new file mode 100644
index 000000000..9d40aadea
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldBareItem.java
@@ -0,0 +1,265 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable Structured Field bare Item.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldBareItem {
+
+    private static final BigDecimal DECIMAL_ROUNDING_LIMIT = new BigDecimal("999999999999.9995");
+
+    private final StructuredFieldType type;
+    private final Object value;
+
+    private StructuredFieldBareItem(final StructuredFieldType type, final Object value) {
+        this.type = type;
+        this.value = value;
+    }
+
+    /**
+     * Creates an Integer bare Item.
+     *
+     * @param value the integer value.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the value is outside the Structured Field range.
+     */
+    public static StructuredFieldBareItem ofInteger(final long value) {
+        checkInteger(value);
+        return new StructuredFieldBareItem(StructuredFieldType.INTEGER, Long.valueOf(value));
+    }
+
+    /**
+     * Creates a Decimal bare Item, applying the Structured Field rounding algorithm.
+     *
+     * @param value the decimal value.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the rounded value has more than 12 integer digits.
+     */
+    public static StructuredFieldBareItem ofDecimal(final BigDecimal value) {
+        Args.notNull(value, "Decimal value");
+        Args.check(value.abs().compareTo(DECIMAL_ROUNDING_LIMIT) < 0,
+                "Structured Field Decimal has more than 12 integer digits after rounding");
+        BigDecimal normalized = value.setScale(3, RoundingMode.HALF_EVEN).stripTrailingZeros();
+        if (normalized.scale() < 1) {
+            normalized = normalized.setScale(1);
+        }
+        final int integerDigits = Math.max(1, normalized.precision() - normalized.scale());
+        Args.check(integerDigits <= 12,
+                "Structured Field Decimal has more than 12 integer digits after rounding");
+        return new StructuredFieldBareItem(StructuredFieldType.DECIMAL, normalized);
+    }
+
+    /**
+     * Creates an ASCII String bare Item.
+     *
+     * @param value the printable ASCII value.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the value contains a character outside SP through {@code ~}.
+     */
+    public static StructuredFieldBareItem ofString(final String value) {
+        StructuredFieldRules.validateString(value);
+        return new StructuredFieldBareItem(StructuredFieldType.STRING, value);
+    }
+
+    /**
+     * Creates a Token bare Item.
+     *
+     * @param value the token value.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the value is not an Structured Field token.
+     */
+    public static StructuredFieldBareItem ofToken(final String value) {
+        StructuredFieldRules.validateToken(value);
+        return new StructuredFieldBareItem(StructuredFieldType.TOKEN, value);
+    }
+
+    /**
+     * Creates a Byte Sequence bare Item using a defensive copy.
+     *
+     * @param value the byte sequence.
+     * @return the bare Item.
+     */
+    public static StructuredFieldBareItem ofByteSequence(final byte[] value) {
+        Args.notNull(value, "Byte Sequence value");
+        return new StructuredFieldBareItem(StructuredFieldType.BYTE_SEQUENCE, value.clone());
+    }
+
+    /**
+     * Creates a Boolean bare Item.
+     *
+     * @param value the boolean value.
+     * @return the bare Item.
+     */
+    public static StructuredFieldBareItem ofBoolean(final boolean value) {
+        return new StructuredFieldBareItem(StructuredFieldType.BOOLEAN, Boolean.valueOf(value));
+    }
+
+    /**
+     * Creates a Date bare Item expressed as seconds since the Unix epoch.
+     *
+     * @param epochSeconds seconds since the Unix epoch.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the value is outside the Structured Field Integer range.
+     */
+    public static StructuredFieldBareItem ofDate(final long epochSeconds) {
+        checkInteger(epochSeconds);
+        return new StructuredFieldBareItem(StructuredFieldType.DATE, Long.valueOf(epochSeconds));
+    }
+
+    /**
+     * Creates a Unicode Display String bare Item.
+     *
+     * @param value the Unicode value.
+     * @return the bare Item.
+     * @throws IllegalArgumentException if the value contains an unpaired surrogate.
+     */
+    public static StructuredFieldBareItem ofDisplayString(final String value) {
+        StructuredFieldRules.validateDisplayString(value);
+        return new StructuredFieldBareItem(StructuredFieldType.DISPLAY_STRING, value);
+    }
+
+    private static void checkInteger(final long value) {
+        Args.check(value >= -StructuredFieldRules.MAX_INTEGER && value <= StructuredFieldRules.MAX_INTEGER,
+                "Structured Field Integer is outside the Structured Field range: %s", value);
+    }
+
+    /**
+     * @return the bare Item type.
+     */
+    public StructuredFieldType getType() {
+        return type;
+    }
+
+    /**
+     * Returns the value using its Java representation. Byte sequences are defensively copied.
+     *
+     * @return the value.
+     */
+    public Object getValue() {
+        return type == StructuredFieldType.BYTE_SEQUENCE ? ((byte[]) value).clone() : value;
+    }
+
+    /**
+     * @return the Integer or Date value.
+     * @throws IllegalStateException if this Item is not an Integer or Date.
+     */
+    public long getLongValue() {
+        requireType(StructuredFieldType.INTEGER, StructuredFieldType.DATE);
+        return ((Long) value).longValue();
+    }
+
+    /**
+     * @return the Decimal value.
+     * @throws IllegalStateException if this Item is not a Decimal.
+     */
+    public BigDecimal getDecimalValue() {
+        requireType(StructuredFieldType.DECIMAL);
+        return (BigDecimal) value;
+    }
+
+    /**
+     * @return the String, Token, or Display String value.
+     * @throws IllegalStateException if this Item is not one of those textual types.
+     */
+    public String getTextValue() {
+        requireType(StructuredFieldType.STRING, StructuredFieldType.TOKEN, StructuredFieldType.DISPLAY_STRING);
+        return (String) value;
+    }
+
+    /**
+     * @return a defensive copy of the Byte Sequence value.
+     * @throws IllegalStateException if this Item is not a Byte Sequence.
+     */
+    public byte[] getByteSequenceValue() {
+        requireType(StructuredFieldType.BYTE_SEQUENCE);
+        return ((byte[]) value).clone();
+    }
+
+    /**
+     * @return the Boolean value.
+     * @throws IllegalStateException if this Item is not a Boolean.
+     */
+    public boolean getBooleanValue() {
+        requireType(StructuredFieldType.BOOLEAN);
+        return ((Boolean) value).booleanValue();
+    }
+
+    private void requireType(final StructuredFieldType... expected) {
+        for (final StructuredFieldType candidate : expected) {
+            if (type == candidate) {
+                return;
+            }
+        }
+        throw new IllegalStateException("Bare Item is " + type);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldBareItem) {
+            final StructuredFieldBareItem that = (StructuredFieldBareItem) obj;
+            if (this.type != that.type) {
+                return false;
+            }
+            return this.type == StructuredFieldType.BYTE_SEQUENCE
+                    ? Arrays.equals((byte[]) this.value, (byte[]) that.value)
+                    : Objects.equals(this.value, that.value);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.type);
+        hash = LangUtils.hashCode(hash, this.type == StructuredFieldType.BYTE_SEQUENCE
+                ? Arrays.hashCode((byte[]) this.value) : this.value.hashCode());
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return StructuredFieldSerializer.serializeBareItem(this);
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java
new file mode 100644
index 000000000..239c13a5c
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldDictionary.java
@@ -0,0 +1,193 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable, ordered Structured Field Dictionary.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldDictionary
+        implements StructuredFieldValue, Iterable<Map.Entry<String, StructuredFieldMember>> {
+
+    private final List<Map.Entry<String, StructuredFieldMember>> entries;
+    private final Map<String, StructuredFieldMember> values;
+
+    private StructuredFieldDictionary(final Map<String, StructuredFieldMember> source) {
+        final List<Map.Entry<String, StructuredFieldMember>> copy = new ArrayList<>(source.size());
+        final Map<String, StructuredFieldMember> lookup = new LinkedHashMap<>();
+        for (final Map.Entry<String, StructuredFieldMember> entry : source.entrySet()) {
+            StructuredFieldRules.validateKey(entry.getKey());
+            final StructuredFieldMember value = Args.notNull(entry.getValue(), "Dictionary member");
+            copy.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), value));
+            lookup.put(entry.getKey(), value);
+        }
+        this.entries = Collections.unmodifiableList(copy);
+        this.values = Collections.unmodifiableMap(lookup);
+    }
+
+    /**
+     * @return a new ordered Dictionary builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    static StructuredFieldDictionary copyOf(final Map<String, StructuredFieldMember> source) {
+        return new StructuredFieldDictionary(source);
+    }
+
+    /**
+     * @return the number of members.
+     */
+    public int size() {
+        return entries.size();
+    }
+
+    /**
+     * @return whether the Dictionary has no members.
+     */
+    public boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+    /**
+     * Looks up a member by key.
+     *
+     * @param key the member key.
+     * @return the member, or {@code null} when absent.
+     */
+    public StructuredFieldMember get(final String key) {
+        return values.get(key);
+    }
+
+    /**
+     * @param index the zero-based ordered index.
+     * @return the member key at the index.
+     */
+    public String getName(final int index) {
+        return entries.get(index).getKey();
+    }
+
+    /**
+     * @param index the zero-based ordered index.
+     * @return the member value at the index.
+     */
+    public StructuredFieldMember getValue(final int index) {
+        return entries.get(index).getValue();
+    }
+
+    /**
+     * @return an unmodifiable, insertion-ordered map view.
+     */
+    public Map<String, StructuredFieldMember> asMap() {
+        return values;
+    }
+
+    @Override
+    public Iterator<Map.Entry<String, StructuredFieldMember>> iterator() {
+        return entries.iterator();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldDictionary) {
+            final StructuredFieldDictionary that = (StructuredFieldDictionary) obj;
+            return Objects.equals(this.entries, that.entries);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.entries);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        final String value = StructuredFieldSerializer.serializeDictionary(this);
+        return value != null ? value : "";
+    }
+
+    /** Builds immutable, insertion-ordered Dictionaries. */
+    public static final class Builder {
+
+        private final LinkedHashMap<String, StructuredFieldMember> values = new LinkedHashMap<>();
+
+        /**
+         * Adds or replaces a member. Replacing a key preserves its original position.
+         *
+         * @param key the member key.
+         * @param value the member value.
+         * @return this builder.
+         */
+        public Builder put(final String key, final StructuredFieldMember value) {
+            StructuredFieldRules.validateKey(key);
+            values.put(key, Args.notNull(value, "Dictionary member"));
+            return this;
+        }
+
+        /**
+         * Adds or replaces a bare Item member.
+         *
+         * @param key the member key.
+         * @param value the bare Item value.
+         * @return this builder.
+         */
+        public Builder put(final String key, final StructuredFieldBareItem value) {
+            return put(key, StructuredFieldItem.of(value));
+        }
+
+        /**
+         * @return an immutable Dictionary.
+         */
+        public StructuredFieldDictionary build() {
+            return StructuredFieldDictionary.copyOf(values);
+        }
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java
new file mode 100644
index 000000000..205a3eae9
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldHeaders.java
@@ -0,0 +1,170 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.MessageHeaders;
+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;
+
+/**
+ * Integration between Structured Field values and HttpComponents message headers.
+ *
+ * @since 5.5
+ */
+public final class StructuredFieldHeaders {
+
+    private StructuredFieldHeaders() {
+    }
+
+    /**
+     * Parses one header as an Structured Field Item.
+     *
+     * @param header the header.
+     * @return the parsed Item.
+     * @throws ParseException if the complete field value is invalid.
+     */
+    public static StructuredFieldItem parseItem(final Header header) throws ParseException {
+        return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseItem);
+    }
+
+    /**
+     * Parses the field named {@code name} as a single Structured Field Item. An Item is a single
+     * value and cannot span field lines, so more than one matching field line is rejected.
+     *
+     * @param headers the message headers.
+     * @param name the case-insensitive field name.
+     * @return the parsed Item.
+     * @throws ParseException if the field is absent, spans multiple field lines, or is invalid.
+     */
+    public static StructuredFieldItem parseItem(final MessageHeaders headers, final String name)
+            throws ParseException {
+        Args.notNull(headers, "Message headers");
+        Args.notBlank(name, "Header name");
+        final Iterator<Header> matching = headers.headerIterator(name);
+        if (!matching.hasNext()) {
+            throw new ParseException("Missing " + name + " field");
+        }
+        final Header header = matching.next();
+        if (matching.hasNext()) {
+            throw new ParseException(name + " Item must not span multiple field lines");
+        }
+        return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseItem);
+    }
+
+    /**
+     * Parses one header as an Structured Field List.
+     *
+     * @param header the header.
+     * @return the parsed List.
+     * @throws ParseException if the complete field value is invalid.
+     */
+    public static StructuredFieldList parseList(final Header header) throws ParseException {
+        return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseList);
+    }
+
+    /**
+     * Parses the matching field lines as a Structured Field List, reading each field line in place
+     * and appending its members, without combining the values into a new buffer.
+     *
+     * @param headers the message headers.
+     * @param name the case-insensitive field name.
+     * @return the parsed List, empty when the field is absent.
+     * @throws ParseException if any field line is invalid.
+     */
+    public static StructuredFieldList parseList(final MessageHeaders headers, final String name)
+            throws ParseException {
+        Args.notNull(headers, "Message headers");
+        Args.notBlank(name, "Header name");
+        final List<StructuredFieldMember> members = new ArrayList<>();
+        MessageSupport.parseElementListStrict(headers, name, (buffer, cursor) ->
+                members.add(StructuredFieldParser.parseListElement(buffer, cursor)));
+        return StructuredFieldList.of(members);
+    }
+
+    /**
+     * Parses one header as an Structured Field Dictionary.
+     *
+     * @param header the header.
+     * @return the parsed Dictionary.
+     * @throws ParseException if the complete field value is invalid.
+     */
+    public static StructuredFieldDictionary parseDictionary(final Header header) throws ParseException {
+        return MessageSupport.parseHeaderValueStrict(header, StructuredFieldParser::parseDictionary);
+    }
+
+    /**
+     * Parses the matching field lines as a Structured Field Dictionary, reading each field line in
+     * place and merging its members, without combining the values into a new buffer. A repeated key
+     * keeps its last value.
+     *
+     * @param headers the message headers.
+     * @param name the case-insensitive field name.
+     * @return the parsed Dictionary, empty when the field is absent.
+     * @throws ParseException if any field line is invalid.
+     */
+    public static StructuredFieldDictionary parseDictionary(final MessageHeaders headers, final String name)
+            throws ParseException {
+        Args.notNull(headers, "Message headers");
+        Args.notBlank(name, "Header name");
+        final Map<String, StructuredFieldMember> members = new LinkedHashMap<>();
+        MessageSupport.parseElementListStrict(headers, name, (buffer, cursor) ->
+                StructuredFieldParser.parseDictionaryElement(buffer, cursor, members));
+        return StructuredFieldDictionary.copyOf(members);
+    }
+
+    /**
+     * Creates a header for a Structured Field value.
+     *
+     * @param name the field name.
+     * @param value the Structured Field value.
+     * @return a header, or {@code null} for an empty List or Dictionary.
+     */
+    public static Header format(final String name, final StructuredFieldValue value) {
+        Args.notBlank(name, "Header name");
+        Args.notNull(value, "Structured Field value");
+        if (value instanceof StructuredFieldList && ((StructuredFieldList) value).isEmpty()
+                || value instanceof StructuredFieldDictionary && ((StructuredFieldDictionary) value).isEmpty()) {
+            return null;
+        }
+        final CharArrayBuffer buffer = new CharArrayBuffer(name.length() + 66);
+        buffer.append(name);
+        buffer.append(": ");
+        StructuredFieldSerializer.serialize(buffer, value);
+        return BufferedHeader.create(buffer);
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java
new file mode 100644
index 000000000..74a7794fc
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldInnerList.java
@@ -0,0 +1,159 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable Structured Field Inner List.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldInnerList implements StructuredFieldMember, Iterable<StructuredFieldItem> {
+
+    private final List<StructuredFieldItem> items;
+    private final StructuredFieldParameters parameters;
+
+    private StructuredFieldInnerList(
+            final List<StructuredFieldItem> items, final StructuredFieldParameters parameters) {
+        final ArrayList<StructuredFieldItem> copy = new ArrayList<>(items.size());
+        for (final StructuredFieldItem item : items) {
+            copy.add(Args.notNull(item, "Inner List Item"));
+        }
+        this.items = Collections.unmodifiableList(copy);
+        this.parameters = Args.notNull(parameters, "Parameters");
+    }
+
+    /**
+     * Creates an Inner List without parameters.
+     *
+     * @param items the ordered Items.
+     * @return the immutable Inner List.
+     */
+    public static StructuredFieldInnerList of(final List<StructuredFieldItem> items) {
+        return new StructuredFieldInnerList(items, StructuredFieldParameters.EMPTY);
+    }
+
+    /**
+     * Creates an Inner List without parameters.
+     *
+     * @param items the ordered Items.
+     * @return the immutable Inner List.
+     */
+    public static StructuredFieldInnerList of(final StructuredFieldItem... items) {
+        return of(Arrays.asList(items));
+    }
+
+    /**
+     * Creates an Inner List with parameters.
+     *
+     * @param items the ordered Items.
+     * @param parameters the Inner List parameters.
+     * @return the immutable Inner List.
+     */
+    public static StructuredFieldInnerList of(
+            final List<StructuredFieldItem> items, final StructuredFieldParameters parameters) {
+        return new StructuredFieldInnerList(items, parameters);
+    }
+
+    /**
+     * @return the number of Items.
+     */
+    public int size() {
+        return items.size();
+    }
+
+    /**
+     * @return whether the Inner List has no Items.
+     */
+    public boolean isEmpty() {
+        return items.isEmpty();
+    }
+
+    /**
+     * @param index the zero-based index.
+     * @return the Item at the index.
+     */
+    public StructuredFieldItem get(final int index) {
+        return items.get(index);
+    }
+
+    /**
+     * @return an unmodifiable ordered Item list.
+     */
+    public List<StructuredFieldItem> getItems() {
+        return items;
+    }
+
+    @Override
+    public StructuredFieldParameters getParameters() {
+        return parameters;
+    }
+
+    @Override
+    public Iterator<StructuredFieldItem> iterator() {
+        return items.iterator();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldInnerList) {
+            final StructuredFieldInnerList that = (StructuredFieldInnerList) obj;
+            return Objects.equals(this.items, that.items)
+                    && Objects.equals(this.parameters, that.parameters);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.items);
+        hash = LangUtils.hashCode(hash, this.parameters);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return StructuredFieldSerializer.serializeInnerList(this);
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java
new file mode 100644
index 000000000..862caa0ae
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldItem.java
@@ -0,0 +1,177 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable Structured Field Item.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldItem implements StructuredFieldValue, StructuredFieldMember {
+
+    private final StructuredFieldBareItem bareItem;
+    private final StructuredFieldParameters parameters;
+
+    private StructuredFieldItem(final StructuredFieldBareItem bareItem, final StructuredFieldParameters parameters) {
+        this.bareItem = Args.notNull(bareItem, "Bare Item");
+        this.parameters = Args.notNull(parameters, "Parameters");
+    }
+
+    /**
+     * Creates an Item without parameters.
+     *
+     * @param bareItem the bare Item value.
+     * @return the immutable Item.
+     */
+    public static StructuredFieldItem of(final StructuredFieldBareItem bareItem) {
+        return new StructuredFieldItem(bareItem, StructuredFieldParameters.EMPTY);
+    }
+
+    /**
+     * @param value the Integer value.
+     * @return an Integer Item without parameters.
+     */
+    public static StructuredFieldItem ofInteger(final long value) {
+        return of(StructuredFieldBareItem.ofInteger(value));
+    }
+
+    /**
+     * @param value the Decimal value.
+     * @return a Decimal Item without parameters.
+     */
+    public static StructuredFieldItem ofDecimal(final BigDecimal value) {
+        return of(StructuredFieldBareItem.ofDecimal(value));
+    }
+
+    /**
+     * @param value the printable ASCII String value.
+     * @return a String Item without parameters.
+     */
+    public static StructuredFieldItem ofString(final String value) {
+        return of(StructuredFieldBareItem.ofString(value));
+    }
+
+    /**
+     * @param value the Token value.
+     * @return a Token Item without parameters.
+     */
+    public static StructuredFieldItem ofToken(final String value) {
+        return of(StructuredFieldBareItem.ofToken(value));
+    }
+
+    /**
+     * @param value the Byte Sequence value.
+     * @return a Byte Sequence Item without parameters.
+     */
+    public static StructuredFieldItem ofByteSequence(final byte[] value) {
+        return of(StructuredFieldBareItem.ofByteSequence(value));
+    }
+
+    /**
+     * @param value the Boolean value.
+     * @return a Boolean Item without parameters.
+     */
+    public static StructuredFieldItem ofBoolean(final boolean value) {
+        return of(StructuredFieldBareItem.ofBoolean(value));
+    }
+
+    /**
+     * @param epochSeconds seconds since the Unix epoch.
+     * @return a Date Item without parameters.
+     */
+    public static StructuredFieldItem ofDate(final long epochSeconds) {
+        return of(StructuredFieldBareItem.ofDate(epochSeconds));
+    }
+
+    /**
+     * @param value the Unicode Display String value.
+     * @return a Display String Item without parameters.
+     */
+    public static StructuredFieldItem ofDisplayString(final String value) {
+        return of(StructuredFieldBareItem.ofDisplayString(value));
+    }
+
+    /**
+     * Creates an Item with parameters.
+     *
+     * @param bareItem the bare Item value.
+     * @param parameters the ordered parameters.
+     * @return the immutable Item.
+     */
+    public static StructuredFieldItem of(
+            final StructuredFieldBareItem bareItem, final StructuredFieldParameters parameters) {
+        return new StructuredFieldItem(bareItem, parameters);
+    }
+
+    /**
+     * @return the bare Item value.
+     */
+    public StructuredFieldBareItem getBareItem() {
+        return bareItem;
+    }
+
+    @Override
+    public StructuredFieldParameters getParameters() {
+        return parameters;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldItem) {
+            final StructuredFieldItem that = (StructuredFieldItem) obj;
+            return Objects.equals(this.bareItem, that.bareItem)
+                    && Objects.equals(this.parameters, that.parameters);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.bareItem);
+        hash = LangUtils.hashCode(hash, this.parameters);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return StructuredFieldSerializer.serializeItem(this);
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java
new file mode 100644
index 000000000..5ae96b25d
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldList.java
@@ -0,0 +1,138 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable Structured Field List.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldList implements StructuredFieldValue, Iterable<StructuredFieldMember> {
+
+    private final List<StructuredFieldMember> members;
+
+    private StructuredFieldList(final List<? extends StructuredFieldMember> members) {
+        final ArrayList<StructuredFieldMember> copy = new ArrayList<>(members.size());
+        for (final StructuredFieldMember member : members) {
+            copy.add(Args.notNull(member, "List member"));
+        }
+        this.members = Collections.unmodifiableList(copy);
+    }
+
+    /**
+     * Creates a List from ordered members.
+     *
+     * @param members the ordered members.
+     * @return the immutable List.
+     */
+    public static StructuredFieldList of(final List<? extends StructuredFieldMember> members) {
+        return new StructuredFieldList(members);
+    }
+
+    /**
+     * Creates a List from ordered members.
+     *
+     * @param members the ordered members.
+     * @return the immutable List.
+     */
+    public static StructuredFieldList of(final StructuredFieldMember... members) {
+        return of(Arrays.asList(members));
+    }
+
+    /**
+     * @return the number of members.
+     */
+    public int size() {
+        return members.size();
+    }
+
+    /**
+     * @return whether the List has no members.
+     */
+    public boolean isEmpty() {
+        return members.isEmpty();
+    }
+
+    /**
+     * @param index the zero-based index.
+     * @return the member at the index.
+     */
+    public StructuredFieldMember get(final int index) {
+        return members.get(index);
+    }
+
+    /**
+     * @return an unmodifiable ordered member list.
+     */
+    public List<StructuredFieldMember> getMembers() {
+        return members;
+    }
+
+    @Override
+    public Iterator<StructuredFieldMember> iterator() {
+        return members.iterator();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldList) {
+            final StructuredFieldList that = (StructuredFieldList) obj;
+            return Objects.equals(this.members, that.members);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.members);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        final String value = StructuredFieldSerializer.serializeList(this);
+        return value != null ? value : "";
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java
new file mode 100644
index 000000000..308d9e02e
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldMember.java
@@ -0,0 +1,42 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+/**
+ * A member of an Structured Field List or Dictionary: an Item or Inner List.
+ *
+ * @since 5.5
+ */
+public interface StructuredFieldMember {
+
+    /**
+     * @return the ordered parameters associated with this member.
+     */
+    StructuredFieldParameters getParameters();
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java
new file mode 100644
index 000000000..1b9781e9c
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParameters.java
@@ -0,0 +1,194 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+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;
+
+/**
+ * Immutable, ordered Structured Field Parameters map.
+ *
+ * @since 5.5
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class StructuredFieldParameters implements Iterable<Map.Entry<String, StructuredFieldBareItem>> {
+
+    /** Empty parameter map. */
+    public static final StructuredFieldParameters EMPTY = new StructuredFieldParameters(Collections.emptyMap());
+
+    private final List<Map.Entry<String, StructuredFieldBareItem>> entries;
+    private final Map<String, StructuredFieldBareItem> values;
+
+    private StructuredFieldParameters(final Map<String, StructuredFieldBareItem> source) {
+        final List<Map.Entry<String, StructuredFieldBareItem>> copy = new ArrayList<>(source.size());
+        final Map<String, StructuredFieldBareItem> lookup = new LinkedHashMap<>();
+        for (final Map.Entry<String, StructuredFieldBareItem> entry : source.entrySet()) {
+            StructuredFieldRules.validateKey(entry.getKey());
+            final StructuredFieldBareItem value = Args.notNull(entry.getValue(), "Parameter value");
+            copy.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), value));
+            lookup.put(entry.getKey(), value);
+        }
+        this.entries = Collections.unmodifiableList(copy);
+        this.values = Collections.unmodifiableMap(lookup);
+    }
+
+    /**
+     * @return a new ordered Parameters builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    static StructuredFieldParameters copyOf(final Map<String, StructuredFieldBareItem> source) {
+        return source.isEmpty() ? EMPTY : new StructuredFieldParameters(source);
+    }
+
+    /**
+     * @return the number of parameters.
+     */
+    public int size() {
+        return entries.size();
+    }
+
+    /**
+     * @return whether there are no parameters.
+     */
+    public boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+    /**
+     * Looks up a parameter by key.
+     *
+     * @param key the parameter key.
+     * @return the bare Item value, or {@code null} when absent.
+     */
+    public StructuredFieldBareItem get(final String key) {
+        return values.get(key);
+    }
+
+    /**
+     * @param index the zero-based ordered index.
+     * @return the parameter key at the index.
+     */
+    public String getName(final int index) {
+        return entries.get(index).getKey();
+    }
+
+    /**
+     * @param index the zero-based ordered index.
+     * @return the parameter value at the index.
+     */
+    public StructuredFieldBareItem getValue(final int index) {
+        return entries.get(index).getValue();
+    }
+
+    /**
+     * @return an unmodifiable, insertion-ordered map view.
+     */
+    public Map<String, StructuredFieldBareItem> asMap() {
+        return values;
+    }
+
+    @Override
+    public Iterator<Map.Entry<String, StructuredFieldBareItem>> iterator() {
+        return entries.iterator();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof StructuredFieldParameters) {
+            final StructuredFieldParameters that = (StructuredFieldParameters) obj;
+            return Objects.equals(this.entries, that.entries);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = LangUtils.HASH_SEED;
+        hash = LangUtils.hashCode(hash, this.entries);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return StructuredFieldSerializer.serializeParameters(this);
+    }
+
+    /** Builds immutable, insertion-ordered Parameters maps. */
+    public static final class Builder {
+
+        private final LinkedHashMap<String, StructuredFieldBareItem> values = new LinkedHashMap<>();
+
+        /**
+         * Adds or replaces a parameter. Replacing a key preserves its original position.
+         *
+         * @param key the parameter key.
+         * @param value the bare Item value.
+         * @return this builder.
+         */
+        public Builder put(final String key, final StructuredFieldBareItem value) {
+            StructuredFieldRules.validateKey(key);
+            values.put(key, Args.notNull(value, "Parameter value"));
+            return this;
+        }
+
+        /**
+         * Adds or replaces a Boolean parameter.
+         *
+         * @param key the parameter key.
+         * @param value the boolean value.
+         * @return this builder.
+         */
+        public Builder putBoolean(final String key, final boolean value) {
+            return put(key, StructuredFieldBareItem.ofBoolean(value));
+        }
+
+        /**
+         * @return an immutable Parameters map.
+         */
+        public StructuredFieldParameters build() {
+            return StructuredFieldParameters.copyOf(values);
+        }
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java
new file mode 100644
index 000000000..a35011cd1
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldParser.java
@@ -0,0 +1,639 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ByteArrayBuffer;
+import org.apache.hc.core5.util.CharArrayBuffer;
+import org.apache.hc.core5.util.Tokenizer;
+
+/**
+ * Strict parser for Structured Field Structured Field Values.
+ *
+ * @since 5.5
+ */
+final class StructuredFieldParser {
+
+    private static final Tokenizer TOKENIZER = Tokenizer.INSTANCE;
+    private static final Tokenizer.Delimiter KEY_TERMINATOR =
+            ch -> !StructuredFieldRules.isKeyChar(ch);
+    private static final Tokenizer.Delimiter TOKEN_TERMINATOR =
+            ch -> !StructuredFieldRules.isTokenChar(ch);
+
+    private final CharSequence input;
+    private final Tokenizer.Cursor cursor;
+
+    private StructuredFieldParser(final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+        this.input = Args.notNull(input, "Structured Field input");
+        this.cursor = Args.notNull(cursor, "Parser cursor");
+        if (cursor.getUpperBound() > input.length()) {
+            throw new IllegalArgumentException("Parser cursor exceeds Structured Field input length");
+        }
+    }
+
+    /**
+     * Parses a complete Structured Field Item field value.
+     *
+     * @param input the field value.
+     * @return the parsed Item.
+     * @throws ParseException if the complete value is invalid.
+     */
+    static StructuredFieldItem parseItem(final CharSequence input) throws ParseException {
+        Args.notNull(input, "Structured Field input");
+        return parseItem(input, new Tokenizer.Cursor(0, input.length()));
+    }
+
+    /**
+     * Parses an Item from the current cursor position through its upper bound.
+     *
+     * @param input the character sequence containing the field value.
+     * @param cursor the bounds and current parse position.
+     * @return the parsed Item.
+     * @throws ParseException if the complete bounded value is invalid.
+     */
+    static StructuredFieldItem parseItem(
+            final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+        final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+        parser.skipSpaces();
+        final StructuredFieldItem result = parser.parseItemValue();
+        parser.skipSpaces();
+        parser.requireEnd();
+        return result;
+    }
+
+    /**
+     * Parses a complete Structured Field List field value.
+     *
+     * @param input the field value.
+     * @return the parsed List.
+     * @throws ParseException if the complete value is invalid.
+     */
+    static StructuredFieldList parseList(final CharSequence input) throws ParseException {
+        Args.notNull(input, "Structured Field input");
+        return parseList(input, new Tokenizer.Cursor(0, input.length()));
+    }
+
+    /**
+     * Parses a List from the current cursor position through its upper bound.
+     *
+     * @param input the character sequence containing the field value.
+     * @param cursor the bounds and current parse position.
+     * @return the parsed List.
+     * @throws ParseException if the complete bounded value is invalid.
+     */
+    static StructuredFieldList parseList(
+            final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+        final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+        parser.skipSpaces();
+        final StructuredFieldList result = parser.parseListValue();
+        parser.skipSpaces();
+        parser.requireEnd();
+        return result;
+    }
+
+    /**
+     * Parses a complete Structured Field Dictionary field value.
+     *
+     * @param input the field value.
+     * @return the parsed Dictionary.
+     * @throws ParseException if the complete value is invalid.
+     */
+    static StructuredFieldDictionary parseDictionary(final CharSequence input) throws ParseException {
+        Args.notNull(input, "Structured Field input");
+        return parseDictionary(input, new Tokenizer.Cursor(0, input.length()));
+    }
+
+    /**
+     * Parses a Dictionary from the current cursor position through its upper bound.
+     *
+     * @param input the character sequence containing the field value.
+     * @param cursor the bounds and current parse position.
+     * @return the parsed Dictionary.
+     * @throws ParseException if the complete bounded value is invalid.
+     */
+    static StructuredFieldDictionary parseDictionary(
+            final CharSequence input, final Tokenizer.Cursor cursor) throws ParseException {
+        final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+        parser.skipSpaces();
+        final StructuredFieldDictionary result = parser.parseDictionaryValue();
+        parser.skipSpaces();
+        parser.requireEnd();
+        return result;
+    }
+
+    /**
+     * Parses a single List member at the current cursor position, consuming the surrounding optional
+     * whitespace so that the cursor stops at the following comma or the upper bound. One field line
+     * element is parsed in place, without copying the underlying data.
+     *
+     * @param input the character sequence containing the field line value.
+     * @param cursor the bounds and current parse position.
+     * @return the parsed member.
+     * @throws ParseException if the member is invalid or is not comma-separated.
+     */
+    static StructuredFieldMember parseListElement(final CharSequence input, final Tokenizer.Cursor cursor)
+            throws ParseException {
+        final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+        parser.skipOptionalWhitespace();
+        final StructuredFieldMember member = parser.parseMember();
+        parser.requireElementSeparator();
+        return member;
+    }
+
+    /**
+     * Parses a single Dictionary entry at the current cursor position into {@code accumulator},
+     * consuming the surrounding optional whitespace so that the cursor stops at the following comma or
+     * the upper bound; a repeated key keeps its last value. One field line element is parsed in place,
+     * without copying the underlying data.
+     *
+     * @param input the character sequence containing the field line value.
+     * @param cursor the bounds and current parse position.
+     * @param accumulator the map into which the parsed entry is merged.
+     * @throws ParseException if the entry is invalid or is not comma-separated.
+     */
+    static void parseDictionaryElement(final CharSequence input, final Tokenizer.Cursor cursor,
+                                       final Map<String, StructuredFieldMember> accumulator) throws ParseException {
+        final StructuredFieldParser parser = new StructuredFieldParser(input, cursor);
+        parser.skipOptionalWhitespace();
+        final String key = parser.parseKey();
+        final StructuredFieldMember member;
+        if (parser.peek('=')) {
+            parser.advance();
+            member = parser.parseMember();
+        } else {
+            member = StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parser.parseParameters());
+        }
+        accumulator.put(key, member);
+        parser.requireElementSeparator();
+    }
+
+    /**
+     * Combines field lines with a comma and parses an Structured Field Item.
+     *
+     * @param fieldValues the field line values.
+     * @return the parsed Item.
+     * @throws ParseException if the combined field value is invalid.
+     */
+    static StructuredFieldItem parseItemLines(final Iterable<? extends CharSequence> fieldValues)
+            throws ParseException {
+        return parseItem(combine(fieldValues));
+    }
+
+    /**
+     * Combines field lines with a comma and parses an Structured Field List.
+     *
+     * @param fieldValues the field line values.
+     * @return the parsed List.
+     * @throws ParseException if the combined field value is invalid.
+     */
+    static StructuredFieldList parseListLines(final Iterable<? extends CharSequence> fieldValues)
+            throws ParseException {
+        return parseList(combine(fieldValues));
+    }
+
+    /**
+     * Combines field lines with a comma and parses an Structured Field Dictionary.
+     *
+     * @param fieldValues the field line values.
+     * @return the parsed Dictionary.
+     * @throws ParseException if the combined field value is invalid.
+     */
+    static StructuredFieldDictionary parseDictionaryLines(
+            final Iterable<? extends CharSequence> fieldValues) throws ParseException {
+        return parseDictionary(combine(fieldValues));
+    }
+
+    private static String combine(final Iterable<? extends CharSequence> fieldValues) {
+        Args.notNull(fieldValues, "Field values");
+        final CharArrayBuffer buffer = new CharArrayBuffer(64);
+        boolean first = true;
+        for (final CharSequence fieldValue : fieldValues) {
+            if (!first) {
+                buffer.append(", ");
+            }
+            buffer.append(Args.notNull(fieldValue, "Field value").toString());
+            first = false;
+        }
+        return buffer.toString();
+    }
+
+    private StructuredFieldList parseListValue() throws ParseException {
+        final List<StructuredFieldMember> members = new ArrayList<>();
+        while (!atEnd()) {
+            members.add(parseMember());
+            skipOptionalWhitespace();
+            if (atEnd()) {
+                break;
+            }
+            require(',');
+            skipOptionalWhitespace();
+            if (atEnd()) {
+                throw error("Trailing comma in Structured Field List");
+            }
+        }
+        return StructuredFieldList.of(members);
+    }
+
+    private StructuredFieldDictionary parseDictionaryValue() throws ParseException {
+        final LinkedHashMap<String, StructuredFieldMember> members = new LinkedHashMap<>();
+        while (!atEnd()) {
+            final String key = parseKey();
+            final StructuredFieldMember member;
+            if (peek('=')) {
+                advance();
+                member = parseMember();
+            } else {
+                member = StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parseParameters());
+            }
+            members.put(key, member);
+            skipOptionalWhitespace();
+            if (atEnd()) {
+                break;
+            }
+            require(',');
+            skipOptionalWhitespace();
+            if (atEnd()) {
+                throw error("Trailing comma in Structured Field Dictionary");
+            }
+        }
+        return StructuredFieldDictionary.copyOf(members);
+    }
+
+    private void requireElementSeparator() throws ParseException {
+        skipOptionalWhitespace();
+        if (!atEnd() && !peek(',')) {
+            throw error("Structured Field members must be separated by a comma");
+        }
+    }
+
+    private StructuredFieldMember parseMember() throws ParseException {
+        return peek('(') ? parseInnerList() : parseItemValue();
+    }
+
+    private StructuredFieldInnerList parseInnerList() throws ParseException {
+        require('(');
+        final List<StructuredFieldItem> items = new ArrayList<>();
+        while (!atEnd()) {
+            skipSpaces();
+            if (peek(')')) {
+                advance();
+                return StructuredFieldInnerList.of(items, parseParameters());
+            }
+            items.add(parseItemValue());
+            if (atEnd() || current() != Tokenizer.SP && current() != ')') {
+                throw error("Inner List Items must be separated by SP");
+            }
+        }
+        throw error("Unterminated Structured Field Inner List");
+    }
+
+    private StructuredFieldItem parseItemValue() throws ParseException {
+        return StructuredFieldItem.of(parseBareItem(), parseParameters());
+    }
+
+    private StructuredFieldParameters parseParameters() throws ParseException {
+        final Map<String, StructuredFieldBareItem> parameters = new LinkedHashMap<>();
+        while (peek(';')) {
+            advance();
+            skipSpaces();
+            final String key = parseKey();
+            final StructuredFieldBareItem value;
+            if (peek('=')) {
+                advance();
+                value = parseBareItem();
+            } else {
+                value = StructuredFieldBareItem.ofBoolean(true);
+            }
+            parameters.put(key, value);
+        }
+        return StructuredFieldParameters.copyOf(parameters);
+    }
+
+    private String parseKey() throws ParseException {
+        if (atEnd() || !StructuredFieldRules.isKeyStart(current())) {
+            throw error("Invalid Structured Field key");
+        }
+        return TOKENIZER.parseContent(input, cursor, KEY_TERMINATOR);
+    }
+
+    private StructuredFieldBareItem parseBareItem() throws ParseException {
+        if (atEnd()) {
+            throw error("Missing Structured Field bare Item");
+        }
+        final char ch = current();
+        if (ch == '-' || StructuredFieldRules.isDigit(ch)) {
+            return parseNumber();
+        }
+        if (ch == Tokenizer.DQUOTE) {
+            return StructuredFieldBareItem.ofString(parseString());
+        }
+        if (StructuredFieldRules.isAlpha(ch) || ch == '*') {
+            return StructuredFieldBareItem.ofToken(parseToken());
+        }
+        if (ch == ':') {
+            return StructuredFieldBareItem.ofByteSequence(parseByteSequence());
+        }
+        if (ch == '?') {
+            return StructuredFieldBareItem.ofBoolean(parseBoolean());
+        }
+        if (ch == '@') {
+            advance();
+            final StructuredFieldBareItem value = parseNumber();
+            if (value.getType() != StructuredFieldType.INTEGER) {
+                throw error("Structured Field Date must be an Integer");
+            }
+            return StructuredFieldBareItem.ofDate(value.getLongValue());
+        }
+        if (ch == '%') {
+            return StructuredFieldBareItem.ofDisplayString(parseDisplayString());
+        }
+        throw error("Unrecognized Structured Field bare Item");
+    }
+
+    private StructuredFieldBareItem parseNumber() throws ParseException {
+        boolean negative = false;
+        if (peek('-')) {
+            negative = true;
+            advance();
+        }
+        if (atEnd() || !StructuredFieldRules.isDigit(current())) {
+            throw error("Invalid Structured Field number");
+        }
+        final int digitsStart = position();
+        int integerDigits = 0;
+        while (!atEnd() && StructuredFieldRules.isDigit(current())) {
+            advance();
+            if (++integerDigits > 15) {
+                throw error("Structured Field Integer has more than 15 digits");
+            }
+        }
+        if (!peek('.')) {
+            final String number = subsequence(digitsStart, position());
+            final long value;
+            try {
+                value = Long.parseLong(number);
+            } catch (final NumberFormatException ex) {
+                throw error("Invalid Structured Field Integer");
+            }
+            return StructuredFieldBareItem.ofInteger(negative ? -value : value);
+        }
+        if (integerDigits > 12) {
+            throw error("Structured Field Decimal has more than 12 integer digits");
+        }
+        advance();
+        final int fractionStart = position();
+        while (!atEnd() && StructuredFieldRules.isDigit(current())) {
+            advance();
+            if (position() - fractionStart > 3) {
+                throw error("Structured Field Decimal has more than 3 fractional digits");
+            }
+        }
+        if (position() == fractionStart) {
+            throw error("Structured Field Decimal has no fractional digits");
+        }
+        final int numberStart = negative ? digitsStart - 1 : digitsStart;
+        return StructuredFieldBareItem.ofDecimal(new BigDecimal(subsequence(numberStart, position())));
+    }
+
+    private String parseString() throws ParseException {
+        require(Tokenizer.DQUOTE);
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        while (!atEnd()) {
+            final char ch = current();
+            advance();
+            if (ch == Tokenizer.ESCAPE) {
+                if (atEnd()) {
+                    throw error("Incomplete escape in Structured Field String");
+                }
+                final char escaped = current();
+                advance();
+                if (escaped != Tokenizer.DQUOTE && escaped != Tokenizer.ESCAPE) {
+                    throw error("Invalid escape in Structured Field String");
+                }
+                buffer.append(escaped);
+            } else if (ch == Tokenizer.DQUOTE) {
+                return buffer.toString();
+            } else if (ch < 0x20 || ch > 0x7e) {
+                throw error("Invalid character in Structured Field String");
+            } else {
+                buffer.append(ch);
+            }
+        }
+        throw error("Unterminated Structured Field String");
+    }
+
+    private String parseToken() {
+        return TOKENIZER.parseContent(input, cursor, TOKEN_TERMINATOR);
+    }
+
+    private byte[] parseByteSequence() throws ParseException {
+        require(':');
+        final int start = position();
+        while (!atEnd() && current() != ':') {
+            final char ch = current();
+            if (!StructuredFieldRules.isAlpha(ch) && !StructuredFieldRules.isDigit(ch)
+                    && ch != '+' && ch != '/' && ch != '=') {
+                throw error("Invalid character in Structured Field Byte Sequence");
+            }
+            advance();
+        }
+        if (atEnd()) {
+            throw error("Unterminated Structured Field Byte Sequence");
+        }
+        final String encoded = subsequence(start, position());
+        advance();
+        final String padded = padBase64(encoded);
+        try {
+            return Base64.getDecoder().decode(padded);
+        } catch (final IllegalArgumentException ex) {
+            throw error("Invalid base64 in Structured Field Byte Sequence");
+        }
+    }
+
+    private String padBase64(final String encoded) throws ParseException {
+        final int paddingStart = encoded.indexOf('=');
+        final int contentLength = paddingStart >= 0 ? paddingStart : encoded.length();
+        for (int i = contentLength; i < encoded.length(); i++) {
+            if (encoded.charAt(i) != '=') {
+                throw error("Invalid padding in Structured Field Byte Sequence");
+            }
+        }
+        final int remainder = contentLength % 4;
+        if (remainder == 1) {
+            throw error("Invalid Structured Field Byte Sequence length");
+        }
+        final int requiredPadding = remainder == 0 ? 0 : 4 - remainder;
+        final int suppliedPadding = encoded.length() - contentLength;
+        if (suppliedPadding > requiredPadding) {
+            throw error("Invalid padding in Structured Field Byte Sequence");
+        }
+        if (suppliedPadding == requiredPadding) {
+            return encoded;
+        }
+        final StringBuilder buffer = new StringBuilder(encoded);
+        for (int i = suppliedPadding; i < requiredPadding; i++) {
+            buffer.append('=');
+        }
+        return buffer.toString();
+    }
+
+    private boolean parseBoolean() throws ParseException {
+        require('?');
+        if (atEnd()) {
+            throw error("Missing Structured Field Boolean value");
+        }
+        final char ch = current();
+        advance();
+        if (ch == '1') {
+            return true;
+        }
+        if (ch == '0') {
+            return false;
+        }
+        throw error("Invalid Structured Field Boolean");
+    }
+
+    private String parseDisplayString() throws ParseException {
+        require('%');
+        require(Tokenizer.DQUOTE);
+        final ByteArrayBuffer bytes = new ByteArrayBuffer(64);
+        while (!atEnd()) {
+            final char ch = current();
+            advance();
+            if (ch < 0x20 || ch > 0x7e) {
+                throw error("Invalid character in Structured Field Display String");
+            }
+            if (ch == Tokenizer.DQUOTE) {
+                try {
+                    return StandardCharsets.UTF_8.newDecoder()
+                            .onMalformedInput(CodingErrorAction.REPORT)
+                            .onUnmappableCharacter(CodingErrorAction.REPORT)
+                            .decode(ByteBuffer.wrap(bytes.array(), 0, bytes.length())).toString();
+                } catch (final CharacterCodingException ex) {
+                    throw error("Invalid UTF-8 in Structured Field Display String");
+                }
+            }
+            if (ch == '%') {
+                if (position() + 2 > cursor.getUpperBound()) {
+                    throw error("Incomplete percent escape in Structured Field Display String");
+                }
+                final int high = lowerHex(current());
+                advance();
+                final int low = lowerHex(current());
+                advance();
+                if (high < 0 || low < 0) {
+                    throw error("Display String percent escapes must use lowercase hexadecimal");
+                }
+                bytes.append(high << 4 | low);
+            } else {
+                bytes.append(ch);
+            }
+        }
+        throw error("Unterminated Structured Field Display String");
+    }
+
+    private static int lowerHex(final char ch) {
+        if (ch <= 0x7f && Character.isDigit(ch)) {
+            return ch - '0';
+        }
+        if (ch >= 'a' && ch <= 'f') {
+            return ch - 'a' + 10;
+        }
+        return -1;
+    }
+
+    private void skipSpaces() {
+        while (peek((char) Tokenizer.SP)) {
+            advance();
+        }
+    }
+
+    private void skipOptionalWhitespace() {
+        while (!atEnd() && (current() == Tokenizer.SP || current() == Tokenizer.HT)) {
+            advance();
+        }
+    }
+
+    private boolean peek(final char ch) {
+        return !atEnd() && current() == ch;
+    }
+
+    private void require(final char ch) throws ParseException {
+        if (!peek(ch)) {
+            throw error("Expected '" + ch + "'");
+        }
+        advance();
+    }
+
+    private void requireEnd() throws ParseException {
+        if (!atEnd()) {
+            throw error("Trailing data after Structured Field value");
+        }
+    }
+
+    private char current() {
+        return input.charAt(position());
+    }
+
+    private int position() {
+        return cursor.getPos();
+    }
+
+    private void advance() {
+        cursor.updatePos(position() + 1);
+    }
+
+    private String subsequence(final int start, final int end) {
+        return input.subSequence(start, end).toString();
+    }
+
+    private boolean atEnd() {
+        return cursor.atEnd();
+    }
+
+    private ParseException error(final String description) {
+        return new ParseException(
+                description,
+                input,
+                cursor.getLowerBound(),
+                cursor.getUpperBound() - cursor.getLowerBound(),
+                position());
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java
new file mode 100644
index 000000000..cd259dec7
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldRules.java
@@ -0,0 +1,110 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+final class StructuredFieldRules {
+
+    static final long MAX_INTEGER = 999999999999999L;
+
+    private StructuredFieldRules() {
+    }
+
+    static boolean isAlpha(final char ch) {
+        return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z';
+    }
+
+    static boolean isLowerAlpha(final char ch) {
+        return ch >= 'a' && ch <= 'z';
+    }
+
+    static boolean isDigit(final char ch) {
+        return ch <= 0x7f && Character.isDigit(ch);
+    }
+
+    static boolean isKeyStart(final char ch) {
+        return isLowerAlpha(ch) || ch == '*';
+    }
+
+    static boolean isKeyChar(final char ch) {
+        return isLowerAlpha(ch) || isDigit(ch) || ch == '_' || ch == '-' || ch == '.' || ch == '*';
+    }
+
+    static boolean isTokenChar(final char ch) {
+        return isAlpha(ch) || isDigit(ch) || "!#$%&'*+-.^_`|~:/".indexOf(ch) >= 0;
+    }
+
+    static void validateKey(final String key) {
+        if (key == null || key.isEmpty() || !isKeyStart(key.charAt(0))) {
+            throw new IllegalArgumentException("Invalid Structured Field key: " + key);
+        }
+        for (int i = 1; i < key.length(); i++) {
+            if (!isKeyChar(key.charAt(i))) {
+                throw new IllegalArgumentException("Invalid Structured Field key: " + key);
+            }
+        }
+    }
+
+    static void validateToken(final String token) {
+        if (token == null || token.isEmpty() || !(isAlpha(token.charAt(0)) || token.charAt(0) == '*')) {
+            throw new IllegalArgumentException("Invalid Structured Field token: " + token);
+        }
+        for (int i = 1; i < token.length(); i++) {
+            if (!isTokenChar(token.charAt(i))) {
+                throw new IllegalArgumentException("Invalid Structured Field token: " + token);
+            }
+        }
+    }
+
+    static void validateString(final String value) {
+        if (value == null) {
+            throw new NullPointerException("String value");
+        }
+        for (int i = 0; i < value.length(); i++) {
+            final char ch = value.charAt(i);
+            if (ch < 0x20 || ch > 0x7e) {
+                throw new IllegalArgumentException("Structured Field String must contain printable ASCII only");
+            }
+        }
+    }
+
+    static void validateDisplayString(final String value) {
+        if (value == null) {
+            throw new NullPointerException("Display String value");
+        }
+        for (int i = 0; i < value.length(); i++) {
+            final char ch = value.charAt(i);
+            if (Character.isHighSurrogate(ch)) {
+                if (++i >= value.length() || !Character.isLowSurrogate(value.charAt(i))) {
+                    throw new IllegalArgumentException("Display String contains an unpaired surrogate");
+                }
+            } else if (Character.isLowSurrogate(ch)) {
+                throw new IllegalArgumentException("Display String contains an unpaired surrogate");
+            }
+        }
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java
new file mode 100644
index 000000000..b988fe465
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldSerializer.java
@@ -0,0 +1,348 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.util.Base64;
+import java.util.BitSet;
+import java.util.Map;
+
+import org.apache.hc.core5.net.PercentCodec;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
+
+/**
+ * Canonical serializer for Structured Field Structured Field Values.
+ *
+ * @since 5.5
+ */
+public final class StructuredFieldSerializer {
+
+    private static final PercentCodec DISPLAY_STRING_CODEC;
+
+    static {
+        final BitSet safeChars = new BitSet(256);
+        safeChars.set(0x20, 0x7f);
+        safeChars.clear('"');
+        safeChars.clear('%');
+        DISPLAY_STRING_CODEC = new PercentCodec(safeChars, true);
+    }
+
+    private StructuredFieldSerializer() {
+    }
+
+    /**
+     * Serializes a top-level field value.
+     *
+     * @param value the Structured Field value.
+     * @return the field value, or {@code null} for an empty List or Dictionary,
+     *         which Structured Field represents by omitting the field.
+     */
+    public static String serialize(final StructuredFieldValue value) {
+        Args.notNull(value, "Structured Field value");
+        if (isEmpty(value)) {
+            return null;
+        }
+        final CharArrayBuffer buffer = new CharArrayBuffer(64);
+        serialize(buffer, value);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends the canonical serialization of a top-level Structured Field value.
+     * Empty Lists and Dictionaries append no characters because Structured Field
+     * represents them by omitting the field.
+     *
+     * @param buffer the destination buffer.
+     * @param value the Structured Field value.
+     */
+    public static void serialize(final CharArrayBuffer buffer, final StructuredFieldValue value) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(value, "Structured Field value");
+        if (value instanceof StructuredFieldItem) {
+            serializeItem(buffer, (StructuredFieldItem) value);
+        } else if (value instanceof StructuredFieldList) {
+            serializeList(buffer, (StructuredFieldList) value);
+        } else if (value instanceof StructuredFieldDictionary) {
+            serializeDictionary(buffer, (StructuredFieldDictionary) value);
+        } else {
+            throw new IllegalArgumentException("Unsupported Structured Field value: " + value.getClass());
+        }
+    }
+
+    /**
+     * @param item the Item.
+     * @return the canonical serialized Item.
+     */
+    public static String serializeItem(final StructuredFieldItem item) {
+        Args.notNull(item, "Item");
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        serializeItem(buffer, item);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a canonical serialized Item.
+     *
+     * @param buffer the destination buffer.
+     * @param item the Item.
+     */
+    public static void serializeItem(final CharArrayBuffer buffer, final StructuredFieldItem item) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(item, "Item");
+        serializeBareItem(buffer, item.getBareItem());
+        serializeParameters(buffer, item.getParameters());
+    }
+
+    /**
+     * @param list the List.
+     * @return the canonical serialized List, or {@code null} when empty.
+     */
+    public static String serializeList(final StructuredFieldList list) {
+        Args.notNull(list, "List");
+        if (list.isEmpty()) {
+            return null;
+        }
+        final CharArrayBuffer buffer = new CharArrayBuffer(64);
+        serializeList(buffer, list);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a canonical serialized List. An empty List appends no characters.
+     *
+     * @param buffer the destination buffer.
+     * @param list the List.
+     */
+    public static void serializeList(final CharArrayBuffer buffer, final StructuredFieldList list) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(list, "List");
+        for (int i = 0; i < list.size(); i++) {
+            if (i > 0) {
+                buffer.append(", ");
+            }
+            serializeMember(buffer, list.get(i));
+        }
+    }
+
+    /**
+     * @param dictionary the Dictionary.
+     * @return the canonical serialized Dictionary, or {@code null} when empty.
+     */
+    public static String serializeDictionary(final StructuredFieldDictionary dictionary) {
+        Args.notNull(dictionary, "Dictionary");
+        if (dictionary.isEmpty()) {
+            return null;
+        }
+        final CharArrayBuffer buffer = new CharArrayBuffer(64);
+        serializeDictionary(buffer, dictionary);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a canonical serialized Dictionary. An empty Dictionary appends no characters.
+     *
+     * @param buffer the destination buffer.
+     * @param dictionary the Dictionary.
+     */
+    public static void serializeDictionary(
+            final CharArrayBuffer buffer, final StructuredFieldDictionary dictionary) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(dictionary, "Dictionary");
+        int index = 0;
+        for (final Map.Entry<String, StructuredFieldMember> entry : dictionary) {
+            if (index++ > 0) {
+                buffer.append(", ");
+            }
+            buffer.append(entry.getKey());
+            final StructuredFieldMember member = entry.getValue();
+            if (member instanceof StructuredFieldItem
+                    && isTrue(((StructuredFieldItem) member).getBareItem())) {
+                serializeParameters(buffer, member.getParameters());
+            } else {
+                buffer.append('=');
+                serializeMember(buffer, member);
+            }
+        }
+    }
+
+    /**
+     * @param innerList the Inner List.
+     * @return the canonical serialized Inner List.
+     */
+    public static String serializeInnerList(final StructuredFieldInnerList innerList) {
+        Args.notNull(innerList, "Inner List");
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        serializeInnerList(buffer, innerList);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a canonical serialized Inner List.
+     *
+     * @param buffer the destination buffer.
+     * @param innerList the Inner List.
+     */
+    public static void serializeInnerList(
+            final CharArrayBuffer buffer, final StructuredFieldInnerList innerList) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(innerList, "Inner List");
+        buffer.append('(');
+        for (int i = 0; i < innerList.size(); i++) {
+            if (i > 0) {
+                buffer.append(' ');
+            }
+            serializeItem(buffer, innerList.get(i));
+        }
+        buffer.append(')');
+        serializeParameters(buffer, innerList.getParameters());
+    }
+
+    /**
+     * @param parameters the Parameters map.
+     * @return the canonical serialized parameters.
+     */
+    public static String serializeParameters(final StructuredFieldParameters parameters) {
+        Args.notNull(parameters, "Parameters");
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        serializeParameters(buffer, parameters);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends canonical serialized parameters.
+     *
+     * @param buffer the destination buffer.
+     * @param parameters the Parameters map.
+     */
+    public static void serializeParameters(
+            final CharArrayBuffer buffer, final StructuredFieldParameters parameters) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(parameters, "Parameters");
+        for (final Map.Entry<String, StructuredFieldBareItem> entry : parameters) {
+            buffer.append(';');
+            buffer.append(entry.getKey());
+            if (!isTrue(entry.getValue())) {
+                buffer.append('=');
+                serializeBareItem(buffer, entry.getValue());
+            }
+        }
+    }
+
+    /**
+     * @param item the bare Item.
+     * @return the canonical serialized bare Item.
+     */
+    public static String serializeBareItem(final StructuredFieldBareItem item) {
+        Args.notNull(item, "Bare Item");
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        serializeBareItem(buffer, item);
+        return buffer.toString();
+    }
+
+    /**
+     * Appends a canonical serialized bare Item.
+     *
+     * @param buffer the destination buffer.
+     * @param item the bare Item.
+     */
+    public static void serializeBareItem(final CharArrayBuffer buffer, final StructuredFieldBareItem item) {
+        Args.notNull(buffer, "Destination");
+        Args.notNull(item, "Bare Item");
+        switch (item.getType()) {
+        case INTEGER:
+            buffer.append(Long.toString(item.getLongValue()));
+            break;
+        case DECIMAL:
+            buffer.append(item.getDecimalValue().toPlainString());
+            break;
+        case STRING:
+            serializeString(buffer, item.getTextValue());
+            break;
+        case TOKEN:
+            buffer.append(item.getTextValue());
+            break;
+        case BYTE_SEQUENCE:
+            buffer.append(':');
+            buffer.append(Base64.getEncoder().encodeToString(item.getByteSequenceValue()));
+            buffer.append(':');
+            break;
+        case BOOLEAN:
+            buffer.append(item.getBooleanValue() ? "?1" : "?0");
+            break;
+        case DATE:
+            buffer.append('@');
+            buffer.append(Long.toString(item.getLongValue()));
+            break;
+        case DISPLAY_STRING:
+            serializeDisplayString(buffer, item.getTextValue());
+            break;
+        default:
+            throw new IllegalStateException("Unsupported bare Item type: " + item.getType());
+        }
+    }
+
+    private static void serializeMember(final CharArrayBuffer buffer, final StructuredFieldMember member) {
+        if (member instanceof StructuredFieldItem) {
+            serializeItem(buffer, (StructuredFieldItem) member);
+        } else if (member instanceof StructuredFieldInnerList) {
+            serializeInnerList(buffer, (StructuredFieldInnerList) member);
+        } else {
+            throw new IllegalArgumentException("Unsupported Structured Field member: " + member.getClass());
+        }
+    }
+
+    private static boolean isTrue(final StructuredFieldBareItem value) {
+        return value.getType() == StructuredFieldType.BOOLEAN && value.getBooleanValue();
+    }
+
+    private static void serializeString(final CharArrayBuffer buffer, final String value) {
+        buffer.append('"');
+        for (int i = 0; i < value.length(); i++) {
+            final char ch = value.charAt(i);
+            if (ch == '"' || ch == '\\') {
+                buffer.append('\\');
+            }
+            buffer.append(ch);
+        }
+        buffer.append('"');
+    }
+
+    private static void serializeDisplayString(final CharArrayBuffer buffer, final String value) {
+        buffer.append('%');
+        buffer.append('"');
+        final StringBuilder encoded = new StringBuilder(value.length());
+        DISPLAY_STRING_CODEC.encode(encoded, value);
+        buffer.append(encoded);
+        buffer.append('"');
+    }
+
+    private static boolean isEmpty(final StructuredFieldValue value) {
+        return value instanceof StructuredFieldList && ((StructuredFieldList) value).isEmpty()
+                || value instanceof StructuredFieldDictionary && ((StructuredFieldDictionary) value).isEmpty();
+    }
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java
new file mode 100644
index 000000000..deab912f5
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldType.java
@@ -0,0 +1,52 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+/**
+ * Structured Field bare Item types.
+ *
+ * @since 5.5
+ */
+public enum StructuredFieldType {
+    /** Integer. */
+    INTEGER,
+    /** Decimal. */
+    DECIMAL,
+    /** Printable ASCII String. */
+    STRING,
+    /** Token. */
+    TOKEN,
+    /** Byte Sequence. */
+    BYTE_SEQUENCE,
+    /** Boolean. */
+    BOOLEAN,
+    /** Date expressed as seconds since the Unix epoch. */
+    DATE,
+    /** Unicode Display String. */
+    DISPLAY_STRING
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java
new file mode 100644
index 000000000..406f5128d
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/StructuredFieldValue.java
@@ -0,0 +1,36 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+/**
+ * Marker interface for the three Structured Field top-level field value types.
+ *
+ * @since 5.5
+ */
+public interface StructuredFieldValue {
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java
new file mode 100644
index 000000000..b537bef46
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/structured/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * ====================================================================
+ * 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/>.
+ *
+ */
+
+/**
+ * Immutable data model, strict parser, canonical serializer, and message-header
+ * integration for Structured Field Structured Field Values for HTTP.
+ */
+package org.apache.hc.core5.http.structured;
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
index 2a7f30b38..e81c667f4 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java
@@ -175,6 +175,11 @@ public class PercentCodec {
 
     static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
                        final BitSet safechars, final boolean blankAsPlus) {
+        encode(buf, content, charset, safechars, blankAsPlus, false);
+    }
+
+    private static void encode(final StringBuilder buf, final CharSequence content, final Charset charset,
+                               final BitSet safechars, final boolean blankAsPlus, final boolean lowerCaseHex) {
         if (content == null) {
             return;
         }
@@ -188,10 +193,10 @@ static void encode(final StringBuilder buf, final CharSequence content, final Ch
                 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);
+                final char hex1 = Character.forDigit((b >> 4) & 0xF, RADIX);
+                final char hex2 = Character.forDigit(b & 0xF, RADIX);
+                buf.append(lowerCaseHex ? hex1 : Character.toUpperCase(hex1));
+                buf.append(lowerCaseHex ? hex2 : Character.toUpperCase(hex2));
             }
         }
     }
@@ -256,21 +261,31 @@ public static String decode(final CharSequence content, final Charset charset) {
     public static final PercentCodec HTTP_TOKEN = new PercentCodec(HTTP_TOKEN_UNRESERVED);
 
     private final BitSet unreserved;
+    private final boolean lowerCaseHex;
 
+    /**
+     * @since 5.5
+     */
     @Internal
-    public PercentCodec(final BitSet unreserved) {
+    public PercentCodec(final BitSet unreserved, final boolean lowerCaseHex) {
         this.unreserved = unreserved;
+        this.lowerCaseHex = lowerCaseHex;
+    }
+
+    @Internal
+    public PercentCodec(final BitSet unreserved) {
+        this(unreserved, false);
     }
 
     public PercentCodec() {
-        this.unreserved = UNRESERVED;
+        this(UNRESERVED, false);
     }
 
     /**
      * @since 5.3
      */
     public void encode(final StringBuilder buf, final CharSequence content) {
-        encode(buf, content, StandardCharsets.UTF_8, unreserved, false);
+        encode(buf, content, StandardCharsets.UTF_8, unreserved, false, lowerCaseHex);
     }
 
     /**
@@ -281,7 +296,7 @@ public String encode(final CharSequence content) {
             return null;
         }
         final StringBuilder buf = new StringBuilder();
-        encode(buf, content, StandardCharsets.UTF_8, unreserved, false);
+        encode(buf, content, StandardCharsets.UTF_8, unreserved, false, lowerCaseHex);
         return buf.toString();
     }
 
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java
new file mode 100644
index 000000000..6f86cba1f
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/StructuredFieldsExample.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * 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.examples;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.HeaderGroup;
+import org.apache.hc.core5.http.structured.StructuredFieldDictionary;
+import org.apache.hc.core5.http.structured.StructuredFieldHeaders;
+import org.apache.hc.core5.http.structured.StructuredFieldInnerList;
+import org.apache.hc.core5.http.structured.StructuredFieldItem;
+import org.apache.hc.core5.http.structured.StructuredFieldList;
+
+/**
+ * Creates and consumes real HTTP fields that use the RFC 9651 data model.
+ */
+public final class StructuredFieldsExample {
+
+    public static void main(final String... args) throws Exception {
+        contentDigestRoundTrip();
+        parsePriority();
+        parseCacheStatus();
+        parseSignatureInput();
+    }
+
+    /** RFC 9530 Content-Digest sender and receiver round trip. */
+    private static void contentDigestRoundTrip() throws Exception {
+        final byte[] body = "{\"message\":\"hello\"}".getBytes(StandardCharsets.UTF_8);
+        final byte[] digest = MessageDigest.getInstance("SHA-256").digest(body);
+
+        final StructuredFieldDictionary contentDigest = StructuredFieldDictionary.builder()
+                .put("sha-256", StructuredFieldItem.ofByteSequence(digest))
+                .build();
+        final Header header = StructuredFieldHeaders.format("Content-Digest", contentDigest);
+        System.out.println(header);
+
+        final HeaderGroup receivedHeaders = new HeaderGroup();
+        receivedHeaders.addHeader(header);
+        final StructuredFieldItem receivedDigest = (StructuredFieldItem)
+                StructuredFieldHeaders.parseDictionary(receivedHeaders, "Content-Digest").get("sha-256");
+        final boolean valid = MessageDigest.isEqual(digest, receivedDigest.getBareItem().getByteSequenceValue());
+        System.out.println("Digest valid: " + valid);
+    }
+
+    /** RFC 9218 Priority is a Dictionary Structured Field. */
+    private static void parsePriority() throws Exception {
+        final StructuredFieldDictionary priority = StructuredFieldHeaders.parseDictionary(
+                new BasicHeader("Priority", "u=0, i"));
+        final long urgency = ((StructuredFieldItem) priority.get("u")).getBareItem().getLongValue();
+        final boolean incremental = ((StructuredFieldItem) priority.get("i"))
+                .getBareItem().getBooleanValue();
+        System.out.println("Priority urgency=" + urgency + ", incremental=" + incremental);
+    }
+
+    /** RFC 9211 Cache-Status is a List Structured Field. */
+    private static void parseCacheStatus() throws Exception {
+        final StructuredFieldList cacheStatus = StructuredFieldHeaders.parseList(
+                new BasicHeader("Cache-Status", "ExampleCache;hit;ttl=376"));
+        final StructuredFieldItem cache = (StructuredFieldItem) cacheStatus.get(0);
+        System.out.println("Cache " + cache.getBareItem().getTextValue()
+                + " hit=" + cache.getParameters().get("hit").getBooleanValue()
+                + " ttl=" + cache.getParameters().get("ttl").getLongValue());
+    }
+
+    /** RFC 9421 Signature-Input is a Dictionary Structured Field. */
+    private static void parseSignatureInput() throws Exception {
+        final Header header = new BasicHeader("Signature-Input",
+                "sig1=(\"@method\" \"@target-uri\" \"content-digest\")"
+                        + ";created=1618884475;keyid=\"test-key-rsa-pss\"");
+        final StructuredFieldInnerList signature = (StructuredFieldInnerList)
+                StructuredFieldHeaders.parseDictionary(header).get("sig1");
+        System.out.println("Signature covers " + signature.size()
+                + " components using key " + signature.getParameters().get("keyid").getTextValue());
+    }
+
+    private StructuredFieldsExample() {
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java
new file mode 100644
index 000000000..9f13c2272
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldConformance.java
@@ -0,0 +1,558 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.ParseException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Runs the complete HTTP Working Group Structured Field test corpus.
+ *
+ * <p>The corpus is revision 1e280c3ed9ffe0ca5fdb1d97219dddc389007677
+ * from <a href="https://github.com/httpwg/structured-field-tests">...</a>.</p>
+ */
+class TestStructuredFieldConformance {
+
+    private static final String ROOT = "/org/apache/hc/core5/http/structured/rfc9651/";
+
+    private static final String[] PARSING_FILES = {
+        "binary.json",
+        "boolean.json",
+        "date.json",
+        "dictionary.json",
+        "display-string.json",
+        "examples.json",
+        "item.json",
+        "key-generated.json",
+        "large-generated.json",
+        "list.json",
+        "listlist.json",
+        "number-generated.json",
+        "number.json",
+        "param-dict.json",
+        "param-list.json",
+        "param-listlist.json",
+        "string-generated.json",
+        "string.json",
+        "token-generated.json",
+        "token.json"
+    };
+
+    private static final String[] SERIALIZATION_FILES = {
+        "serialisation-tests/key-generated.json",
+        "serialisation-tests/number.json",
+        "serialisation-tests/string-generated.json",
+        "serialisation-tests/token-generated.json"
+    };
+
+    @Test
+    void testOfficialCorpus() throws Exception {
+        final List<String> failures = new ArrayList<>();
+        int count = 0;
+        for (final String file : PARSING_FILES) {
+            count += runFile(file, true, failures);
+        }
+        for (final String file : SERIALIZATION_FILES) {
+            count += runFile(file, false, failures);
+        }
+        Assertions.assertEquals(2135, count, "Unexpected number of HTTPWG test cases");
+        if (!failures.isEmpty()) {
+            Assertions.fail(failures.size() + " HTTPWG conformance failure(s):\n" + String.join("\n", failures));
+        }
+    }
+
+    private static int runFile(
+            final String file, final boolean parsing, final List<String> failures) throws IOException {
+        final Object document;
+        try (InputStream inputStream = TestStructuredFieldConformance.class.getResourceAsStream(ROOT + file)) {
+            if (inputStream == null) {
+                throw new IOException("Missing test corpus resource " + file);
+            }
+            document = new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).parse();
+        }
+        final List<?> tests = list(document);
+        for (final Object value : tests) {
+            final Map<?, ?> test = map(value);
+            final String name = string(test.get("name"));
+            try {
+                if (parsing) {
+                    runParsingCase(test);
+                } else {
+                    runSerializationCase(test);
+                }
+            } catch (final Exception ex) {
+                if (!booleanValue(test.get("must_fail")) && !booleanValue(test.get("can_fail"))) {
+                    failures.add(file + " :: " + name + " :: unexpected "
+                            + ex.getClass().getSimpleName() + ": " + ex.getMessage());
+                }
+            } catch (final AssertionError ex) {
+                failures.add(file + " :: " + name + " :: " + ex.getMessage());
+            }
+        }
+        return tests.size();
+    }
+
+    private static void runParsingCase(final Map<?, ?> test) throws Exception {
+        final boolean mustFail = booleanValue(test.get("must_fail"));
+        try {
+            final StructuredFieldValue actual = parse(
+                    string(test.get("header_type")), strings(test.get("raw")));
+            if (mustFail) {
+                throw new AssertionError("parsing succeeded but must_fail is true");
+            }
+            final StructuredFieldValue expected = value(
+                    string(test.get("header_type")), test.get("expected"));
+            if (!expected.equals(actual)) {
+                throw new AssertionError("expected " + expected + " but parsed " + actual);
+            }
+            verifyCanonical(test, actual);
+        } catch (final ParseException | IllegalArgumentException ex) {
+            if (!mustFail) {
+                throw ex;
+            }
+        }
+    }
+
+    private static void runSerializationCase(final Map<?, ?> test) {
+        final boolean mustFail = booleanValue(test.get("must_fail"));
+        try {
+            final StructuredFieldValue value = value(
+                    string(test.get("header_type")), test.get("expected"));
+            final String actual = StructuredFieldSerializer.serialize(value);
+            if (mustFail) {
+                throw new AssertionError("serialization succeeded but must_fail is true: " + actual);
+            }
+            verifyCanonical(test, value);
+        } catch (final IllegalArgumentException ex) {
+            if (!mustFail) {
+                throw ex;
+            }
+        }
+    }
+
+    private static void verifyCanonical(final Map<?, ?> test, final StructuredFieldValue value) {
+        final Object canonical = test.containsKey("canonical") ? test.get("canonical") : test.get("raw");
+        final List<String> fieldLines = strings(canonical);
+        final String expected = fieldLines.isEmpty() ? null : String.join(", ", fieldLines);
+        final String actual = StructuredFieldSerializer.serialize(value);
+        if (expected == null ? actual != null : !expected.equals(actual)) {
+            throw new AssertionError("expected canonical " + expected + " but serialized " + actual);
+        }
+    }
+
+    private static StructuredFieldValue parse(final String type, final List<String> lines) throws ParseException {
+        switch (type) {
+        case "item":
+            return StructuredFieldParser.parseItemLines(lines);
+        case "list":
+            return StructuredFieldParser.parseListLines(lines);
+        case "dictionary":
+            return StructuredFieldParser.parseDictionaryLines(lines);
+        default:
+            throw new IllegalArgumentException("Unknown header type " + type);
+        }
+    }
+
+    private static StructuredFieldValue value(final String type, final Object expected) {
+        switch (type) {
+        case "item":
+            return item(expected);
+        case "list":
+            return structuredList(expected);
+        case "dictionary":
+            return dictionary(expected);
+        default:
+            throw new IllegalArgumentException("Unknown header type " + type);
+        }
+    }
+
+    private static StructuredFieldList structuredList(final Object value) {
+        final List<StructuredFieldMember> members = new ArrayList<>();
+        for (final Object member : list(value)) {
+            members.add(member(member));
+        }
+        return StructuredFieldList.of(members);
+    }
+
+    private static StructuredFieldDictionary dictionary(final Object value) {
+        final StructuredFieldDictionary.Builder builder = StructuredFieldDictionary.builder();
+        for (final Object entry : list(value)) {
+            final List<?> pair = list(entry);
+            builder.put(string(pair.get(0)), member(pair.get(1)));
+        }
+        return builder.build();
+    }
+
+    private static StructuredFieldMember member(final Object value) {
+        final List<?> pair = list(value);
+        if (pair.get(0) instanceof List<?>) {
+            final List<StructuredFieldItem> items = new ArrayList<>();
+            for (final Object item : list(pair.get(0))) {
+                items.add(item(item));
+            }
+            return StructuredFieldInnerList.of(items, parameters(pair.get(1)));
+        }
+        return item(value);
+    }
+
+    private static StructuredFieldItem item(final Object value) {
+        final List<?> pair = list(value);
+        return StructuredFieldItem.of(bareItem(pair.get(0)), parameters(pair.get(1)));
+    }
+
+    private static StructuredFieldParameters parameters(final Object value) {
+        final StructuredFieldParameters.Builder builder = StructuredFieldParameters.builder();
+        for (final Object parameter : list(value)) {
+            final List<?> pair = list(parameter);
+            builder.put(string(pair.get(0)), bareItem(pair.get(1)));
+        }
+        return builder.build();
+    }
+
+    private static StructuredFieldBareItem bareItem(final Object value) {
+        if (value instanceof Boolean) {
+            return StructuredFieldBareItem.ofBoolean(((Boolean) value).booleanValue());
+        }
+        if (value instanceof Long) {
+            return StructuredFieldBareItem.ofInteger(((Long) value).longValue());
+        }
+        if (value instanceof BigDecimal) {
+            return StructuredFieldBareItem.ofDecimal((BigDecimal) value);
+        }
+        if (value instanceof String) {
+            return StructuredFieldBareItem.ofString((String) value);
+        }
+        final Map<?, ?> typed = map(value);
+        final String type = string(typed.get("__type"));
+        switch (type) {
+        case "token":
+            return StructuredFieldBareItem.ofToken(string(typed.get("value")));
+        case "binary":
+            return StructuredFieldBareItem.ofByteSequence(decodeBase32(string(typed.get("value"))));
+        case "date":
+            return StructuredFieldBareItem.ofDate(((Long) typed.get("value")).longValue());
+        case "displaystring":
+            return StructuredFieldBareItem.ofDisplayString(string(typed.get("value")));
+        default:
+            throw new IllegalArgumentException("Unknown bare Item type " + type);
+        }
+    }
+
+    private static byte[] decodeBase32(final String encoded) {
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+        int bits = 0;
+        int buffer = 0;
+        for (int i = 0; i < encoded.length(); i++) {
+            final char ch = encoded.charAt(i);
+            if (ch == '=') {
+                break;
+            }
+            final int value = ch >= 'A' && ch <= 'Z' ? ch - 'A'
+                    : ch >= '2' && ch <= '7' ? ch - '2' + 26 : -1;
+            if (value < 0) {
+                throw new IllegalArgumentException("Invalid base32 character " + ch);
+            }
+            buffer = buffer << 5 | value;
+            bits += 5;
+            if (bits >= 8) {
+                output.write(buffer >> (bits - 8) & 0xff);
+                bits -= 8;
+            }
+        }
+        return output.toByteArray();
+    }
+
+    private static boolean booleanValue(final Object value) {
+        return value instanceof Boolean && ((Boolean) value).booleanValue();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<Object> list(final Object value) {
+        if (!(value instanceof List<?>)) {
+            throw new IllegalArgumentException("Expected JSON array but got " + value);
+        }
+        return (List<Object>) value;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Map<Object, Object> map(final Object value) {
+        if (!(value instanceof Map<?, ?>)) {
+            throw new IllegalArgumentException("Expected JSON object but got " + value);
+        }
+        return (Map<Object, Object>) value;
+    }
+
+    private static String string(final Object value) {
+        if (!(value instanceof String)) {
+            throw new IllegalArgumentException("Expected JSON string but got " + value);
+        }
+        return (String) value;
+    }
+
+    private static List<String> strings(final Object value) {
+        final List<String> strings = new ArrayList<>();
+        for (final Object element : list(value)) {
+            strings.add(string(element));
+        }
+        return strings;
+    }
+
+    private static final class JsonReader {
+
+        private final Reader reader;
+        private int current = -2;
+
+        JsonReader(final Reader reader) {
+            this.reader = reader;
+        }
+
+        Object parse() throws IOException {
+            final Object value = readValue();
+            skipWhitespace();
+            if (peek() != -1) {
+                throw error("Trailing JSON data");
+            }
+            return value;
+        }
+
+        private Object readValue() throws IOException {
+            skipWhitespace();
+            switch (peek()) {
+            case '[':
+                return readArray();
+            case '{':
+                return readObject();
+            case '"':
+                return readString();
+            case 't':
+                readLiteral("true");
+                return Boolean.TRUE;
+            case 'f':
+                readLiteral("false");
+                return Boolean.FALSE;
+            case 'n':
+                readLiteral("null");
+                return null;
+            default:
+                return readNumber();
+            }
+        }
+
+        private List<Object> readArray() throws IOException {
+            expect('[');
+            final List<Object> values = new ArrayList<>();
+            skipWhitespace();
+            if (peek() == ']') {
+                read();
+                return values;
+            }
+            while (true) {
+                values.add(readValue());
+                skipWhitespace();
+                final int ch = read();
+                if (ch == ']') {
+                    return values;
+                }
+                if (ch != ',') {
+                    throw error("Expected ',' or ']'");
+                }
+            }
+        }
+
+        private Map<Object, Object> readObject() throws IOException {
+            expect('{');
+            final Map<Object, Object> values = new LinkedHashMap<>();
+            skipWhitespace();
+            if (peek() == '}') {
+                read();
+                return values;
+            }
+            while (true) {
+                skipWhitespace();
+                final String key = readString();
+                skipWhitespace();
+                expect(':');
+                values.put(key, readValue());
+                skipWhitespace();
+                final int ch = read();
+                if (ch == '}') {
+                    return values;
+                }
+                if (ch != ',') {
+                    throw error("Expected ',' or '}'");
+                }
+            }
+        }
+
+        private String readString() throws IOException {
+            expect('"');
+            final StringBuilder buffer = new StringBuilder();
+            while (true) {
+                final int ch = read();
+                if (ch == -1) {
+                    throw error("Unterminated JSON string");
+                }
+                if (ch == '"') {
+                    return buffer.toString();
+                }
+                if (ch == '\\') {
+                    final int escaped = read();
+                    switch (escaped) {
+                    case '"':
+                    case '\\':
+                    case '/':
+                        buffer.append((char) escaped);
+                        break;
+                    case 'b':
+                        buffer.append('\b');
+                        break;
+                    case 'f':
+                        buffer.append('\f');
+                        break;
+                    case 'n':
+                        buffer.append('\n');
+                        break;
+                    case 'r':
+                        buffer.append('\r');
+                        break;
+                    case 't':
+                        buffer.append('\t');
+                        break;
+                    case 'u':
+                        buffer.append((char) readHex4());
+                        break;
+                    default:
+                        throw error("Invalid JSON escape");
+                    }
+                } else {
+                    buffer.append((char) ch);
+                }
+            }
+        }
+
+        private Number readNumber() throws IOException {
+            final StringBuilder buffer = new StringBuilder();
+            int ch = peek();
+            if (ch == '-') {
+                buffer.append((char) read());
+                ch = peek();
+            }
+            if (ch < '0' || ch > '9') {
+                throw error("Invalid JSON number");
+            }
+            while ((ch = peek()) >= '0' && ch <= '9') {
+                buffer.append((char) read());
+            }
+            boolean decimal = false;
+            if (peek() == '.') {
+                decimal = true;
+                buffer.append((char) read());
+                while ((ch = peek()) >= '0' && ch <= '9') {
+                    buffer.append((char) read());
+                }
+            }
+            ch = peek();
+            if (ch == 'e' || ch == 'E') {
+                decimal = true;
+                buffer.append((char) read());
+                ch = peek();
+                if (ch == '+' || ch == '-') {
+                    buffer.append((char) read());
+                }
+                while ((ch = peek()) >= '0' && ch <= '9') {
+                    buffer.append((char) read());
+                }
+            }
+            return decimal ? new BigDecimal(buffer.toString()) : Long.valueOf(buffer.toString());
+        }
+
+        private void readLiteral(final String literal) throws IOException {
+            for (int i = 0; i < literal.length(); i++) {
+                expect(literal.charAt(i));
+            }
+        }
+
+        private int readHex4() throws IOException {
+            int value = 0;
+            for (int i = 0; i < 4; i++) {
+                final int ch = read();
+                final int digit = ch >= '0' && ch <= '9' ? ch - '0'
+                        : ch >= 'a' && ch <= 'f' ? ch - 'a' + 10
+                        : ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1;
+                if (digit < 0) {
+                    throw error("Invalid JSON Unicode escape");
+                }
+                value = value << 4 | digit;
+            }
+            return value;
+        }
+
+        private void skipWhitespace() throws IOException {
+            int ch;
+            while ((ch = peek()) == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
+                read();
+            }
+        }
+
+        private void expect(final int expected) throws IOException {
+            if (read() != expected) {
+                throw error("Expected '" + (char) expected + "'");
+            }
+        }
+
+        private int peek() throws IOException {
+            if (current == -2) {
+                current = reader.read();
+            }
+            return current;
+        }
+
+        private int read() throws IOException {
+            final int value = peek();
+            current = -2;
+            return value;
+        }
+
+        private IOException error(final String message) {
+            return new IOException(message);
+        }
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java
new file mode 100644
index 000000000..4c1e3fb4a
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldParser.java
@@ -0,0 +1,135 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.util.Tokenizer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldParser {
+
+    @Test
+    void testAllBareItemTypes() throws Exception {
+        assertItem("42", StructuredFieldType.INTEGER, Long.valueOf(42));
+        assertItem("-999999999999999", StructuredFieldType.INTEGER, Long.valueOf(-999999999999999L));
+        assertItem("4.5", StructuredFieldType.DECIMAL, new BigDecimal("4.5"));
+        assertItem("\"hello \\\"world\\\"\"", StructuredFieldType.STRING, "hello \"world\"");
+        assertItem("foo123/456", StructuredFieldType.TOKEN, "foo123/456");
+        assertItem("?1", StructuredFieldType.BOOLEAN, Boolean.TRUE);
+        assertItem("@1659578233", StructuredFieldType.DATE, Long.valueOf(1659578233));
+        assertItem("%\"f%c3%bc%c3%bc\"", StructuredFieldType.DISPLAY_STRING, "füü");
+
+        final StructuredFieldBareItem bytes = StructuredFieldParser.parseItem(":aGVsbG8=:").getBareItem();
+        Assertions.assertEquals(StructuredFieldType.BYTE_SEQUENCE, bytes.getType());
+        Assertions.assertArrayEquals("hello".getBytes(StandardCharsets.US_ASCII), bytes.getByteSequenceValue());
+    }
+
+    @Test
+    void testListAndInnerList() throws Exception {
+        final StructuredFieldList list = StructuredFieldParser.parseList(
+                "sugar, (tea \"with milk\";q=0.7);fresh, rum;proof=40");
+        Assertions.assertEquals(3, list.size());
+        Assertions.assertEquals("sugar", ((StructuredFieldItem) list.get(0)).getBareItem().getTextValue());
+        final StructuredFieldInnerList inner = (StructuredFieldInnerList) list.get(1);
+        Assertions.assertEquals(2, inner.size());
+        Assertions.assertEquals(new BigDecimal("0.7"), inner.get(1).getParameters().get("q").getDecimalValue());
+        Assertions.assertTrue(inner.getParameters().get("fresh").getBooleanValue());
+        Assertions.assertEquals(40L, ((StructuredFieldItem) list.get(2)).getParameters().get("proof").getLongValue());
+    }
+
+    @Test
+    void testDictionaryAndDuplicateReplacement() throws Exception {
+        final StructuredFieldDictionary dictionary = StructuredFieldParser.parseDictionary(
+                "a=?0, b, c;foo=bar, a=(1 2);valid");
+        Assertions.assertEquals(3, dictionary.size());
+        Assertions.assertEquals("a", dictionary.getName(0));
+        Assertions.assertTrue(dictionary.get("a") instanceof StructuredFieldInnerList);
+        Assertions.assertTrue(((StructuredFieldItem) dictionary.get("b")).getBareItem().getBooleanValue());
+        Assertions.assertEquals("bar", ((StructuredFieldItem) dictionary.get("c"))
+                .getParameters().get("foo").getTextValue());
+    }
+
+    @Test
+    void testDuplicateParameterReplacementPreservesPosition() throws Exception {
+        final StructuredFieldParameters parameters = StructuredFieldParser.parseItem("token;a=1;b=2;a=3")
+                .getParameters();
+        Assertions.assertEquals(2, parameters.size());
+        Assertions.assertEquals("a", parameters.getName(0));
+        Assertions.assertEquals(3L, parameters.get("a").getLongValue());
+    }
+
+    @Test
+    void testMultipleFieldLines() throws Exception {
+        final StructuredFieldList list = StructuredFieldParser.parseListLines(Arrays.asList("one, two", "three"));
+        Assertions.assertEquals(3, list.size());
+        Assertions.assertEquals("one, two, three", StructuredFieldSerializer.serializeList(list));
+
+        final StructuredFieldDictionary dictionary = StructuredFieldParser.parseDictionaryLines(
+                Arrays.asList("a=1", "b=2"));
+        Assertions.assertEquals("a=1, b=2", StructuredFieldSerializer.serializeDictionary(dictionary));
+    }
+
+    @Test
+    void testTokenizerCursorBoundsAndPosition() throws Exception {
+        final String input = "xx 42 yy";
+        final Tokenizer.Cursor cursor = new Tokenizer.Cursor(2, 6);
+        final StructuredFieldItem item = StructuredFieldParser.parseItem(input, cursor);
+        Assertions.assertEquals(42L, item.getBareItem().getLongValue());
+        Assertions.assertEquals(6, cursor.getPos());
+    }
+
+    @Test
+    void testRfcMinimumContainerSizes() throws Exception {
+        final StringBuilder listValue = new StringBuilder();
+        for (int i = 0; i < 1024; i++) {
+            if (i > 0) {
+                listValue.append(',');
+            }
+            listValue.append(i);
+        }
+        Assertions.assertEquals(1024, StructuredFieldParser.parseList(listValue).size());
+
+        final StringBuilder parameters = new StringBuilder("token");
+        for (int i = 0; i < 256; i++) {
+            parameters.append(";p").append(i);
+        }
+        Assertions.assertEquals(256, StructuredFieldParser.parseItem(parameters).getParameters().size());
+    }
+
+    private static void assertItem(
+            final String encoded, final StructuredFieldType type, final Object expected) throws ParseException {
+        final StructuredFieldBareItem item = StructuredFieldParser.parseItem(encoded).getBareItem();
+        Assertions.assertEquals(type, item.getType());
+        Assertions.assertEquals(expected, item.getValue());
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java
new file mode 100644
index 000000000..a0a7772d9
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldRealWorldHeaders.java
@@ -0,0 +1,99 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldRealWorldHeaders {
+
+    @Test
+    void testRfc9218Priority() throws Exception {
+        final StructuredFieldDictionary priority = StructuredFieldHeaders.parseDictionary(
+                new BasicHeader("Priority", "u=0, i"));
+        Assertions.assertEquals(0L, item(priority, "u").getBareItem().getLongValue());
+        Assertions.assertTrue(item(priority, "i").getBareItem().getBooleanValue());
+    }
+
+    @Test
+    void testRfc9530ContentDigest() throws Exception {
+        final byte[] content = "{\"hello\":\"world\"}\n".getBytes(StandardCharsets.UTF_8);
+        final byte[] digest = MessageDigest.getInstance("SHA-256").digest(content);
+        final StructuredFieldDictionary value = StructuredFieldDictionary.builder()
+                .put("sha-256", StructuredFieldItem.ofByteSequence(digest))
+                .build();
+        final Header header = StructuredFieldHeaders.format("Content-Digest", value);
+        final byte[] parsed = item(StructuredFieldHeaders.parseDictionary(header), "sha-256")
+                .getBareItem().getByteSequenceValue();
+        Assertions.assertTrue(MessageDigest.isEqual(digest, parsed));
+    }
+
+    @Test
+    void testRfc9211CacheStatus() throws Exception {
+        final StructuredFieldList status = StructuredFieldHeaders.parseList(
+                new BasicHeader("Cache-Status", "ExampleCache;hit;ttl=376"));
+        final StructuredFieldItem cache = (StructuredFieldItem) status.get(0);
+        Assertions.assertEquals("ExampleCache", cache.getBareItem().getTextValue());
+        Assertions.assertTrue(cache.getParameters().get("hit").getBooleanValue());
+        Assertions.assertEquals(376L, cache.getParameters().get("ttl").getLongValue());
+    }
+
+    @Test
+    void testRfc9209ProxyStatus() throws Exception {
+        final StructuredFieldList status = StructuredFieldHeaders.parseList(new BasicHeader(
+                "Proxy-Status", "ExampleCDN;error=connection_timeout;received-status=504"));
+        final StructuredFieldItem proxy = (StructuredFieldItem) status.get(0);
+        Assertions.assertEquals("connection_timeout",
+                proxy.getParameters().get("error").getTextValue());
+        Assertions.assertEquals(504L,
+                proxy.getParameters().get("received-status").getLongValue());
+    }
+
+    @Test
+    void testRfc9421SignatureInput() throws Exception {
+        final StructuredFieldDictionary signatureInput = StructuredFieldHeaders.parseDictionary(new BasicHeader(
+                "Signature-Input", "sig1=(\"@method\" \"@target-uri\" \"content-digest\")"
+                        + ";created=1618884475;keyid=\"test-key-rsa-pss\""));
+        final StructuredFieldInnerList signature = (StructuredFieldInnerList) signatureInput.get("sig1");
+        Assertions.assertEquals(3, signature.size());
+        Assertions.assertEquals("@method", signature.get(0).getBareItem().getTextValue());
+        Assertions.assertEquals(1618884475L,
+                signature.getParameters().get("created").getLongValue());
+        Assertions.assertEquals("test-key-rsa-pss",
+                signature.getParameters().get("keyid").getTextValue());
+    }
+
+    private static StructuredFieldItem item(final StructuredFieldDictionary dictionary, final String key) {
+        return (StructuredFieldItem) dictionary.get(key);
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java
new file mode 100644
index 000000000..be02880a2
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldSerializer.java
@@ -0,0 +1,191 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.ParseException;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.BufferedHeader;
+import org.apache.hc.core5.http.message.HeaderGroup;
+import org.apache.hc.core5.util.CharArrayBuffer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStructuredFieldSerializer {
+
+    @Test
+    void testCanonicalNumbersAndDisplayString() {
+        Assertions.assertEquals("0.0", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0001"))));
+        Assertions.assertEquals("1.234", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2344"))));
+        Assertions.assertEquals("1.234", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2345"))));
+        Assertions.assertEquals("1.236", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("1.2355"))));
+        Assertions.assertEquals("0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("0.0015"))));
+        Assertions.assertEquals("0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("0.0025"))));
+        Assertions.assertEquals("-0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0015"))));
+        Assertions.assertEquals("-0.002", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("-0.0025"))));
+        Assertions.assertEquals("10.0", bare(StructuredFieldBareItem.ofDecimal(new BigDecimal("9.9995"))));
+        Assertions.assertEquals("%\"F%c3%bc%c3%9fe %22%25%22\"",
+                bare(StructuredFieldBareItem.ofDisplayString("Füße \"%\"")));
+    }
+
+    @Test
+    void testCanonicalDictionaryAndParameters() {
+        final StructuredFieldParameters parameters = StructuredFieldParameters.builder()
+                .putBoolean("valid", true)
+                .putBoolean("cached", false)
+                .put("ttl", StructuredFieldBareItem.ofInteger(60))
+                .build();
+        final StructuredFieldDictionary dictionary = StructuredFieldDictionary.builder()
+                .put("available", StructuredFieldItem.of(StructuredFieldBareItem.ofBoolean(true), parameters))
+                .put("rating", StructuredFieldItem.of(
+                        StructuredFieldBareItem.ofDecimal(new BigDecimal("1.500"))))
+                .put("feelings", StructuredFieldInnerList.of(
+                        StructuredFieldItem.of(StructuredFieldBareItem.ofToken("joy")),
+                        StructuredFieldItem.of(StructuredFieldBareItem.ofToken("sadness"))))
+                .build();
+        Assertions.assertEquals(
+                "available;valid;cached=?0;ttl=60, rating=1.5, feelings=(joy sadness)",
+                StructuredFieldSerializer.serializeDictionary(dictionary));
+    }
+
+    @Test
+    void testEmptyContainersOmitField() throws Exception {
+        final StructuredFieldList list = StructuredFieldList.of();
+        final StructuredFieldDictionary dictionary = StructuredFieldDictionary.builder().build();
+        Assertions.assertNull(StructuredFieldSerializer.serializeList(list));
+        Assertions.assertNull(StructuredFieldSerializer.serializeDictionary(dictionary));
+        Assertions.assertNull(StructuredFieldHeaders.format("Example", list));
+        Assertions.assertTrue(StructuredFieldParser.parseList("").isEmpty());
+        Assertions.assertTrue(StructuredFieldParser.parseDictionary("").isEmpty());
+    }
+
+    @Test
+    void testMessageHeadersCombineLinesCaseInsensitively() throws Exception {
+        final HeaderGroup headers = new HeaderGroup();
+        headers.addHeader(new BasicHeader("Example", "one, two"));
+        headers.addHeader(new BasicHeader("EXAMPLE", "three"));
+        final StructuredFieldList value = StructuredFieldHeaders.parseList(headers, "example");
+        Assertions.assertEquals(3, value.size());
+        final Header formatted = StructuredFieldHeaders.format("Example", value);
+        Assertions.assertNotNull(formatted);
+        Assertions.assertTrue(formatted instanceof BufferedHeader);
+        Assertions.assertEquals("one, two, three", formatted.getValue());
+        Assertions.assertEquals(value, StructuredFieldHeaders.parseList(formatted));
+    }
+
+    @Test
+    void testMessageHeadersCombineFormattedHeaderLines() throws Exception {
+        final HeaderGroup headers = new HeaderGroup();
+        headers.addHeader(bufferedHeader("Example: one, two"));
+        headers.addHeader(bufferedHeader("Example: three"));
+        final StructuredFieldList value = StructuredFieldHeaders.parseList(headers, "example");
+        Assertions.assertEquals(3, value.size());
+        Assertions.assertEquals("one, two, three",
+                StructuredFieldHeaders.format("Example", value).getValue());
+    }
+
+    @Test
+    void testMessageHeadersItemMustNotSpanFieldLines() throws Exception {
+        final HeaderGroup single = new HeaderGroup();
+        single.addHeader(new BasicHeader("Example", "10"));
+        Assertions.assertEquals(
+                StructuredFieldHeaders.parseItem(new BasicHeader("Example", "10")),
+                StructuredFieldHeaders.parseItem(single, "example"));
+
+        final HeaderGroup multiple = new HeaderGroup();
+        multiple.addHeader(new BasicHeader("Example", "10"));
+        multiple.addHeader(new BasicHeader("Example", "20"));
+        Assertions.assertThrows(ParseException.class,
+                () -> StructuredFieldHeaders.parseItem(multiple, "example"));
+    }
+
+    @Test
+    void testMessageHeadersListMembersMustBeCommaSeparated() {
+        final HeaderGroup headers = new HeaderGroup();
+        headers.addHeader(new BasicHeader("Example", "a b"));
+        Assertions.assertThrows(ParseException.class,
+                () -> StructuredFieldHeaders.parseList(headers, "example"));
+    }
+
+    private static BufferedHeader bufferedHeader(final String line) {
+        final CharArrayBuffer buffer = new CharArrayBuffer(line.length());
+        buffer.append(line);
+        return BufferedHeader.create(buffer);
+    }
+
+    @Test
+    void testSerializeIntoExistingBuffer() {
+        final CharArrayBuffer buffer = new CharArrayBuffer(32);
+        buffer.append("Priority: ");
+        StructuredFieldSerializer.serialize(buffer, StructuredFieldDictionary.builder()
+                .put("u", StructuredFieldItem.ofInteger(0))
+                .put("i", StructuredFieldItem.ofBoolean(true))
+                .build());
+        Assertions.assertEquals("Priority: u=0, i", buffer.toString());
+    }
+
+    @Test
+    void testImmutabilityAndValueSemantics() {
+        final byte[] bytes = {1, 2, 3};
+        final StructuredFieldBareItem item = StructuredFieldBareItem.ofByteSequence(bytes);
+        bytes[0] = 9;
+        Assertions.assertArrayEquals(new byte[] {1, 2, 3}, item.getByteSequenceValue());
+        final byte[] returned = item.getByteSequenceValue();
+        returned[0] = 8;
+        Assertions.assertArrayEquals(new byte[] {1, 2, 3}, item.getByteSequenceValue());
+
+        final StructuredFieldList list = StructuredFieldList.of(
+                StructuredFieldItem.of(StructuredFieldBareItem.ofInteger(1)));
+        Assertions.assertThrows(UnsupportedOperationException.class,
+                () -> list.getMembers().add(StructuredFieldItem.of(StructuredFieldBareItem.ofInteger(2))));
+        Assertions.assertEquals(list, StructuredFieldList.of(Arrays.asList(list.get(0))));
+    }
+
+    @Test
+    void testValidationAtConstructionBoundary() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldBareItem.ofInteger(1000000000000000L));
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldBareItem.ofDecimal(new BigDecimal("999999999999.9995")));
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldBareItem.ofString("é"));
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldBareItem.ofToken("1token"));
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldParameters.builder().putBoolean("Upper", true));
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldBareItem.ofDisplayString("\ud800"));
+    }
+
+    private static String bare(final StructuredFieldBareItem item) {
+        return StructuredFieldSerializer.serializeBareItem(item);
+    }
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java
new file mode 100644
index 000000000..c8484e416
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/structured/TestStructuredFieldStrictness.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * 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.structured;
+
+import org.apache.hc.core5.http.ParseException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Strictness cases aligned with the HTTPWG structured-field-tests corpus.
+ */
+class TestStructuredFieldStrictness {
+
+    @Test
+    void testInvalidItems() {
+        assertItemFails("", "-", "9999999999999999", "1.", "1.2345", "1000000000000.0",
+                "\"unterminated", "\"bad\\q\"", "?2", "@1.2", "'single'", "\té", "١");
+    }
+
+    @Test
+    void testInvalidContainers() {
+        assertListFails("one,", "one,,two", "(one\ttwo)", "(one,two)", "(one", "one ;x");
+        assertDictionaryFails("A=1", "a =1", "a=1,", "a==1", "1a=1", "a=(1\t2)");
+    }
+
+    @Test
+    void testStrictByteSequences() throws Exception {
+        Assertions.assertEquals(5,
+                StructuredFieldParser.parseItem(":aGVsbG8:").getBareItem().getByteSequenceValue().length);
+        Assertions.assertEquals(":YQ==:", StructuredFieldSerializer.serializeItem(
+                StructuredFieldParser.parseItem(":YQ=:")));
+        assertItemFails(":=aGVsbG8=:", ":a=GVsbG8=:", ":aGVsb G8=:", ":aGVsbG8=", ":_-Ah:", ":",
+                ":YQ===:");
+
+        final StructuredFieldItem nonZeroPadBits = StructuredFieldParser.parseItem(":iZ==:");
+        Assertions.assertEquals(":iQ==:", StructuredFieldSerializer.serializeItem(nonZeroPadBits));
+    }
+
+    @Test
+    void testStrictDisplayStrings() throws Exception {
+        final StructuredFieldItem item = StructuredFieldParser.parseItem("%\"foo %22bar%22 \\ baz\"");
+        Assertions.assertEquals("foo \"bar\" \\ baz", item.getBareItem().getTextValue());
+        Assertions.assertEquals("%\"foo %22bar%22 \\ baz\"", StructuredFieldSerializer.serializeItem(item));
+
+        final StructuredFieldItem overEncoded = StructuredFieldParser.parseItem("%\"%61\"");
+        Assertions.assertEquals("%\"a\"", StructuredFieldSerializer.serializeItem(overEncoded));
+
+        assertItemFails("%\"f%C3%BC\"", "%\"füü\"", "%\"%c3%28\"", "%\"%a0%a1\"",
+                "%\"%e2%28%a1\"", "%\"%f0%28%8c%28\"", "%\"%g0\"", "%\"%\"", "%foo");
+    }
+
+    @Test
+    void testOnlySpacesAreAllowedAtTopLevel() throws Exception {
+        Assertions.assertEquals(1L, StructuredFieldParser.parseItem(" 1 ").getBareItem().getLongValue());
+        assertItemFails(" \t 1", "1 \t ");
+    }
+
+    @Test
+    void testUnicodeDigitsAreNotRfcDigits() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> StructuredFieldParameters.builder().putBoolean("aÙ¡", true));
+    }
+
+    private static void assertItemFails(final String... values) {
+        for (final String value : values) {
+            Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseItem(value), value);
+        }
+    }
+
+    private static void assertListFails(final String... values) {
+        for (final String value : values) {
+            Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseList(value), value);
+        }
+    }
+
+    private static void assertDictionaryFails(final String... values) {
+        for (final String value : values) {
+            Assertions.assertThrows(ParseException.class, () -> StructuredFieldParser.parseDictionary(value), value);
+        }
+    }
+}
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
index c6a2b0459..42c220c8f 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java
@@ -30,6 +30,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
 import java.util.stream.Stream;
 
 import org.junit.jupiter.api.Test;
@@ -50,6 +51,15 @@ void testCoding() {
         assertEquals("blah%21%20~%20huh%3F", buf.toString());
     }
 
+    @Test
+    void testLowerCaseHexEncoding() {
+        final BitSet safeChars = new BitSet(256);
+        safeChars.set(' ');
+        final PercentCodec codec = new PercentCodec(safeChars, true);
+        assertEquals(" %c3%bc%22%25", codec.encode(" ü\"%"));
+        assertEquals("%C3%BC", PercentCodec.RFC3986.encode("ü"));
+    }
+
     @Test
     void testDecoding() {
         assertEquals("blah! ~ huh?", PercentCodec.decode("blah%21%20~%20huh%3F", StandardCharsets.UTF_8));
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json
new file mode 100644
index 000000000..bead6e1a3
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/binary.json
@@ -0,0 +1,106 @@
+[
+    {
+        "name": "basic binary",
+        "raw": [":aGVsbG8=:"],
+        "header_type": "item",
+        "expected": [
+            {"__type": "binary", "value": "NBSWY3DP"},
+            []]
+    },
+    {
+        "name": "empty binary",
+        "raw": ["::"],
+        "header_type": "item",
+        "expected": [
+            {"__type": "binary", "value": ""},
+            []]
+    },
+    {
+        "name": "padding at beginning",
+        "raw": [":=aGVsbG8=:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "padding in middle",
+        "raw": [":a=GVsbG8=:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad padding",
+        "raw": [":aGVsbG8:"],
+        "header_type": "item",
+        "expected": [
+            {"__type": "binary", "value": "NBSWY3DP"},
+            []],
+        "can_fail": true,
+        "canonical": [":aGVsbG8=:"]
+    },
+    {
+        "name": "bad padding dot",
+        "raw": [":aGVsbG8.:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad end delimiter",
+        "raw": [":aGVsbG8="],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "extra whitespace",
+        "raw": [":aGVsb G8=:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "all whitespace",
+        "raw": [":    :"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "extra chars",
+        "raw": [":aGVsbG!8=:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "suffix chars",
+        "raw": [":aGVsbG8=!:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "non-zero pad bits",
+        "raw": [":iZ==:"],
+        "header_type": "item",
+        "expected": [
+            {"__type": "binary", "value": "RE======"},
+            []],
+        "can_fail": true,
+        "canonical": [":iQ==:"]
+    },
+    {
+        "name": "non-ASCII binary",
+        "raw": [":/+Ah:"],
+        "header_type": "item",
+        "expected": [
+            {"__type": "binary", "value": "77QCC==="},
+            []]
+    },
+    {
+        "name": "base64url binary",
+        "raw": [":_-Ah:"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "binary prefix only",
+        "raw": [":"],
+        "header_type": "item",
+        "must_fail": true
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json
new file mode 100644
index 000000000..e3fc09226
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/boolean.json
@@ -0,0 +1,75 @@
+[
+    {
+        "name": "basic true boolean",
+        "raw": ["?1"],
+        "header_type": "item",
+        "expected": [true, []]
+    },
+    {
+        "name": "basic false boolean",
+        "raw": ["?0"],
+        "header_type": "item",
+        "expected": [false, []]
+    },
+    {
+        "name": "unknown boolean",
+        "raw": ["?Q"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "whitespace boolean",
+        "raw": ["? 1"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "negative zero boolean",
+        "raw": ["?-0"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "T boolean",
+        "raw": ["?T"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "F boolean",
+        "raw": ["?F"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "t boolean",
+        "raw": ["?t"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "f boolean",
+        "raw": ["?f"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "spelled-out True boolean",
+        "raw": ["?True"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "spelled-out False boolean",
+        "raw": ["?False"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "boolean prefix only",
+        "raw": ["?"],
+        "header_type": "item",
+        "must_fail": true
+    }
+]
+
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json
new file mode 100644
index 000000000..8c41ff784
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/date.json
@@ -0,0 +1,107 @@
+[
+    {
+        "name": "date - 1970-01-01 00:00:00",
+        "raw": ["@0"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 0}, []]
+    },
+    {
+        "name": "date - 2022-08-04 01:57:13",
+        "raw": ["@1659578233"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 1659578233}, []]
+    },
+    {
+        "name": "date - 1917-05-30 22:02:47",
+        "raw": ["@-1659578233"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": -1659578233}, []]
+    },
+    {
+        "name": "date - 2^31",
+        "raw": ["@2147483648"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 2147483648}, []]
+    },
+    {
+        "name": "date - 2^32",
+        "raw": ["@4294967296"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 4294967296}, []]
+    },
+    {
+        "name": "interoperability max date - 9999-12-31 00:00:00",
+        "raw": ["@253402214400"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 253402214400}, []]
+    },
+    {
+        "name": "interoperability min date - 0001-01-01 00:00:00",
+        "raw": ["@-62135596800"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": -62135596800}, []]
+    },
+    {
+        "name": "date - decimal",
+        "raw": ["@1659578233.12"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "syntactic max date - 999,999,999,999,999",
+        "raw": ["@999999999999999"],
+        "header_type": "item",
+        "can_fail": true,
+        "expected": [{"__type": "date", "value": 999999999999999}, []]
+    },
+    {
+        "name": "syntactic min date - -999,999,999,999,999",
+        "raw": ["@-999999999999999"],
+        "header_type": "item",
+        "can_fail": true,
+        "expected": [{"__type": "date", "value": -999999999999999}, []]
+    },
+    {
+        "name": "too large syntactic date - 1,000,000,000,000,000",
+        "raw": ["@1000000000000000"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "too small syntactic date - -1,000,000,000,000,000",
+        "raw": ["@-1000000000000000"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "date with negative zero",
+        "raw": ["@-0"],
+        "header_type": "item",
+        "expected": [{"__type": "date", "value": 0}, []],
+        "canonical": ["@0"]
+    },
+    {
+        "name": "date - empty",
+        "raw": ["@"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "date - sign only",
+        "raw": ["@-"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "date - space",
+        "raw": ["@ 12345678"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "date - non-digit",
+        "raw": ["@abc"],
+        "header_type": "item",
+        "must_fail": true
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json
new file mode 100644
index 000000000..0a7a7dda6
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/dictionary.json
@@ -0,0 +1,169 @@
+[
+    {
+        "name": "basic dictionary",
+        "raw": ["en=\"Applepie\", da=:w4ZibGV0w6ZydGUK:"],
+        "header_type": "dictionary",
+        "expected": [["en", ["Applepie", []]], ["da", [
+            {"__type": "binary", "value": "YODGE3DFOTB2M4TUMUFA===="},
+            []]
+        ]]
+    },
+    {
+        "name": "empty dictionary",
+        "raw": [""],
+        "header_type": "dictionary",
+        "expected": [],
+        "canonical": []
+    },
+    {
+        "name": "single item dictionary",
+        "raw": ["a=1"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]]]
+    },
+    {
+        "name": "list item dictionary",
+        "raw": ["a=(1 2)"],
+        "header_type": "dictionary",
+        "expected": [["a", [[[1, []], [2, []]], []]]]
+    },
+    {
+        "name": "single list item dictionary",
+        "raw": ["a=(1)"],
+        "header_type": "dictionary",
+        "expected": [["a", [[[1, []]], []]]]
+    },
+    {
+        "name": "empty list item dictionary",
+        "raw": ["a=()"],
+        "header_type": "dictionary",
+        "expected": [["a", [[], []]]]
+    },
+    {
+        "name": "no whitespace dictionary",
+        "raw": ["a=1,b=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [2, []]]],
+        "canonical": ["a=1, b=2"]
+    },
+    {
+        "name": "extra whitespace dictionary",
+        "raw": ["a=1 ,  b=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [2, []]]],
+        "canonical": ["a=1, b=2"]
+    },
+    {
+        "name": "tab separated dictionary",
+        "raw": ["a=1\t,\tb=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [2, []]]],
+        "canonical": ["a=1, b=2"]
+    },
+    {
+        "name": "leading whitespace dictionary",
+        "raw": ["     a=1 ,  b=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [2, []]]],
+        "canonical": ["a=1, b=2"]
+    },
+    {
+        "name": "whitespace before = dictionary",
+        "raw": ["a =1, b=2"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "whitespace after = dictionary",
+        "raw": ["a=1, b= 2"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "two lines dictionary",
+        "raw": ["a=1", "b=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [2, []]]],
+        "canonical": ["a=1, b=2"]
+    },
+    {
+        "name": "missing value dictionary",
+        "raw": ["a=1, b, c=3"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [true, []]], ["c", [3, []]]]
+    },
+    {
+        "name": "all missing value dictionary",
+        "raw": ["a, b, c"],
+        "header_type": "dictionary",
+        "expected": [["a", [true, []]], ["b", [true, []]], ["c", [true, []]]]
+    },
+    {
+        "name": "start missing value dictionary",
+        "raw": ["a, b=2"],
+        "header_type": "dictionary",
+        "expected": [["a", [true, []]], ["b", [2, []]]]
+    },
+    {
+        "name": "end missing value dictionary",
+        "raw": ["a=1, b"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [true, []]]]
+    },
+    {
+        "name": "missing value with params dictionary",
+        "raw": ["a=1, b;foo=9, c=3"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [true, [["foo", 9]]]], ["c", [3, []]]]
+    },
+    {
+        "name": "explicit true value with params dictionary",
+        "raw": ["a=1, b=?1;foo=9, c=3"],
+        "header_type": "dictionary",
+        "expected": [["a", [1, []]], ["b", [true, [["foo", 9]]]], ["c", [3, []]]],
+        "canonical": ["a=1, b;foo=9, c=3"]
+    },
+    {
+        "name": "trailing comma dictionary",
+        "raw": ["a=1, b=2,"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "empty item dictionary",
+        "raw": ["a=1,,b=2,"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "duplicate key dictionary",
+        "raw": ["a=1,b=2,a=3"],
+        "header_type": "dictionary",
+        "expected": [["a", [3, []]], ["b", [2, []]]],
+        "canonical": ["a=3, b=2"]
+    },
+    {
+        "name": "dictionary key ordering",
+        "raw": ["m, z, t"],
+        "header_type": "dictionary",
+        "expected": [["m", [true, []]], ["z", [true, []]], ["t", [true, []]]]
+    },
+    {
+        "name": "numeric key dictionary",
+        "raw": ["a=1,1b=2,a=1"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "uppercase key dictionary",
+        "raw": ["a=1,B=2,a=1"],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "bad key dictionary",
+        "raw": ["a=1,b!=2,a=1"],
+        "header_type": "dictionary",
+        "must_fail": true
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json
new file mode 100644
index 000000000..3fca7c692
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/display-string.json
@@ -0,0 +1,137 @@
+[
+    {
+        "name": "basic display string (ascii content)",
+        "raw": ["%\"foo bar\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": "foo bar"}, []]
+    },
+    {
+        "name": "all printable ascii",
+        "raw": ["%\" !%22#$%25&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"}, []]
+    },
+    {
+        "name": "non-ascii display string (uppercase escaping)",
+        "raw": ["%\"f%C3%BC%C3%BC\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "non-ascii display string (lowercase escaping)",
+        "raw": ["%\"f%c3%bc%c3%bc\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": "füü"}, []]
+    },
+    {
+        "name": "non-ascii display string (unescaped)",
+        "raw": ["%\"füü\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "tab in display string",
+        "raw": ["%\"\t\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "newline in display string",
+        "raw": ["%\"\n\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "single quoted display string",
+        "raw": ["%'foo'"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "unquoted display string",
+        "raw": ["%foo"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "display string missing initial quote",
+        "raw": ["%foo\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "unbalanced display string",
+        "raw": ["%\"foo"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "display string quoting",
+        "raw": ["%\"foo %22bar%22 \\ baz\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": "foo \"bar\" \\ baz"}, []]
+    },
+    {
+        "name": "bad display string escaping",
+        "raw": ["%\"foo %a"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad display string utf-8 (invalid 2-byte seq)",
+        "raw": ["%\"%c3%28\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad display string utf-8 (invalid sequence id)",
+        "raw": ["%\"%a0%a1\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad display string utf-8 (invalid hex)",
+        "raw": ["%\"%g0%1w\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad display string utf-8 (invalid 3-byte seq)",
+        "raw": ["%\"%e2%28%a1\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "bad display string utf-8 (invalid 4-byte seq)",
+        "raw": ["%\"%f0%28%8c%28\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "BOM in display string",
+        "raw": ["%\"BOM: %ef%bb%bf\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": "BOM: \uFEFF"}, []]
+    },
+    {
+        "name": "two lines display string",
+        "raw": ["%\"foo", "bar\""],
+        "header_type": "item",
+        "can_fail": true,
+        "expected": [{"__type": "displaystring", "value": "foo, bar"}, []],
+        "canonical": ["%\"foo, bar\""]
+    },
+    {
+        "name": "truncated display string escape (zero digits)",
+        "raw": ["%\"%\""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "over-encoded display string",
+        "raw": ["%\"%61\""],
+        "header_type": "item",
+        "expected": [{"__type": "displaystring", "value": "a"}, []],
+        "canonical": ["%\"a\""]
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json
new file mode 100644
index 000000000..eb7dc349f
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/examples.json
@@ -0,0 +1,208 @@
+[
+    {
+        "name": "Foo-Example",
+        "raw": ["2; foourl=\"https://foo.example.com/\""],
+        "header_type": "item",
+        "expected": [2, [["foourl", "https://foo.example.com/"]]],
+        "canonical": ["2;foourl=\"https://foo.example.com/\""]
+    },
+    {
+        "name": "Example-StrListHeader",
+        "raw": ["\"foo\", \"bar\", \"It was the best of times.\""],
+        "header_type": "list",
+        "expected": [
+            ["foo", []],
+            ["bar", []],
+            ["It was the best of times.", []]
+        ]
+    },
+    {
+        "name": "Example-Hdr (list on one line)",
+        "raw": ["foo, bar"],
+        "header_type": "list",
+        "expected": [
+            [{"__type":"token", "value":"foo"}, []],
+            [{"__type":"token", "value":"bar"}, []]
+        ]
+    },
+    {
+        "name": "Example-Hdr (list on two lines)",
+        "raw": ["foo", "bar"],
+        "header_type": "list",
+        "expected": [
+            [{"__type":"token", "value":"foo"}, []],
+            [{"__type":"token", "value":"bar"}, []]
+        ],
+        "canonical": ["foo, bar"]
+    },
+    {
+        "name": "Example-StrListListHeader",
+        "raw": ["(\"foo\" \"bar\"), (\"baz\"), (\"bat\" \"one\"), ()"],
+        "header_type": "list",
+        "expected": [
+            [[
+                ["foo", []],
+                ["bar", []]
+            ], []],
+            [[
+                ["baz", []]
+            ], []],
+            [[
+                ["bat", []],
+                ["one", []]
+            ], []],
+            [[], []]
+        ]
+    },
+    {
+        "name": "Example-ListListParam",
+        "raw": ["(\"foo\"; a=1;b=2);lvl=5, (\"bar\" \"baz\");lvl=1"],
+        "header_type": "list",
+        "expected": [
+            [[
+                ["foo", [["a", 1], ["b", 2]]]
+            ], [["lvl", 5]]],
+            [[
+                ["bar", []], ["baz", []]
+            ], [["lvl", 1]]]
+        ],
+        "canonical": ["(\"foo\";a=1;b=2);lvl=5, (\"bar\" \"baz\");lvl=1"]
+    },
+
+    {
+        "name": "Example-ParamListHeader",
+        "raw": ["abc;a=1;b=2; cde_456, (ghi;jk=4 l);q=\"9\";r=w"],
+        "header_type": "list",
+        "expected": [
+            [{"__type": "token", "value": "abc"}, [["a", 1], ["b", 2], ["cde_456", true]]],
+            [
+            [
+                [{"__type": "token", "value": "ghi"}, [["jk", 4]]],
+                [{"__type": "token", "value": "l"}, []]
+            ],
+            [["q", "9"], ["r", {"__type": "token", "value": "w"}]]
+            ]
+        ],
+        "canonical": ["abc;a=1;b=2;cde_456, (ghi;jk=4 l);q=\"9\";r=w"]
+    },
+    {
+        "name": "Example-IntHeader",
+        "raw": ["1; a; b=?0"],
+        "header_type": "item",
+        "expected": [1, [["a", true], ["b", false]]],
+        "canonical": ["1;a;b=?0"]
+    },
+    {
+        "name": "Example-DictHeader",
+        "raw": ["en=\"Applepie\", da=:w4ZibGV0w6ZydGU=:"],
+        "header_type": "dictionary",
+        "expected": [
+            ["en", ["Applepie", []]],
+            ["da", [{"__type": "binary", "value": "YODGE3DFOTB2M4TUMU======"}, []]]
+        ]
+    },
+    {
+        "name": "Example-DictHeader (boolean values)",
+        "raw": ["a=?0, b, c; foo=bar"],
+        "header_type": "dictionary",
+        "expected": [
+            ["a", [false, []]],
+            ["b", [true, []]],
+            ["c", [true, [["foo", {"__type": "token", "value": "bar"}]]]]
+        ],
+        "canonical": ["a=?0, b, c;foo=bar"]
+    },
+    {
+        "name": "Example-DictListHeader",
+        "raw": ["rating=1.5, feelings=(joy sadness)"],
+        "header_type": "dictionary",
+        "expected": [
+            ["rating", [1.5, []]],
+            ["feelings", [[
+                [{"__type": "token", "value": "joy"}, []],
+                [{"__type": "token", "value": "sadness"}, []]
+            ], []]]
+        ]
+    },
+    {
+        "name": "Example-MixDict",
+        "raw": ["a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid"],
+        "header_type": "dictionary",
+        "expected": [
+            ["a", [[
+                [1, []],
+                [2, []]
+            ], []]],
+            ["b", [3, []]],
+            ["c", [4, [["aa", {"__type": "token", "value": "bb"}]]]],
+            ["d", [[
+                [5, []],
+                [6, []]
+            ], [["valid", true]]]]
+        ],
+        "canonical": ["a=(1 2), b=3, c=4;aa=bb, d=(5 6);valid"]
+    },
+    {
+        "name": "Example-Hdr (dictionary on one line)",
+        "raw": ["foo=1, bar=2"],
+        "header_type": "dictionary",
+        "expected": [
+            ["foo", [1, []]],
+            ["bar", [2, []]]
+        ]
+    },
+    {
+        "name": "Example-Hdr (dictionary on two lines)",
+        "raw": ["foo=1", "bar=2"],
+        "header_type": "dictionary",
+        "expected": [
+            ["foo", [1, []]],
+            ["bar", [2, []]]
+        ],
+        "canonical": ["foo=1, bar=2"]
+    },
+
+    {
+        "name": "Example-IntItemHeader",
+        "raw": ["5"],
+        "header_type": "item",
+        "expected": [5, []]
+    },
+    {
+        "name": "Example-IntItemHeader (params)",
+        "raw": ["5; foo=bar"],
+        "header_type": "item",
+        "expected": [5, [["foo", {"__type": "token", "value": "bar"}]]],
+        "canonical": ["5;foo=bar"]
+    },
+    {
+        "name": "Example-IntegerHeader",
+        "raw": ["42"],
+        "header_type": "item",
+        "expected": [42, []]
+    },
+    {
+        "name": "Example-FloatHeader",
+        "raw": ["4.5"],
+        "header_type": "item",
+        "expected": [4.5, []]
+    },
+    {
+        "name": "Example-StringHeader",
+        "raw": ["\"hello world\""],
+        "header_type": "item",
+        "expected": ["hello world", []]
+    },
+    {
+        "name": "Example-BinaryHdr",
+        "raw": [":cHJldGVuZCB0aGlzIGlzIGJpbmFyeSBjb250ZW50Lg==:"],
+        "header_type": "item",
+        "expected": [{"__type": "binary", "value": "OBZGK5DFNZSCA5DINFZSA2LTEBRGS3TBOJ4SAY3PNZ2GK3TUFY======"}, []]
+    },
+    {
+        "name": "Example-BoolHdr",
+        "raw": ["?1"],
+        "header_type": "item",
+        "expected": [true, []]
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json
new file mode 100644
index 000000000..e86cea0e9
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/item.json
@@ -0,0 +1,34 @@
+[
+    {
+        "name": "empty item",
+        "raw": [""],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "leading space",
+        "raw": [" \t 1"],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "trailing space",
+        "raw": ["1 \t "],
+        "header_type": "item",
+        "must_fail": true
+    },
+    {
+        "name": "leading and trailing space",
+        "raw": ["  1  "],
+        "header_type": "item",
+        "expected": [1, []],
+        "canonical": ["1"]
+    },
+    {
+        "name": "leading and trailing whitespace",
+        "raw": ["     1  "],
+        "header_type": "item",
+        "expected": [1, []],
+        "canonical": ["1"]
+    }
+]
diff --git a/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json
new file mode 100644
index 000000000..efa67d963
--- /dev/null
+++ b/httpcore5/src/test/resources/org/apache/hc/core5/http/structured/rfc9651/key-generated.json
@@ -0,0 +1,7013 @@
+[
+    {
+        "name": "0x00 as a single-character dictionary key",
+        "raw": [
+            "\u0000=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x01 as a single-character dictionary key",
+        "raw": [
+            "\u0001=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x02 as a single-character dictionary key",
+        "raw": [
+            "\u0002=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x03 as a single-character dictionary key",
+        "raw": [
+            "\u0003=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x04 as a single-character dictionary key",
+        "raw": [
+            "\u0004=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x05 as a single-character dictionary key",
+        "raw": [
+            "\u0005=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x06 as a single-character dictionary key",
+        "raw": [
+            "\u0006=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x07 as a single-character dictionary key",
+        "raw": [
+            "\u0007=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x08 as a single-character dictionary key",
+        "raw": [
+            "\b=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x09 as a single-character dictionary key",
+        "raw": [
+            "\t=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0a as a single-character dictionary key",
+        "raw": [
+            "\n=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0b as a single-character dictionary key",
+        "raw": [
+            "\u000b=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0c as a single-character dictionary key",
+        "raw": [
+            "\f=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0d as a single-character dictionary key",
+        "raw": [
+            "\r=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0e as a single-character dictionary key",
+        "raw": [
+            "\u000e=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0f as a single-character dictionary key",
+        "raw": [
+            "\u000f=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x10 as a single-character dictionary key",
+        "raw": [
+            "\u0010=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x11 as a single-character dictionary key",
+        "raw": [
+            "\u0011=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x12 as a single-character dictionary key",
+        "raw": [
+            "\u0012=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x13 as a single-character dictionary key",
+        "raw": [
+            "\u0013=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x14 as a single-character dictionary key",
+        "raw": [
+            "\u0014=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x15 as a single-character dictionary key",
+        "raw": [
+            "\u0015=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x16 as a single-character dictionary key",
+        "raw": [
+            "\u0016=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x17 as a single-character dictionary key",
+        "raw": [
+            "\u0017=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x18 as a single-character dictionary key",
+        "raw": [
+            "\u0018=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x19 as a single-character dictionary key",
+        "raw": [
+            "\u0019=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1a as a single-character dictionary key",
+        "raw": [
+            "\u001a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1b as a single-character dictionary key",
+        "raw": [
+            "\u001b=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1c as a single-character dictionary key",
+        "raw": [
+            "\u001c=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1d as a single-character dictionary key",
+        "raw": [
+            "\u001d=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1e as a single-character dictionary key",
+        "raw": [
+            "\u001e=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1f as a single-character dictionary key",
+        "raw": [
+            "\u001f=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x20 as a single-character dictionary key",
+        "raw": [
+            "=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x21 as a single-character dictionary key",
+        "raw": [
+            "!=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x22 as a single-character dictionary key",
+        "raw": [
+            "\"=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x23 as a single-character dictionary key",
+        "raw": [
+            "#=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x24 as a single-character dictionary key",
+        "raw": [
+            "$=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x25 as a single-character dictionary key",
+        "raw": [
+            "%=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x26 as a single-character dictionary key",
+        "raw": [
+            "&=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x27 as a single-character dictionary key",
+        "raw": [
+            "'=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x28 as a single-character dictionary key",
+        "raw": [
+            "(=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x29 as a single-character dictionary key",
+        "raw": [
+            ")=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2a as a single-character dictionary key",
+        "raw": [
+            "*=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "*",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x2b as a single-character dictionary key",
+        "raw": [
+            "+=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2c as a single-character dictionary key",
+        "raw": [
+            ",=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2d as a single-character dictionary key",
+        "raw": [
+            "-=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2e as a single-character dictionary key",
+        "raw": [
+            ".=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2f as a single-character dictionary key",
+        "raw": [
+            "/=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x30 as a single-character dictionary key",
+        "raw": [
+            "0=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x31 as a single-character dictionary key",
+        "raw": [
+            "1=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x32 as a single-character dictionary key",
+        "raw": [
+            "2=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x33 as a single-character dictionary key",
+        "raw": [
+            "3=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x34 as a single-character dictionary key",
+        "raw": [
+            "4=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x35 as a single-character dictionary key",
+        "raw": [
+            "5=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x36 as a single-character dictionary key",
+        "raw": [
+            "6=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x37 as a single-character dictionary key",
+        "raw": [
+            "7=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x38 as a single-character dictionary key",
+        "raw": [
+            "8=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x39 as a single-character dictionary key",
+        "raw": [
+            "9=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3a as a single-character dictionary key",
+        "raw": [
+            ":=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3b as a single-character dictionary key",
+        "raw": [
+            ";=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3c as a single-character dictionary key",
+        "raw": [
+            "<=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3d as a single-character dictionary key",
+        "raw": [
+            "==1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3e as a single-character dictionary key",
+        "raw": [
+            ">=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3f as a single-character dictionary key",
+        "raw": [
+            "?=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x40 as a single-character dictionary key",
+        "raw": [
+            "@=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x41 as a single-character dictionary key",
+        "raw": [
+            "A=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x42 as a single-character dictionary key",
+        "raw": [
+            "B=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x43 as a single-character dictionary key",
+        "raw": [
+            "C=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x44 as a single-character dictionary key",
+        "raw": [
+            "D=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x45 as a single-character dictionary key",
+        "raw": [
+            "E=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x46 as a single-character dictionary key",
+        "raw": [
+            "F=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x47 as a single-character dictionary key",
+        "raw": [
+            "G=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x48 as a single-character dictionary key",
+        "raw": [
+            "H=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x49 as a single-character dictionary key",
+        "raw": [
+            "I=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4a as a single-character dictionary key",
+        "raw": [
+            "J=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4b as a single-character dictionary key",
+        "raw": [
+            "K=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4c as a single-character dictionary key",
+        "raw": [
+            "L=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4d as a single-character dictionary key",
+        "raw": [
+            "M=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4e as a single-character dictionary key",
+        "raw": [
+            "N=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4f as a single-character dictionary key",
+        "raw": [
+            "O=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x50 as a single-character dictionary key",
+        "raw": [
+            "P=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x51 as a single-character dictionary key",
+        "raw": [
+            "Q=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x52 as a single-character dictionary key",
+        "raw": [
+            "R=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x53 as a single-character dictionary key",
+        "raw": [
+            "S=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x54 as a single-character dictionary key",
+        "raw": [
+            "T=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x55 as a single-character dictionary key",
+        "raw": [
+            "U=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x56 as a single-character dictionary key",
+        "raw": [
+            "V=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x57 as a single-character dictionary key",
+        "raw": [
+            "W=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x58 as a single-character dictionary key",
+        "raw": [
+            "X=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x59 as a single-character dictionary key",
+        "raw": [
+            "Y=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5a as a single-character dictionary key",
+        "raw": [
+            "Z=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5b as a single-character dictionary key",
+        "raw": [
+            "[=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5c as a single-character dictionary key",
+        "raw": [
+            "\\=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5d as a single-character dictionary key",
+        "raw": [
+            "]=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5e as a single-character dictionary key",
+        "raw": [
+            "^=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5f as a single-character dictionary key",
+        "raw": [
+            "_=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x60 as a single-character dictionary key",
+        "raw": [
+            "`=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x61 as a single-character dictionary key",
+        "raw": [
+            "a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x62 as a single-character dictionary key",
+        "raw": [
+            "b=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "b",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x63 as a single-character dictionary key",
+        "raw": [
+            "c=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "c",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x64 as a single-character dictionary key",
+        "raw": [
+            "d=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "d",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x65 as a single-character dictionary key",
+        "raw": [
+            "e=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "e",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x66 as a single-character dictionary key",
+        "raw": [
+            "f=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "f",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x67 as a single-character dictionary key",
+        "raw": [
+            "g=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "g",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x68 as a single-character dictionary key",
+        "raw": [
+            "h=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "h",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x69 as a single-character dictionary key",
+        "raw": [
+            "i=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "i",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6a as a single-character dictionary key",
+        "raw": [
+            "j=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "j",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6b as a single-character dictionary key",
+        "raw": [
+            "k=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "k",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6c as a single-character dictionary key",
+        "raw": [
+            "l=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "l",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6d as a single-character dictionary key",
+        "raw": [
+            "m=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "m",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6e as a single-character dictionary key",
+        "raw": [
+            "n=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "n",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6f as a single-character dictionary key",
+        "raw": [
+            "o=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "o",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x70 as a single-character dictionary key",
+        "raw": [
+            "p=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "p",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x71 as a single-character dictionary key",
+        "raw": [
+            "q=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "q",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x72 as a single-character dictionary key",
+        "raw": [
+            "r=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "r",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x73 as a single-character dictionary key",
+        "raw": [
+            "s=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "s",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x74 as a single-character dictionary key",
+        "raw": [
+            "t=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "t",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x75 as a single-character dictionary key",
+        "raw": [
+            "u=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "u",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x76 as a single-character dictionary key",
+        "raw": [
+            "v=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "v",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x77 as a single-character dictionary key",
+        "raw": [
+            "w=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "w",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x78 as a single-character dictionary key",
+        "raw": [
+            "x=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "x",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x79 as a single-character dictionary key",
+        "raw": [
+            "y=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "y",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7a as a single-character dictionary key",
+        "raw": [
+            "z=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "z",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7b as a single-character dictionary key",
+        "raw": [
+            "{=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7c as a single-character dictionary key",
+        "raw": [
+            "|=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7d as a single-character dictionary key",
+        "raw": [
+            "}=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7e as a single-character dictionary key",
+        "raw": [
+            "~=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7f as a single-character dictionary key",
+        "raw": [
+            "\u007f=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x00 in dictionary key",
+        "raw": [
+            "a\u0000a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x01 in dictionary key",
+        "raw": [
+            "a\u0001a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x02 in dictionary key",
+        "raw": [
+            "a\u0002a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x03 in dictionary key",
+        "raw": [
+            "a\u0003a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x04 in dictionary key",
+        "raw": [
+            "a\u0004a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x05 in dictionary key",
+        "raw": [
+            "a\u0005a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x06 in dictionary key",
+        "raw": [
+            "a\u0006a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x07 in dictionary key",
+        "raw": [
+            "a\u0007a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x08 in dictionary key",
+        "raw": [
+            "a\ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x09 in dictionary key",
+        "raw": [
+            "a\ta=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0a in dictionary key",
+        "raw": [
+            "a\na=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0b in dictionary key",
+        "raw": [
+            "a\u000ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0c in dictionary key",
+        "raw": [
+            "a\fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0d in dictionary key",
+        "raw": [
+            "a\ra=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0e in dictionary key",
+        "raw": [
+            "a\u000ea=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0f in dictionary key",
+        "raw": [
+            "a\u000fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x10 in dictionary key",
+        "raw": [
+            "a\u0010a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x11 in dictionary key",
+        "raw": [
+            "a\u0011a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x12 in dictionary key",
+        "raw": [
+            "a\u0012a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x13 in dictionary key",
+        "raw": [
+            "a\u0013a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x14 in dictionary key",
+        "raw": [
+            "a\u0014a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x15 in dictionary key",
+        "raw": [
+            "a\u0015a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x16 in dictionary key",
+        "raw": [
+            "a\u0016a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x17 in dictionary key",
+        "raw": [
+            "a\u0017a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x18 in dictionary key",
+        "raw": [
+            "a\u0018a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x19 in dictionary key",
+        "raw": [
+            "a\u0019a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1a in dictionary key",
+        "raw": [
+            "a\u001aa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1b in dictionary key",
+        "raw": [
+            "a\u001ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1c in dictionary key",
+        "raw": [
+            "a\u001ca=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1d in dictionary key",
+        "raw": [
+            "a\u001da=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1e in dictionary key",
+        "raw": [
+            "a\u001ea=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1f in dictionary key",
+        "raw": [
+            "a\u001fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x20 in dictionary key",
+        "raw": [
+            "a a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x21 in dictionary key",
+        "raw": [
+            "a!a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x22 in dictionary key",
+        "raw": [
+            "a\"a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x23 in dictionary key",
+        "raw": [
+            "a#a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x24 in dictionary key",
+        "raw": [
+            "a$a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x25 in dictionary key",
+        "raw": [
+            "a%a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x26 in dictionary key",
+        "raw": [
+            "a&a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x27 in dictionary key",
+        "raw": [
+            "a'a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x28 in dictionary key",
+        "raw": [
+            "a(a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x29 in dictionary key",
+        "raw": [
+            "a)a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2a in dictionary key",
+        "raw": [
+            "a*a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a*a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x2b in dictionary key",
+        "raw": [
+            "a+a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2c in dictionary key",
+        "raw": [
+            "a,a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ],
+        "canonical": [
+            "a=1"
+        ]
+    },
+    {
+        "name": "0x2d in dictionary key",
+        "raw": [
+            "a-a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a-a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x2e in dictionary key",
+        "raw": [
+            "a.a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a.a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x2f in dictionary key",
+        "raw": [
+            "a/a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x30 in dictionary key",
+        "raw": [
+            "a0a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a0a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x31 in dictionary key",
+        "raw": [
+            "a1a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a1a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x32 in dictionary key",
+        "raw": [
+            "a2a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a2a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x33 in dictionary key",
+        "raw": [
+            "a3a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a3a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x34 in dictionary key",
+        "raw": [
+            "a4a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a4a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x35 in dictionary key",
+        "raw": [
+            "a5a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a5a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x36 in dictionary key",
+        "raw": [
+            "a6a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a6a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x37 in dictionary key",
+        "raw": [
+            "a7a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a7a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x38 in dictionary key",
+        "raw": [
+            "a8a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a8a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x39 in dictionary key",
+        "raw": [
+            "a9a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a9a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x3a in dictionary key",
+        "raw": [
+            "a:a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3b in dictionary key",
+        "raw": [
+            "a;a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a",
+                [
+                    true,
+                    [
+                        [
+                            "a",
+                            1
+                        ]
+                    ]
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x3c in dictionary key",
+        "raw": [
+            "a<a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3d in dictionary key",
+        "raw": [
+            "a=a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3e in dictionary key",
+        "raw": [
+            "a>a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3f in dictionary key",
+        "raw": [
+            "a?a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x40 in dictionary key",
+        "raw": [
+            "a@a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x41 in dictionary key",
+        "raw": [
+            "aAa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x42 in dictionary key",
+        "raw": [
+            "aBa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x43 in dictionary key",
+        "raw": [
+            "aCa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x44 in dictionary key",
+        "raw": [
+            "aDa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x45 in dictionary key",
+        "raw": [
+            "aEa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x46 in dictionary key",
+        "raw": [
+            "aFa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x47 in dictionary key",
+        "raw": [
+            "aGa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x48 in dictionary key",
+        "raw": [
+            "aHa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x49 in dictionary key",
+        "raw": [
+            "aIa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4a in dictionary key",
+        "raw": [
+            "aJa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4b in dictionary key",
+        "raw": [
+            "aKa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4c in dictionary key",
+        "raw": [
+            "aLa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4d in dictionary key",
+        "raw": [
+            "aMa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4e in dictionary key",
+        "raw": [
+            "aNa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4f in dictionary key",
+        "raw": [
+            "aOa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x50 in dictionary key",
+        "raw": [
+            "aPa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x51 in dictionary key",
+        "raw": [
+            "aQa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x52 in dictionary key",
+        "raw": [
+            "aRa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x53 in dictionary key",
+        "raw": [
+            "aSa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x54 in dictionary key",
+        "raw": [
+            "aTa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x55 in dictionary key",
+        "raw": [
+            "aUa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x56 in dictionary key",
+        "raw": [
+            "aVa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x57 in dictionary key",
+        "raw": [
+            "aWa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x58 in dictionary key",
+        "raw": [
+            "aXa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x59 in dictionary key",
+        "raw": [
+            "aYa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5a in dictionary key",
+        "raw": [
+            "aZa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5b in dictionary key",
+        "raw": [
+            "a[a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5c in dictionary key",
+        "raw": [
+            "a\\a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5d in dictionary key",
+        "raw": [
+            "a]a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5e in dictionary key",
+        "raw": [
+            "a^a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5f in dictionary key",
+        "raw": [
+            "a_a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a_a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x60 in dictionary key",
+        "raw": [
+            "a`a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x61 in dictionary key",
+        "raw": [
+            "aaa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aaa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x62 in dictionary key",
+        "raw": [
+            "aba=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aba",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x63 in dictionary key",
+        "raw": [
+            "aca=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aca",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x64 in dictionary key",
+        "raw": [
+            "ada=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ada",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x65 in dictionary key",
+        "raw": [
+            "aea=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aea",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x66 in dictionary key",
+        "raw": [
+            "afa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "afa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x67 in dictionary key",
+        "raw": [
+            "aga=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aga",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x68 in dictionary key",
+        "raw": [
+            "aha=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aha",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x69 in dictionary key",
+        "raw": [
+            "aia=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aia",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6a in dictionary key",
+        "raw": [
+            "aja=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aja",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6b in dictionary key",
+        "raw": [
+            "aka=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aka",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6c in dictionary key",
+        "raw": [
+            "ala=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ala",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6d in dictionary key",
+        "raw": [
+            "ama=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ama",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6e in dictionary key",
+        "raw": [
+            "ana=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ana",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6f in dictionary key",
+        "raw": [
+            "aoa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aoa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x70 in dictionary key",
+        "raw": [
+            "apa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "apa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x71 in dictionary key",
+        "raw": [
+            "aqa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aqa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x72 in dictionary key",
+        "raw": [
+            "ara=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ara",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x73 in dictionary key",
+        "raw": [
+            "asa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "asa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x74 in dictionary key",
+        "raw": [
+            "ata=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ata",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x75 in dictionary key",
+        "raw": [
+            "aua=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aua",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x76 in dictionary key",
+        "raw": [
+            "ava=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ava",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x77 in dictionary key",
+        "raw": [
+            "awa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "awa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x78 in dictionary key",
+        "raw": [
+            "axa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "axa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x79 in dictionary key",
+        "raw": [
+            "aya=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aya",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7a in dictionary key",
+        "raw": [
+            "aza=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aza",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7b in dictionary key",
+        "raw": [
+            "a{a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7c in dictionary key",
+        "raw": [
+            "a|a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7d in dictionary key",
+        "raw": [
+            "a}a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7e in dictionary key",
+        "raw": [
+            "a~a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7f in dictionary key",
+        "raw": [
+            "a\u007fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x00 starting a dictionary key",
+        "raw": [
+            "\u0000a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x01 starting a dictionary key",
+        "raw": [
+            "\u0001a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x02 starting a dictionary key",
+        "raw": [
+            "\u0002a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x03 starting a dictionary key",
+        "raw": [
+            "\u0003a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x04 starting a dictionary key",
+        "raw": [
+            "\u0004a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x05 starting a dictionary key",
+        "raw": [
+            "\u0005a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x06 starting a dictionary key",
+        "raw": [
+            "\u0006a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x07 starting a dictionary key",
+        "raw": [
+            "\u0007a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x08 starting a dictionary key",
+        "raw": [
+            "\ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x09 starting a dictionary key",
+        "raw": [
+            "\ta=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0a starting a dictionary key",
+        "raw": [
+            "\na=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0b starting a dictionary key",
+        "raw": [
+            "\u000ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0c starting a dictionary key",
+        "raw": [
+            "\fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0d starting a dictionary key",
+        "raw": [
+            "\ra=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0e starting a dictionary key",
+        "raw": [
+            "\u000ea=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x0f starting a dictionary key",
+        "raw": [
+            "\u000fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x10 starting a dictionary key",
+        "raw": [
+            "\u0010a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x11 starting a dictionary key",
+        "raw": [
+            "\u0011a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x12 starting a dictionary key",
+        "raw": [
+            "\u0012a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x13 starting a dictionary key",
+        "raw": [
+            "\u0013a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x14 starting a dictionary key",
+        "raw": [
+            "\u0014a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x15 starting a dictionary key",
+        "raw": [
+            "\u0015a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x16 starting a dictionary key",
+        "raw": [
+            "\u0016a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x17 starting a dictionary key",
+        "raw": [
+            "\u0017a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x18 starting a dictionary key",
+        "raw": [
+            "\u0018a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x19 starting a dictionary key",
+        "raw": [
+            "\u0019a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1a starting a dictionary key",
+        "raw": [
+            "\u001aa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1b starting a dictionary key",
+        "raw": [
+            "\u001ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1c starting a dictionary key",
+        "raw": [
+            "\u001ca=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1d starting a dictionary key",
+        "raw": [
+            "\u001da=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1e starting a dictionary key",
+        "raw": [
+            "\u001ea=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x1f starting a dictionary key",
+        "raw": [
+            "\u001fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x20 starting a dictionary key",
+        "raw": [
+            " a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ],
+        "canonical": [
+            "a=1"
+        ]
+    },
+    {
+        "name": "0x21 starting a dictionary key",
+        "raw": [
+            "!a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x22 starting a dictionary key",
+        "raw": [
+            "\"a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x23 starting a dictionary key",
+        "raw": [
+            "#a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x24 starting a dictionary key",
+        "raw": [
+            "$a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x25 starting a dictionary key",
+        "raw": [
+            "%a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x26 starting a dictionary key",
+        "raw": [
+            "&a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x27 starting a dictionary key",
+        "raw": [
+            "'a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x28 starting a dictionary key",
+        "raw": [
+            "(a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x29 starting a dictionary key",
+        "raw": [
+            ")a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2a starting a dictionary key",
+        "raw": [
+            "*a=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "*a",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x2b starting a dictionary key",
+        "raw": [
+            "+a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2c starting a dictionary key",
+        "raw": [
+            ",a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2d starting a dictionary key",
+        "raw": [
+            "-a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2e starting a dictionary key",
+        "raw": [
+            ".a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x2f starting a dictionary key",
+        "raw": [
+            "/a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x30 starting a dictionary key",
+        "raw": [
+            "0a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x31 starting a dictionary key",
+        "raw": [
+            "1a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x32 starting a dictionary key",
+        "raw": [
+            "2a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x33 starting a dictionary key",
+        "raw": [
+            "3a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x34 starting a dictionary key",
+        "raw": [
+            "4a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x35 starting a dictionary key",
+        "raw": [
+            "5a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x36 starting a dictionary key",
+        "raw": [
+            "6a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x37 starting a dictionary key",
+        "raw": [
+            "7a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x38 starting a dictionary key",
+        "raw": [
+            "8a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x39 starting a dictionary key",
+        "raw": [
+            "9a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3a starting a dictionary key",
+        "raw": [
+            ":a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3b starting a dictionary key",
+        "raw": [
+            ";a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3c starting a dictionary key",
+        "raw": [
+            "<a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3d starting a dictionary key",
+        "raw": [
+            "=a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3e starting a dictionary key",
+        "raw": [
+            ">a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x3f starting a dictionary key",
+        "raw": [
+            "?a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x40 starting a dictionary key",
+        "raw": [
+            "@a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x41 starting a dictionary key",
+        "raw": [
+            "Aa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x42 starting a dictionary key",
+        "raw": [
+            "Ba=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x43 starting a dictionary key",
+        "raw": [
+            "Ca=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x44 starting a dictionary key",
+        "raw": [
+            "Da=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x45 starting a dictionary key",
+        "raw": [
+            "Ea=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x46 starting a dictionary key",
+        "raw": [
+            "Fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x47 starting a dictionary key",
+        "raw": [
+            "Ga=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x48 starting a dictionary key",
+        "raw": [
+            "Ha=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x49 starting a dictionary key",
+        "raw": [
+            "Ia=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4a starting a dictionary key",
+        "raw": [
+            "Ja=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4b starting a dictionary key",
+        "raw": [
+            "Ka=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4c starting a dictionary key",
+        "raw": [
+            "La=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4d starting a dictionary key",
+        "raw": [
+            "Ma=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4e starting a dictionary key",
+        "raw": [
+            "Na=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x4f starting a dictionary key",
+        "raw": [
+            "Oa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x50 starting a dictionary key",
+        "raw": [
+            "Pa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x51 starting a dictionary key",
+        "raw": [
+            "Qa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x52 starting a dictionary key",
+        "raw": [
+            "Ra=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x53 starting a dictionary key",
+        "raw": [
+            "Sa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x54 starting a dictionary key",
+        "raw": [
+            "Ta=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x55 starting a dictionary key",
+        "raw": [
+            "Ua=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x56 starting a dictionary key",
+        "raw": [
+            "Va=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x57 starting a dictionary key",
+        "raw": [
+            "Wa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x58 starting a dictionary key",
+        "raw": [
+            "Xa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x59 starting a dictionary key",
+        "raw": [
+            "Ya=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5a starting a dictionary key",
+        "raw": [
+            "Za=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5b starting a dictionary key",
+        "raw": [
+            "[a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5c starting a dictionary key",
+        "raw": [
+            "\\a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5d starting a dictionary key",
+        "raw": [
+            "]a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5e starting a dictionary key",
+        "raw": [
+            "^a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x5f starting a dictionary key",
+        "raw": [
+            "_a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x60 starting a dictionary key",
+        "raw": [
+            "`a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x61 starting a dictionary key",
+        "raw": [
+            "aa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "aa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x62 starting a dictionary key",
+        "raw": [
+            "ba=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ba",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x63 starting a dictionary key",
+        "raw": [
+            "ca=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ca",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x64 starting a dictionary key",
+        "raw": [
+            "da=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "da",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x65 starting a dictionary key",
+        "raw": [
+            "ea=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ea",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x66 starting a dictionary key",
+        "raw": [
+            "fa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "fa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x67 starting a dictionary key",
+        "raw": [
+            "ga=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ga",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x68 starting a dictionary key",
+        "raw": [
+            "ha=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ha",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x69 starting a dictionary key",
+        "raw": [
+            "ia=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ia",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6a starting a dictionary key",
+        "raw": [
+            "ja=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ja",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6b starting a dictionary key",
+        "raw": [
+            "ka=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ka",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6c starting a dictionary key",
+        "raw": [
+            "la=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "la",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6d starting a dictionary key",
+        "raw": [
+            "ma=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ma",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6e starting a dictionary key",
+        "raw": [
+            "na=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "na",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x6f starting a dictionary key",
+        "raw": [
+            "oa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "oa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x70 starting a dictionary key",
+        "raw": [
+            "pa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "pa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x71 starting a dictionary key",
+        "raw": [
+            "qa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "qa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x72 starting a dictionary key",
+        "raw": [
+            "ra=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ra",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x73 starting a dictionary key",
+        "raw": [
+            "sa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "sa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x74 starting a dictionary key",
+        "raw": [
+            "ta=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ta",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x75 starting a dictionary key",
+        "raw": [
+            "ua=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ua",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x76 starting a dictionary key",
+        "raw": [
+            "va=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "va",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x77 starting a dictionary key",
+        "raw": [
+            "wa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "wa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x78 starting a dictionary key",
+        "raw": [
+            "xa=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "xa",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x79 starting a dictionary key",
+        "raw": [
+            "ya=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "ya",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7a starting a dictionary key",
+        "raw": [
+            "za=1"
+        ],
+        "header_type": "dictionary",
+        "expected": [
+            [
+                "za",
+                [
+                    1,
+                    []
+                ]
+            ]
+        ]
+    },
+    {
+        "name": "0x7b starting a dictionary key",
+        "raw": [
+            "{a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7c starting a dictionary key",
+        "raw": [
+            "|a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7d starting a dictionary key",
+        "raw": [
+            "}a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7e starting a dictionary key",
+        "raw": [
+            "~a=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x7f starting a dictionary key",
+        "raw": [
+            "\u007fa=1"
+        ],
+        "header_type": "dictionary",
+        "must_fail": true
+    },
+    {
+        "name": "0x00 in parameterised list key",
+        "raw": [
+            "foo; a\u0000a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x01 in parameterised list key",
+        "raw": [
+            "foo; a\u0001a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x02 in parameterised list key",
+        "raw": [
+            "foo; a\u0002a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x03 in parameterised list key",
+        "raw": [
+            "foo; a\u0003a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x04 in parameterised list key",
+        "raw": [
+            "foo; a\u0004a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x05 in parameterised list key",
+        "raw": [
+            "foo; a\u0005a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x06 in parameterised list key",
+        "raw": [
+            "foo; a\u0006a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x07 in parameterised list key",
+        "raw": [
+            "foo; a\u0007a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x08 in parameterised list key",
+        "raw": [
+            "foo; a\ba=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x09 in parameterised list key",
+        "raw": [
+            "foo; a\ta=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0a in parameterised list key",
+        "raw": [
+            "foo; a\na=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0b in parameterised list key",
+        "raw": [
+            "foo; a\u000ba=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0c in parameterised list key",
+        "raw": [
+            "foo; a\fa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0d in parameterised list key",
+        "raw": [
+            "foo; a\ra=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0e in parameterised list key",
+        "raw": [
+            "foo; a\u000ea=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x0f in parameterised list key",
+        "raw": [
+            "foo; a\u000fa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x10 in parameterised list key",
+        "raw": [
+            "foo; a\u0010a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x11 in parameterised list key",
+        "raw": [
+            "foo; a\u0011a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x12 in parameterised list key",
+        "raw": [
+            "foo; a\u0012a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x13 in parameterised list key",
+        "raw": [
+            "foo; a\u0013a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x14 in parameterised list key",
+        "raw": [
+            "foo; a\u0014a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x15 in parameterised list key",
+        "raw": [
+            "foo; a\u0015a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x16 in parameterised list key",
+        "raw": [
+            "foo; a\u0016a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x17 in parameterised list key",
+        "raw": [
+            "foo; a\u0017a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x18 in parameterised list key",
+        "raw": [
+            "foo; a\u0018a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x19 in parameterised list key",
+        "raw": [
+            "foo; a\u0019a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1a in parameterised list key",
+        "raw": [
+            "foo; a\u001aa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1b in parameterised list key",
+        "raw": [
+            "foo; a\u001ba=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1c in parameterised list key",
+        "raw": [
+            "foo; a\u001ca=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1d in parameterised list key",
+        "raw": [
+            "foo; a\u001da=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1e in parameterised list key",
+        "raw": [
+            "foo; a\u001ea=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x1f in parameterised list key",
+        "raw": [
+            "foo; a\u001fa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x20 in parameterised list key",
+        "raw": [
+            "foo; a a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x21 in parameterised list key",
+        "raw": [
+            "foo; a!a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x22 in parameterised list key",
+        "raw": [
+            "foo; a\"a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x23 in parameterised list key",
+        "raw": [
+            "foo; a#a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x24 in parameterised list key",
+        "raw": [
+            "foo; a$a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x25 in parameterised list key",
+        "raw": [
+            "foo; a%a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x26 in parameterised list key",
+        "raw": [
+            "foo; a&a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x27 in parameterised list key",
+        "raw": [
+            "foo; a'a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x28 in parameterised list key",
+        "raw": [
+            "foo; a(a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x29 in parameterised list key",
+        "raw": [
+            "foo; a)a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x2a in parameterised list key",
+        "raw": [
+            "foo; a*a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a*a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a*a=1"
+        ]
+    },
+    {
+        "name": "0x2b in parameterised list key",
+        "raw": [
+            "foo; a+a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x2c in parameterised list key",
+        "raw": [
+            "foo; a,a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x2d in parameterised list key",
+        "raw": [
+            "foo; a-a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a-a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a-a=1"
+        ]
+    },
+    {
+        "name": "0x2e in parameterised list key",
+        "raw": [
+            "foo; a.a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a.a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a.a=1"
+        ]
+    },
+    {
+        "name": "0x2f in parameterised list key",
+        "raw": [
+            "foo; a/a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x30 in parameterised list key",
+        "raw": [
+            "foo; a0a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a0a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a0a=1"
+        ]
+    },
+    {
+        "name": "0x31 in parameterised list key",
+        "raw": [
+            "foo; a1a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a1a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a1a=1"
+        ]
+    },
+    {
+        "name": "0x32 in parameterised list key",
+        "raw": [
+            "foo; a2a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a2a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a2a=1"
+        ]
+    },
+    {
+        "name": "0x33 in parameterised list key",
+        "raw": [
+            "foo; a3a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a3a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a3a=1"
+        ]
+    },
+    {
+        "name": "0x34 in parameterised list key",
+        "raw": [
+            "foo; a4a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a4a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a4a=1"
+        ]
+    },
+    {
+        "name": "0x35 in parameterised list key",
+        "raw": [
+            "foo; a5a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a5a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a5a=1"
+        ]
+    },
+    {
+        "name": "0x36 in parameterised list key",
+        "raw": [
+            "foo; a6a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a6a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a6a=1"
+        ]
+    },
+    {
+        "name": "0x37 in parameterised list key",
+        "raw": [
+            "foo; a7a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a7a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a7a=1"
+        ]
+    },
+    {
+        "name": "0x38 in parameterised list key",
+        "raw": [
+            "foo; a8a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a8a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a8a=1"
+        ]
+    },
+    {
+        "name": "0x39 in parameterised list key",
+        "raw": [
+            "foo; a9a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a9a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a9a=1"
+        ]
+    },
+    {
+        "name": "0x3a in parameterised list key",
+        "raw": [
+            "foo; a:a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x3b in parameterised list key",
+        "raw": [
+            "foo; a;a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a=1"
+        ]
+    },
+    {
+        "name": "0x3c in parameterised list key",
+        "raw": [
+            "foo; a<a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x3d in parameterised list key",
+        "raw": [
+            "foo; a=a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x3e in parameterised list key",
+        "raw": [
+            "foo; a>a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x3f in parameterised list key",
+        "raw": [
+            "foo; a?a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x40 in parameterised list key",
+        "raw": [
+            "foo; a@a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x41 in parameterised list key",
+        "raw": [
+            "foo; aAa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x42 in parameterised list key",
+        "raw": [
+            "foo; aBa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x43 in parameterised list key",
+        "raw": [
+            "foo; aCa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x44 in parameterised list key",
+        "raw": [
+            "foo; aDa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x45 in parameterised list key",
+        "raw": [
+            "foo; aEa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x46 in parameterised list key",
+        "raw": [
+            "foo; aFa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x47 in parameterised list key",
+        "raw": [
+            "foo; aGa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x48 in parameterised list key",
+        "raw": [
+            "foo; aHa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x49 in parameterised list key",
+        "raw": [
+            "foo; aIa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4a in parameterised list key",
+        "raw": [
+            "foo; aJa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4b in parameterised list key",
+        "raw": [
+            "foo; aKa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4c in parameterised list key",
+        "raw": [
+            "foo; aLa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4d in parameterised list key",
+        "raw": [
+            "foo; aMa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4e in parameterised list key",
+        "raw": [
+            "foo; aNa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x4f in parameterised list key",
+        "raw": [
+            "foo; aOa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x50 in parameterised list key",
+        "raw": [
+            "foo; aPa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x51 in parameterised list key",
+        "raw": [
+            "foo; aQa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x52 in parameterised list key",
+        "raw": [
+            "foo; aRa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x53 in parameterised list key",
+        "raw": [
+            "foo; aSa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x54 in parameterised list key",
+        "raw": [
+            "foo; aTa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x55 in parameterised list key",
+        "raw": [
+            "foo; aUa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x56 in parameterised list key",
+        "raw": [
+            "foo; aVa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x57 in parameterised list key",
+        "raw": [
+            "foo; aWa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x58 in parameterised list key",
+        "raw": [
+            "foo; aXa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x59 in parameterised list key",
+        "raw": [
+            "foo; aYa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5a in parameterised list key",
+        "raw": [
+            "foo; aZa=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5b in parameterised list key",
+        "raw": [
+            "foo; a[a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5c in parameterised list key",
+        "raw": [
+            "foo; a\\a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5d in parameterised list key",
+        "raw": [
+            "foo; a]a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5e in parameterised list key",
+        "raw": [
+            "foo; a^a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x5f in parameterised list key",
+        "raw": [
+            "foo; a_a=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "a_a",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;a_a=1"
+        ]
+    },
+    {
+        "name": "0x60 in parameterised list key",
+        "raw": [
+            "foo; a`a=1"
+        ],
+        "header_type": "list",
+        "must_fail": true
+    },
+    {
+        "name": "0x61 in parameterised list key",
+        "raw": [
+            "foo; aaa=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aaa",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aaa=1"
+        ]
+    },
+    {
+        "name": "0x62 in parameterised list key",
+        "raw": [
+            "foo; aba=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aba",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aba=1"
+        ]
+    },
+    {
+        "name": "0x63 in parameterised list key",
+        "raw": [
+            "foo; aca=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aca",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aca=1"
+        ]
+    },
+    {
+        "name": "0x64 in parameterised list key",
+        "raw": [
+            "foo; ada=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "ada",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;ada=1"
+        ]
+    },
+    {
+        "name": "0x65 in parameterised list key",
+        "raw": [
+            "foo; aea=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aea",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aea=1"
+        ]
+    },
+    {
+        "name": "0x66 in parameterised list key",
+        "raw": [
+            "foo; afa=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "afa",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;afa=1"
+        ]
+    },
+    {
+        "name": "0x67 in parameterised list key",
+        "raw": [
+            "foo; aga=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aga",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aga=1"
+        ]
+    },
+    {
+        "name": "0x68 in parameterised list key",
+        "raw": [
+            "foo; aha=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aha",
+                        1
+                    ]
+                ]
+            ]
+        ],
+        "canonical": [
+            "foo;aha=1"
+        ]
+    },
+    {
+        "name": "0x69 in parameterised list key",
+        "raw": [
+            "foo; aia=1"
+        ],
+        "header_type": "list",
+        "expected": [
+            [
+                {
+                    "__type": "token",
+                    "value": "foo"
+                },
+                [
+                    [
+                        "aia",
+                        1
+                    ]
... 47338 lines suppressed ...