codec-http2: Lazily translate cookies for HTTP/1 (#9251)

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.
This commit is contained in:
Kevin Oliver 2019-06-19 02:03:49 -07:00 committed by Norman Maurer
parent e3be64fd9c
commit 750bd9f7b7
2 changed files with 53 additions and 24 deletions

View File

@ -33,6 +33,7 @@ import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.UnstableApi; import io.netty.util.internal.UnstableApi;
import java.net.URI; import java.net.URI;
@ -360,9 +361,7 @@ public final class HttpConversionUtil {
HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception { HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest); Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
try { try {
for (Entry<CharSequence, CharSequence> entry : inputHeaders) { translator.translateHeaders(inputHeaders);
translator.translate(entry);
}
} catch (Http2Exception ex) { } catch (Http2Exception ex) {
throw ex; throw ex;
} catch (Throwable t) { } catch (Throwable t) {
@ -620,29 +619,43 @@ public final class HttpConversionUtil {
translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
} }
public void translate(Entry<CharSequence, CharSequence> entry) throws Http2Exception { public void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
final CharSequence name = entry.getKey(); // lazily created as needed
final CharSequence value = entry.getValue(); StringBuilder cookies = null;
AsciiString translatedName = translations.get(name);
if (translatedName != null) { for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
output.add(translatedName, AsciiString.of(value)); final CharSequence name = entry.getKey();
} else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { final CharSequence value = entry.getValue();
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3 AsciiString translatedName = translations.get(name);
// All headers that start with ':' are only valid in HTTP/2 context if (translatedName != null) {
if (name.length() == 0 || name.charAt(0) == ':') { output.add(translatedName, AsciiString.of(value));
throw streamError(streamId, PROTOCOL_ERROR, } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", 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 (COOKIE.equals(name)) { if (name.length() == 0 || name.charAt(0) == ':') {
// combine the cookie values into 1 header entry. throw streamError(streamId, PROTOCOL_ERROR,
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5 "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
String existingCookie = output.get(COOKIE); }
output.set(COOKIE, if (COOKIE.equals(name)) {
(existingCookie != null) ? (existingCookie + "; " + value) : value); // combine the cookie values into 1 header entry.
} else { // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
output.add(name, value); 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 {
} }
} }
} }

View File

@ -17,10 +17,12 @@ package io.netty.handler.codec.http2;
import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.junit.Test; import org.junit.Test;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; 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.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS; import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
@ -143,4 +145,18 @@ public class HttpConversionUtilTest {
assertEquals(1, out.size()); assertEquals(1, out.size());
assertSame("world", out.get("hello")); 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()));
}
} }