Repository: netty/netty
File: codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java (Java)

Code before:
        CharSequenceMap<AsciiString> connectionBlacklist =
            toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
        while (iter.hasNext()) {
            Entry<CharSequence, CharSequence> entry = iter.next();
            final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
            if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
                // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
                if (aName.contentEqualsIgnoreCase(TE)) {
                    toHttp2HeadersFilterTE(entry, out);
                } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
                    AsciiString value = AsciiString.of(entry.getValue());
                    // split up cookies to allow for better compression
                    // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
                    try {
                        int index = value.forEachByte(FIND_SEMI_COLON);
                        if (index != -1) {
                            int start = 0;
                            do {
                                out.add(COOKIE, value.subSequence(start, index, false));
                                // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1)
                                if (index + 1 >= value.length() || value.charAt(index + 1) != ' ') {
                                    throw new IllegalArgumentException(
                                            "cookie value has a semicolon not followed by a space, counter to spec: "
                                                    + value);
                                }
                                start = index + 2;
                            } while (start < value.length() &&
                                    (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1);
                            if (start >= value.length()) {
                                throw new IllegalArgumentException("cookie value is of unexpected format: " + value);
                            }
                            out.add(COOKIE, value.subSequence(start, value.length(), false));
                        } else {
                            out.add(COOKIE, value);
                        }
                    } catch (IllegalArgumentException e) {
                        throw e;
                    } catch (Exception e) {
                        // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught
                        // because of the ByteProcessor interface.
                        throw new IllegalStateException(e);
                    }
                } else {
                    out.add(aName, entry.getValue());
                }
            }
        }
    }

    /**
     * Generate an HTTP/2 {code :path} from a URI in accordance with

Review comment (suggestion):
I think this method was not expected to throw exceptions at all so starting to do so now doesn't seem quite right. As you've noted the `SP` token isn't universally adhered to so it feels like the right thing to do is be lenient on what we accept and not require the space.
If we really want to adhere to the spec then it seems that if we can't pull apart the cookie header into multiple individual headers we should just not pull it apart at all and add the whole thing as-is. The only reason we pull it apart is for a HPACK optimization.

Code after:
        CharSequenceMap<AsciiString> connectionBlacklist =
            toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
        while (iter.hasNext()) {
            Entry<CharSequence, CharSequence> entry = iter.next();
            final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
            if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) {
                // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE
                if (aName.contentEqualsIgnoreCase(TE)) {
                    toHttp2HeadersFilterTE(entry, out);
                } else if (aName.contentEqualsIgnoreCase(COOKIE)) {
                    CharSequence valueCs = entry.getValue();
                    // validate
                    boolean invalid = false;
                    for (int i = 0; i < valueCs.length(); i++) {
                        char c = valueCs.charAt(i);
                        if (c == ';') {
                            if (i + 1 >= valueCs.length() || valueCs.charAt(i + 1) != ' ') {
                                // semicolon not followed by space. invalid, don't split
                                invalid = true;
                                break;
                            }
                            i++; // skip space
                        } else if (c > 255) {
                            // not ascii, don't split
                            invalid = true;
                            break;
                        }
                    }

                    if (invalid) {
                        out.add(COOKIE, valueCs);
                    } else {
                        splitValidCookieHeader(out, valueCs);
                    }
                } else {
                    out.add(aName, entry.getValue());
                }
            }
        }
    }

    private static void splitValidCookieHeader(Http2Headers out, CharSequence valueCs) {
        try {
            AsciiString value = AsciiString.of(valueCs);
            // split up cookies to allow for better compression
            // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
            int index = value.forEachByte(FIND_SEMI_COLON);
            if (index != -1) {
                int start = 0;
                do {
                    out.add(COOKIE, value.subSequence(start, index, false));