Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override HttpRequestDecoder.createMessage() for perfomance #4863

Merged
merged 29 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1de569a
Fork netty's `httpServerCodec` and replace its usage
echo304 May 6, 2023
6b41e20
Fix test compile failure
echo304 May 7, 2023
ffa914f
Add licenses
echo304 May 9, 2023
9911367
Implement ArmeriaHttpHeaders
echo304 May 29, 2023
b688903
Implement `ArmeriaDefaultHttpRequest`
echo304 May 29, 2023
9de5522
Modify `HttpServerCodec.createMessage` to use `ArmeriaDefaultHttpRequ…
echo304 May 30, 2023
743a6fc
Modify `Http1RequestDecoder.channelRead` to use `ArmeriaDefaultHttpRe…
echo304 May 30, 2023
efd741e
Merge branch 'main' into custom-http-server-codec
echo304 Jun 2, 2023
bba7d8d
Reflect code review
echo304 Jun 3, 2023
8bccbc8
Fix styles
echo304 Jun 3, 2023
65ad9cd
Fix constructor of `ArmeriaHttpHeaders` to invoke `add()` method of i…
echo304 Jun 4, 2023
ce07dc1
Add `headers()` getter on `ArmeriaDefaultHttpRequest`
echo304 Jun 5, 2023
b81f037
Add test for `ArmeriaHttpHeaders` and fix logic
echo304 Jun 5, 2023
a2a2f48
Try another approach
echo304 Jun 8, 2023
deaa03c
Fix infinite loop bug
echo304 Jun 14, 2023
665c104
Embed building headers logic into `ArmeriaHttpHeaders.buildRequestHea…
echo304 Jun 21, 2023
2391c7b
Merge remote-tracking branch 'upstream/main' into custom-http-server-…
echo304 Jun 22, 2023
7515d8f
Fix ArmeriaHttpHeadersTest failed tests
echo304 Jul 5, 2023
89bc4ea
Merge branch 'main' into custom-http-server-codec
echo304 Jul 5, 2023
39092ea
Fix failed shouldSupportBindingOnDomainSocket test
echo304 Jul 5, 2023
c9f7520
Fix failed testUnsupportedMethod test
echo304 Jul 6, 2023
9ecd024
Fix styles
echo304 Jul 6, 2023
2cc3f13
Merge branch 'main' into custom-http-server-codec
echo304 Jul 11, 2023
f48f148
Merge branch 'main' into custom-http-server-codec
jrhee17 Mar 25, 2024
19ab386
minor cleanups
jrhee17 Mar 26, 2024
340a6f5
hide over-exposed class
jrhee17 Mar 26, 2024
ca51df6
lint
jrhee17 Mar 26, 2024
c80eb0c
Hide static methods and Change ArmeriaHttpHeadersFactory to enum
minwoox Apr 2, 2024
1eaad3a
minor fix
ikhoon Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -649,21 +649,18 @@
* {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
*/
public static RequestHeaders toArmeria(
ChannelHandlerContext ctx, HttpRequest in,
ChannelHandlerContext ctx, HttpRequest in, RequestHeadersBuilder out,
ServerConfig cfg, String scheme, RequestTarget reqTarget) throws URISyntaxException {

final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
final RequestHeadersBuilder out = RequestHeaders.builder();
out.sizeHint(inHeaders.size());
out.method(firstNonNull(HttpMethod.tryParse(in.method().name()), HttpMethod.UNKNOWN))
.scheme(scheme)
.path(reqTarget.toString());

// Add the HTTP headers which have not been consumed above
toArmeria(inHeaders, out);
if (!out.contains(HttpHeaderNames.HOST)) {
out.add(HttpHeaderNames.HOST, defaultAuthority(ctx, cfg));
}
purgeHttp1OnlyHeaders(inHeaders, out);
return out.build();
}

Expand Down Expand Up @@ -696,6 +693,8 @@

/**
* Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
* Functionally, this method is expected to behavior in the same way as
* {@link #purgeHttp1OnlyHeaders(io.netty.handler.codec.http.HttpHeaders, HttpHeadersBuilder)}.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I haven't migrated this method to use purgeHttp1OnlyHeaders since this is probably slower for other code paths using this method without zero-copy

public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeadersBuilder out) {
final Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
Expand Down Expand Up @@ -741,6 +740,89 @@
maybeSetContentLengthUnknown(inHeaders.contains(HttpHeaderNames.CONTENT_LENGTH), out);
}

/**
* Removes HTTP/1 specified headers from a mutable headers map.
* Functionally this method is expected to behave the same as
* {@link #toArmeria(io.netty.handler.codec.http.HttpHeaders, HttpHeadersBuilder)}.
* This method should be preferred going forward as we continue implementing zero-copy
* for HTTP1 en/decoders.
*/
public static void purgeHttp1OnlyHeaders(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
maybeSetTeHeader(inHeaders, out);
maybeRemoveConnectionHeaders(inHeaders, out);
maybeSetCookie(inHeaders, out);
maybeSetContentLengthUnknown(inHeaders.contains(HttpHeaderNames.CONTENT_LENGTH), out);
}

private static void maybeRemoveConnectionHeaders(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
final CaseInsensitiveMap connectionDisallowedList =
toLowercaseMap(inHeaders.valueCharSequenceIterator(HttpHeaderNames.CONNECTION), 8);
final boolean isWebSocketUpgrade = isWebSocketUpgrade(inHeaders);
connectionDisallowedList.forEach(entry -> out.remove(entry.getKey()));
HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.forEach(entry -> out.remove(entry.getKey()));
if (isWebSocketUpgrade) {
out.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE.toString());
out.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET.toString());
}
}

private static void maybeSetCookie(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
// Cookies must be concatenated into a single octet string.
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
if (out.contains(HttpHeaderNames.COOKIE)) {
final StringJoiner cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
inHeaders.getAll(HttpHeaderNames.COOKIE).forEach(
value -> COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add));
out.set(HttpHeaderNames.COOKIE, cookieJoiner.toString());

Check warning on line 779 in core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java#L776-L779

Added lines #L776 - L779 were not covered by tests
}
}

public static void maybeSetTeHeader(io.netty.handler.codec.http.HttpHeaders inHeaders,
HttpHeadersBuilder out) {
if (!inHeaders.contains(HttpHeaderNames.TE)) {
return;
}
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 makes a special exception for TE
final boolean hasTrailersTe = findDelimitedIgnoreCase(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS,
inHeaders);
if (hasTrailersTe) {
out.set(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
} else {
out.remove(HttpHeaderNames.TE);

Check warning on line 794 in core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java#L794

Added line #L794 was not covered by tests
}
}

private static boolean isWebSocketUpgrade(io.netty.handler.codec.http.HttpHeaders inHeaders) {
final boolean isUpgrade = findDelimitedIgnoreCase(HttpHeaderNames.CONNECTION,
HttpHeaderValues.UPGRADE, inHeaders);
final boolean isWebsocket = findDelimitedIgnoreCase(HttpHeaderNames.UPGRADE,
HttpHeaderValues.WEBSOCKET, inHeaders);
return isUpgrade && isWebsocket;
}

public static boolean findDelimitedIgnoreCase(AsciiString targetName, AsciiString targetValue,
io.netty.handler.codec.http.HttpHeaders httpHeaders) {
final List<String> allValues = httpHeaders.getAll(targetName);
if (allValues.isEmpty()) {
return false;
}
for (String value: allValues) {
if (targetValue.contentEqualsIgnoreCase(value)) {
return true;
}
final List<CharSequence> values = StringUtil.unescapeCsvFields(value);
for (CharSequence field : values) {
if (targetValue.contentEqualsIgnoreCase(AsciiString.trim(field))) {
return true;

Check warning on line 819 in core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java#L819

Added line #L819 was not covered by tests
}
}
}
return false;
}

private static void maybeSetContentLengthUnknown(boolean hasContentLength, HttpHeadersBuilder out) {
if (hasContentLength) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation 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:
*
* https://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.
*/

package com.linecorp.armeria.server;

import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpHeadersFactory;

final class ArmeriaHttpHeadersFactory implements HttpHeadersFactory {

static final ArmeriaHttpHeadersFactory INSTANCE = new ArmeriaHttpHeadersFactory();

private ArmeriaHttpHeadersFactory() {}

@Override
public HttpHeaders newHeaders() {
return new NettyHttp1Headers();
}

@Override
public HttpHeaders newEmptyHeaders() {
return new NettyHttp1Headers();

Check warning on line 35 in core/src/main/java/com/linecorp/armeria/server/ArmeriaHttpHeadersFactory.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/server/ArmeriaHttpHeadersFactory.java#L35

Added line #L35 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,16 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
return;
}

// Convert the Netty HttpHeaders into Armeria RequestHeaders.
assert msg instanceof NettyHttp1Request;
// Precompute values that rely on CONNECTION related headers since they will be cleaned
// after ArmeriaHttpUtil#toArmeria is called
final boolean keepAlive = HttpUtil.isKeepAlive(nettyReq);
final boolean transferEncodingChunked = HttpUtil.isTransferEncodingChunked(nettyReq);

final NettyHttp1Headers nettyHttp1Headers = (NettyHttp1Headers) nettyReq.headers();
final RequestHeaders headers =
ArmeriaHttpUtil.toArmeria(ctx, nettyReq, cfg, scheme.toString(), reqTarget);
ArmeriaHttpUtil.toArmeria(ctx, nettyReq, nettyHttp1Headers.builder(),
cfg, scheme.toString(), reqTarget);
// Do not accept unsupported methods.
final HttpMethod method = headers.method();
switch (method) {
Expand Down Expand Up @@ -268,8 +275,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
}
}

final boolean keepAlive = HttpUtil.isKeepAlive(nettyReq);
final boolean endOfStream = contentEmpty && !HttpUtil.isTransferEncodingChunked(nettyReq);
final boolean endOfStream = contentEmpty && !transferEncodingChunked;
this.req = req = DecodedHttpRequest.of(endOfStream, eventLoop, id, 1, headers,
keepAlive, inboundTrafficController, routingCtx);

Expand Down
145 changes: 145 additions & 0 deletions core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2023 LINE Corporation
*
* LINE Corporation 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:
*
* https://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.
*/
/*
* Copyright 2012 The Netty Project
*
* The Netty Project 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:
*
* https://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.
*/
package com.linecorp.armeria.server;
minwoox marked this conversation as resolved.
Show resolved Hide resolved

import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.CombinedChannelDuplexHandler;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpStatusClass;
import io.netty.handler.codec.http.HttpVersion;

minwoox marked this conversation as resolved.
Show resolved Hide resolved
/**
* Forked from {@link HttpClientCodec}.
* A combination of {@link HttpRequestDecoder} and {@link HttpResponseEncoder}
* which enables easier server side HTTP implementation.
*/
final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodec {

// Forked from https://github.com/netty/netty/blob/cf624c93c5f97097f1b13fe926ed50c32c8b1430/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java

/** A queue that is used for correlating a request and a response. */
private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();

/**
* Creates a new instance with the default decoder options
* ({@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
* {@code maxChunkSize (8192)}).
*/
HttpServerCodec() {
this(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE);
}

/**
* Creates a new instance with the specified decoder options.
*/
HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
init(new HttpServerRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize),
new HttpServerResponseEncoder());
}

/**
* Upgrades to another protocol from HTTP. Removes the {@link HttpRequestDecoder} and
* {@link HttpResponseEncoder} from the pipeline.
*/
@Override
public void upgradeFrom(ChannelHandlerContext ctx) {
ctx.pipeline().remove(this);
}

private final class HttpServerRequestDecoder extends HttpRequestDecoder {

HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
super(maxInitialLineLength, maxHeaderSize, maxChunkSize);
}

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
final int oldSize = out.size();
super.decode(ctx, buffer, out);
final int size = out.size();
for (int i = oldSize; i < size; i++) {
final Object obj = out.get(i);
if (obj instanceof HttpRequest) {
queue.add(((HttpRequest) obj).method());
}
}
}

@Override
protected HttpMessage createMessage(String[] initialLine) throws Exception {
return new NettyHttp1Request(
HttpVersion.valueOf(initialLine[2]),
HttpMethod.valueOf(initialLine[0]), initialLine[1]);
}
}

private final class HttpServerResponseEncoder extends HttpResponseEncoder {

private HttpMethod method;

@Override
protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) {
if (!isAlwaysEmpty && HttpMethod.CONNECT.equals(method) &&
msg.status().codeClass() == HttpStatusClass.SUCCESS) {
// Stripping Transfer-Encoding:
// See section 3.3.1 of https://datatracker.ietf.org/doc/rfc7230
msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
return;

Check warning on line 133 in core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java

View check run for this annotation

Codecov / codecov/patch

core/src/main/java/com/linecorp/armeria/server/HttpServerCodec.java#L132-L133

Added lines #L132 - L133 were not covered by tests
}

super.sanitizeHeadersBeforeEncode(msg, isAlwaysEmpty);
}

@Override
protected boolean isContentAlwaysEmpty(@SuppressWarnings("unused") HttpResponse msg) {
method = queue.poll();
return HttpMethod.HEAD.equals(method) || super.isContentAlwaysEmpty(msg);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.util.AsciiString;
Expand Down