Skip to content

Commit

Permalink
codec-http2: Lazily translate cookies for HTTP/1 (#9251)
Browse files Browse the repository at this point in the history
Motivation:

For HTTP/2 messages with multiple cookies HttpConversionUtil.addHttp2ToHttpHeaders spends a good portion of time creating throwaway StringBuilders.

Modification:

Handle cookies lazily by using a ThreadLocal StringBuilder and then converting it to the H1 header at the end.

Result:

Less allocations.
  • Loading branch information
kevinoliver authored and normanmaurer committed Jun 19, 2019
1 parent 01cfd78 commit c32c9b4
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 24 deletions.
Expand Up @@ -33,6 +33,7 @@
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AsciiString;
import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.UnstableApi;

import java.net.URI;
Expand Down Expand Up @@ -360,9 +361,7 @@ public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders
HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
try {
for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
translator.translate(entry);
}
translator.translateHeaders(inputHeaders);
} catch (Http2Exception ex) {
throw ex;
} catch (Throwable t) {
Expand Down Expand Up @@ -620,29 +619,43 @@ private static final class Http2ToHttpHeaderTranslator {
translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
}

public void translate(Entry<CharSequence, CharSequence> entry) throws Http2Exception {
final CharSequence name = entry.getKey();
final CharSequence value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null) {
output.add(translatedName, AsciiString.of(value));
} else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (name.length() == 0 || name.charAt(0) == ':') {
throw streamError(streamId, PROTOCOL_ERROR,
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
}
if (COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
String existingCookie = output.get(COOKIE);
output.set(COOKIE,
(existingCookie != null) ? (existingCookie + "; " + value) : value);
} else {
output.add(name, value);
public void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
// lazily created as needed
StringBuilder cookies = null;

for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
final CharSequence name = entry.getKey();
final CharSequence value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null) {
output.add(translatedName, AsciiString.of(value));
} else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (name.length() == 0 || name.charAt(0) == ':') {
throw streamError(streamId, PROTOCOL_ERROR,
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
}
if (COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
if (cookies == null) {
cookies = InternalThreadLocalMap.get().stringBuilder();
} else if (cookies.length() > 0) {
cookies.append("; ");
}
cookies.append(value);
} else {
output.add(name, value);
}
}
}
if (cookies != null) {
output.add(COOKIE, cookies.toString());
}
}

private void translateHeader(Entry<CharSequence, CharSequence> entry) throws Http2Exception {
}
}
}
Expand Up @@ -17,10 +17,12 @@

import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AsciiString;
import org.junit.Test;

import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
import static io.netty.handler.codec.http.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
Expand Down Expand Up @@ -143,4 +145,18 @@ public void stripConnectionNomineesWithCsv() {
assertEquals(1, out.size());
assertSame("world", out.get("hello"));
}

@Test
public void addHttp2ToHttpHeadersCombinesCookies() throws Http2Exception {
Http2Headers inHeaders = new DefaultHttp2Headers();
inHeaders.add("yes", "no");
inHeaders.add(COOKIE, "foo=bar");
inHeaders.add(COOKIE, "bax=baz");

HttpHeaders outHeaders = new DefaultHttpHeaders();

HttpConversionUtil.addHttp2ToHttpHeaders(5, inHeaders, outHeaders, HttpVersion.HTTP_1_1, false, false);
assertEquals("no", outHeaders.get("yes"));
assertEquals("foo=bar; bax=baz", outHeaders.get(COOKIE.toString()));
}
}

0 comments on commit c32c9b4

Please sign in to comment.