All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.http.server.netty.NettyHttpRequest Maven / Gradle / Ivy

There is a newer version: 4.7.9
Show newest version
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed 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 io.micronaut.http.server.netty;

import io.micronaut.buffer.netty.NettyByteBufferFactory;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.attr.AttributeHolder;
import io.micronaut.core.bind.ArgumentBinder;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.ConvertibleValues;
import io.micronaut.core.convert.value.MutableConvertibleValues;
import io.micronaut.core.convert.value.MutableConvertibleValuesMap;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.io.buffer.DelegateByteBuffer;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpAttributes;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpVersion;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpParameters;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.PushCapableHttpRequest;
import io.micronaut.http.ServerHttpRequest;
import io.micronaut.http.body.ByteBody;
import io.micronaut.http.body.CloseableByteBody;
import io.micronaut.http.body.InternalByteBody;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.cookie.Cookies;
import io.micronaut.http.netty.AbstractNettyHttpRequest;
import io.micronaut.http.netty.NettyHttpHeaders;
import io.micronaut.http.netty.NettyHttpParameters;
import io.micronaut.http.netty.NettyHttpRequestBuilder;
import io.micronaut.http.netty.body.AvailableNettyByteBody;
import io.micronaut.http.netty.body.NettyByteBody;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.netty.cookies.NettyCookie;
import io.micronaut.http.netty.cookies.NettyCookies;
import io.micronaut.http.netty.stream.DefaultStreamedHttpRequest;
import io.micronaut.http.netty.stream.DelegateStreamedHttpRequest;
import io.micronaut.http.netty.stream.StreamedHttpRequest;
import io.micronaut.http.server.HttpServerConfiguration;
import io.micronaut.http.server.netty.handler.Http2ServerHandler;
import io.micronaut.http.server.netty.multipart.NettyCompletedFileUpload;
import io.micronaut.web.router.DefaultUriRouteMatch;
import io.micronaut.web.router.RouteMatch;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http2.DefaultHttp2PushPromiseFrame;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2FrameCodec;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Stream;
import io.netty.handler.codec.http2.Http2StreamChannel;
import io.netty.handler.codec.http2.Http2StreamChannelBootstrap;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLSession;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * Delegates to the Netty {@link io.netty.handler.codec.http.HttpRequest} instance.
 *
 * @param  The type
 * @author Graeme Rocher
 * @since 1.0
 */
@Internal
public class NettyHttpRequest extends AbstractNettyHttpRequest implements HttpRequest, PushCapableHttpRequest, io.micronaut.http.FullHttpRequest, ServerHttpRequest {
    private static final Logger LOG = LoggerFactory.getLogger(NettyHttpRequest.class);

    /**
     * Headers to exclude from the push promise sent to the client. We use
     * {@link io.netty.handler.codec.http.HttpHeaders} to store this set, so that the semantics (e.g. case
     * insensitivity) are the same as for the actual header storage, so that we don't accidentally copy headers we're
     * not supposed to.
     */
    private static final io.netty.handler.codec.http.HttpHeaders SERVER_PUSH_EXCLUDE_HEADERS;

    static {
        SERVER_PUSH_EXCLUDE_HEADERS = new DefaultHttpHeaders();
        // from JavaEE PushBuilder javadoc: "The existing request headers of the current HttpServletRequest are added to the builder, except for: "
        // "Conditional headers (defined in RFC 7232)"
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.ETAG, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.IF_MATCH, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.IF_MODIFIED_SINCE, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.IF_NONE_MATCH, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.IF_UNMODIFIED_SINCE, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.LAST_MODIFIED, "");
        // "Range headers" (RFC 7233)
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.ACCEPT_RANGES, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.CONTENT_RANGE, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.IF_RANGE, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.RANGE, "");
        // "Expect headers"
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.EXPECT, "");
        // "Referrer headers"
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.REFERER, "");
        // "Authorization headers"
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.PROXY_AUTHENTICATE, "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpHeaderNames.PROXY_AUTHORIZATION, "");
        // we do copy other authorization headers and cookies. This is a potential security risk, e.g. if there's an
        // intermediate HTTP proxy that adds auth headers that the client isn't supposed to see – the client will
        // receive a copy of those headers in the PUSH_PROMISE. However, I'm not sure if the client will utilize the
        // pushed response properly if we don't send the authorization headers. It might also depend on the Vary
        // header. This behavior is documented in PushCapableHttpRequest.

        // some netty headers we won't copy
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpConversionUtil.ExtensionHeaderNames.PATH.text(), "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "");
        SERVER_PUSH_EXCLUDE_HEADERS.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), "");
        // we do copy the weight and dependency id
    }

    /**
     * ONLY for NettyBodyAnnotationBinder use.
     */
    @Internal
    @SuppressWarnings("VisibilityModifier")
    public ArgumentBinder.BindingResult> convertibleBody;

    private final NettyHttpHeaders headers;
    private final ChannelHandlerContext channelHandlerContext;
    private final HttpServerConfiguration serverConfiguration;
    private MutableConvertibleValues attributes;
    private NettyCookies nettyCookies;
    private final CloseableByteBody body;
    @Nullable
    private FormRouteCompleter formRouteCompleter;
    private ExecutionFlow routeWaitsFor = ExecutionFlow.just(null);
    private Object legacyBody;

    private final BodyConvertor bodyConvertor = newBodyConvertor();

    /**
     * @param nettyRequest        The {@link io.netty.handler.codec.http.HttpRequest}
     * @param body                The request body
     * @param ctx                 The {@link ChannelHandlerContext}
     * @param environment         The Environment
     * @param serverConfiguration The {@link HttpServerConfiguration}
     * @throws IllegalArgumentException When the request URI is invalid
     */
    @SuppressWarnings("MagicNumber")
    public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest,
                            CloseableByteBody body,
                            ChannelHandlerContext ctx,
                            ConversionService environment,
                            HttpServerConfiguration serverConfiguration) throws IllegalArgumentException {
        super(nettyRequest, environment);
        Objects.requireNonNull(nettyRequest, "Netty request cannot be null");
        Objects.requireNonNull(ctx, "ChannelHandlerContext cannot be null");
        Objects.requireNonNull(environment, "Environment cannot be null");
        this.serverConfiguration = serverConfiguration;
        this.channelHandlerContext = ctx;
        this.headers = new NettyHttpHeaders(nettyRequest.headers(), conversionService);
        this.body = body;
    }

    @Override
    public final ByteBody byteBody() {
        return body;
    }

    public final void setLegacyBody(Object legacyBody) {
        this.legacyBody = legacyBody;
    }

    public final void addRouteWaitsFor(ExecutionFlow executionFlow) {
        routeWaitsFor = routeWaitsFor.then(() -> executionFlow);
    }

    public final ExecutionFlow getRouteWaitsFor() {
        return routeWaitsFor;
    }

    public final FormRouteCompleter formRouteCompleter() {
        assert isFormOrMultipartData();
        if (formRouteCompleter == null) {
            formRouteCompleter = new FormRouteCompleter((RouteMatch) getAttribute(HttpAttributes.ROUTE_MATCH).get(), getChannelHandlerContext().channel().eventLoop());
        }
        return formRouteCompleter;
    }

    public final boolean hasFormRouteCompleter() {
        return formRouteCompleter != null;
    }

    @Override
    public MutableHttpRequest mutate() {
        return new NettyMutableHttpRequest();
    }

    @NonNull
    @Override
    public Optional getAttribute(CharSequence name) {
        return Optional.ofNullable(getAttributes().getValue(Objects.requireNonNull(name, "Name cannot be null").toString()));
    }

    @Override
    public HttpVersion getHttpVersion() {
        HttpPipelineBuilder.StreamPipeline pipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE.get()).get();
        if (pipeline != null) {
            return pipeline.httpVersion;
        }
        // Http2ServerHandler case
        return findConnectionHandler() == null ? HttpVersion.HTTP_1_1 : HttpVersion.HTTP_2_0;
    }

    @Override
    public String toString() {
        return getMethodName() + " " + getUri();
    }

    /**
     * @return Obtain a reference to the native Netty HTTP request
     */
    public io.netty.handler.codec.http.HttpRequest getNativeRequest() {
        return nettyRequest;
    }

    /**
     * @return The {@link ChannelHandlerContext}
     */
    public ChannelHandlerContext getChannelHandlerContext() {
        return channelHandlerContext;
    }

    @Override
    public Cookies getCookies() {
        NettyCookies cookies = this.nettyCookies;
        if (cookies == null) {
            synchronized (this) { // double check
                cookies = this.nettyCookies;
                if (cookies == null) {
                    cookies = new NettyCookies(getPath(), headers.getNettyHeaders(), conversionService);
                    this.nettyCookies = cookies;
                }
            }
        }
        return cookies;
    }

    @Override
    public InetSocketAddress getRemoteAddress() {
        return (InetSocketAddress) getChannelHandlerContext()
            .channel()
            .remoteAddress();
    }

    @Override
    public InetSocketAddress getServerAddress() {
        return (InetSocketAddress) getChannelHandlerContext()
            .channel()
            .localAddress();
    }

    @Override
    public String getServerName() {
        return getServerAddress().getHostName();
    }

    @Override
    public boolean isSecure() {
        ChannelHandlerContext channelHandlerContext = getChannelHandlerContext();
        return channelHandlerContext.pipeline().get(SslHandler.class) != null;
    }

    @Override
    public Optional getOrigin() {
        return headers.getOrigin();
    }

    @Override
    public HttpHeaders getHeaders() {
        return headers;
    }

    @Override
    public MutableConvertibleValues getAttributes() {
        MutableConvertibleValues attributes = this.attributes;
        if (attributes == null) {
            synchronized (this) { // double check
                attributes = this.attributes;
                if (attributes == null) {
                    attributes = new MutableConvertibleValuesMap<>(new HashMap<>(8));
                    this.attributes = attributes;
                }
            }
        }
        return attributes;
    }

    @Override
    public HttpRequest setAttribute(CharSequence name, Object value) {
        // This is the copy from the super method to avoid the type pollution
        if (StringUtils.isNotEmpty(name)) {
            if (value == null) {
                getAttributes().remove(name.toString());
            } else {
                getAttributes().put(name.toString(), value);
            }
        }
        return this;
    }

    @Override
    public Optional getSslSession() {
        Supplier sup = channelHandlerContext.channel().attr(HttpPipelineBuilder.SSL_SESSION_ATTRIBUTE.get()).get();
        return sup == null ? Optional.empty() : Optional.ofNullable(sup.get());
    }

    @SuppressWarnings("unchecked")
    @Override
    public Optional getBody() {
        if (hasFormRouteCompleter()) {
            return Optional.of((T) formRouteCompleter().asMap(serverConfiguration.getDefaultCharset()));
        } else {
            return Optional.ofNullable((T) legacyBody);
        }
    }

    @Override
    public  Optional getBody(Class type) {
        return getBody(Argument.of(type));
    }

    @SuppressWarnings("unchecked")
    @Override
    public  Optional getBody(ArgumentConversionContext conversionContext) {
        return getBody().flatMap(t -> bodyConvertor.convert(conversionContext, t));
    }

    /**
     * Release and cleanup resources.
     */
    @Internal
    public void release() {
        Object routeMatchO = ((AttributeHolder) this).getAttribute(HttpAttributes.ROUTE_MATCH).orElse(null);
        // usually this is a DefaultUriRouteMatch, avoid scalability issues here
        RouteMatch routeMatch = routeMatchO instanceof DefaultUriRouteMatch urm ? urm : (RouteMatch) routeMatchO;
        if (routeMatch != null) {
            // discard parameters that have already been bound
            for (Object toDiscard : routeMatch.getVariableValues().values()) {
                if (toDiscard instanceof io.micronaut.core.io.buffer.ReferenceCounted rc) {
                    rc.release();
                }
                if (toDiscard instanceof io.netty.util.ReferenceCounted rc) {
                    rc.release();
                }
                if (toDiscard instanceof NettyCompletedFileUpload fu) {
                    fu.discard();
                }
            }
        }
        body.close();
        if (formRouteCompleter != null) {
            formRouteCompleter.release();
        }
        if (attributes != null) {
            attributes.forEach((k, v) -> {
                //noinspection StringEquality
                if (k == HttpAttributes.ROUTE_MATCH.toString() || k == HttpAttributes.ROUTE_INFO.toString() || v instanceof String) {
                    // perf: avoid an instanceof in releaseIfNecessary
                    return;
                }
                releaseIfNecessary(v);
            });
        }
    }

    /**
     * @param value An object with a value
     */
    protected void releaseIfNecessary(Object value) {
        if (value instanceof ReferenceCounted referenceCounted) {
            int i = referenceCounted.refCnt();
            if (i != 0) {
                referenceCounted.release();
            }
        }
    }

    @Nullable
    private ChannelHandlerContext findConnectionHandler() {
        ChannelHandlerContext current = channelHandlerContext.pipeline().context(Http2ConnectionHandler.class);
        if (current != null) {
            return current;
        }
        Channel parentChannel = channelHandlerContext.channel().parent();
        if (parentChannel != null) {
            return parentChannel.pipeline().context(Http2FrameCodec.class);
        }
        return null;
    }

    @Override
    public boolean isServerPushSupported() {
        ChannelHandlerContext http2ConnectionHandlerContext = findConnectionHandler();
        return http2ConnectionHandlerContext != null &&
            ((Http2ConnectionHandler) http2ConnectionHandlerContext.handler()).connection().remote().allowPushTo();
    }

    @Override
    public PushCapableHttpRequest serverPush(@NonNull HttpRequest request) {
        ChannelHandlerContext connectionHandlerContext = findConnectionHandler();
        if (connectionHandlerContext != null) {
            Http2ConnectionHandler connectionHandler = (Http2ConnectionHandler) connectionHandlerContext.handler();

            if (!connectionHandler.connection().remote().allowPushTo()) {
                throw new UnsupportedOperationException("Server push not supported by this client: Client is HTTP2 but does not report support for this feature");
            }

            URI configuredUri = request.getUri();
            String scheme = configuredUri.getScheme();
            if (scheme == null) {
                scheme = connectionHandlerContext.channel().pipeline().get(SslHandler.class) == null ? SCHEME_HTTP : SCHEME_HTTPS;
            }
            String authority = configuredUri.getAuthority();
            if (authority == null) {
                // this is potentially user-controlled.
                authority = this.getHeaders().get("Host");
            }
            String path = configuredUri.getPath();
            if (path == null || !path.startsWith("/")) {
                throw new IllegalArgumentException("Request must have an absolute path");
            }
            String query = configuredUri.getQuery();
            String fragment = configuredUri.getFragment();

            URI fixedUri;
            try {
                fixedUri = new URI(scheme, authority, path, query, fragment);
            } catch (URISyntaxException e) {
                throw new IllegalArgumentException("Illegal URI", e);
            }

            // request used to trigger our handlers
            io.netty.handler.codec.http.HttpRequest inboundRequestNoBody = NettyHttpRequestBuilder.asBuilder(request).toHttpRequestWithoutBody();
            FullHttpRequest inboundRequest = new DefaultFullHttpRequest(
                inboundRequestNoBody.protocolVersion(),
                inboundRequestNoBody.method(),
                inboundRequestNoBody.uri(),
                Unpooled.EMPTY_BUFFER,
                inboundRequestNoBody.headers(),
                EmptyHttpHeaders.INSTANCE
            );

            // copy headers from our request
            for (Iterator> itr = headers.getNettyHeaders().iteratorCharSequence(); itr.hasNext(); ) {
                Map.Entry entry = itr.next();
                if (!inboundRequest.headers().contains(entry.getKey()) && !SERVER_PUSH_EXCLUDE_HEADERS.contains(entry.getKey())) {
                    inboundRequest.headers().add(entry.getKey(), entry.getValue());
                }
            }
            if (!inboundRequest.headers().contains(HttpHeaderNames.REFERER)) {
                inboundRequest.headers().add(HttpHeaderNames.REFERER, getUri().toString());
            }

            // request used to compute the headers for the PUSH_PROMISE frame
            io.netty.handler.codec.http.HttpRequest outboundRequest = new DefaultHttpRequest(
                inboundRequest.protocolVersion(),
                inboundRequest.method(),
                fixedUri.toString(),
                inboundRequest.headers()
            );
            Http2Headers outboundHeaders = HttpConversionUtil.toHttp2Headers(outboundRequest, false);

            if (channelHandlerContext.channel() instanceof Http2StreamChannel streamChannel) {
                int ourStream = streamChannel.stream().id();
                HttpPipelineBuilder.StreamPipeline originalStreamPipeline = channelHandlerContext.channel().attr(HttpPipelineBuilder.STREAM_PIPELINE_ATTRIBUTE.get()).get();

                new Http2StreamChannelBootstrap(channelHandlerContext.channel().parent())
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(@NonNull Http2StreamChannel ch) throws Exception {
                            int newStream = ch.stream().id();

                            channelHandlerContext.write(new DefaultHttp2PushPromiseFrame(outboundHeaders)
                                .stream(((Http2StreamChannel) channelHandlerContext.channel()).stream())
                                .pushStream(ch.stream()));

                            originalStreamPipeline.initializeChildPipelineForPushPromise(ch);

                            inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStream);
                            inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStream);

                            // delay until our handling is complete
                            connectionHandlerContext.executor().execute(() -> {
                                try {
                                    ch.pipeline().context(ChannelPipelineCustomizer.HANDLER_HTTP_DECODER).fireChannelRead(inboundRequest);
                                } catch (Exception e) {
                                    LOG.warn("Failed to complete push promise", e);
                                }
                            });
                        }
                    })
                    .open()
                    .addListener((GenericFutureListener>) future -> {
                        try {
                            future.sync();
                        } catch (Exception e) {
                            if (e instanceof InterruptedException) {
                                Thread.currentThread().interrupt();
                            }
                            LOG.warn("Failed to complete push promise", e);
                        }
                    });
            } else {
                int ourStreamId = headers.getNettyHeaders().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
                if (channelHandlerContext.executor().inEventLoop()) {
                    serverPush0(connectionHandler, ourStreamId, inboundRequest, connectionHandlerContext, outboundHeaders);
                } else {
                    channelHandlerContext.executor().execute(() -> serverPush0(connectionHandler, ourStreamId, inboundRequest, connectionHandlerContext, outboundHeaders));
                }
            }
            return this;
        } else {
            throw new UnsupportedOperationException("Server push not supported by this client: Not a HTTP2 client");
        }
    }

    private static void serverPush0(Http2ConnectionHandler connectionHandler, int ourStreamId, FullHttpRequest inboundRequest, ChannelHandlerContext connectionHandlerContext, Http2Headers outboundHeaders) {
        try {
            Http2Stream ourStream = connectionHandler.connection().stream(ourStreamId);
            if (ourStream == null) {
                // this is a bit ugly. Ideally serverPush would return a Publisher that completes when the promise is sent
                throw new IllegalStateException("Push promise origin stream is already gone. " +
                    "This can happen if serverPush() is called outside the event loop, and the primary response is sent before the push promise. " +
                    "Please either call serverPush() on the event loop, or delay the primary response.");
            }
            int newStreamId = connectionHandler.connection().local().incrementAndGetNextStreamId();

            inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), newStreamId);
            inboundRequest.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), ourStreamId);

            connectionHandler.encoder().writePushPromise(connectionHandlerContext, ourStreamId, newStreamId, outboundHeaders, 0, connectionHandlerContext.newPromise())
                .addListener((ChannelFutureListener) future -> {
                    if (future.isSuccess()) {
                        try {
                            ((Http2ServerHandler.ConnectionHandler) connectionHandler).handleFakeRequest(connectionHandler.connection().stream(newStreamId), inboundRequest);
                        } catch (Exception e) {
                            LOG.warn("Failed to send push promise", e);
                        }
                    } else {
                        LOG.warn("Failed to send push promise", future.cause());
                    }
                });
        } catch (Exception e) {
            LOG.warn("Failed to send push promise", e);
        }
    }

    @Override
    protected Charset initCharset(Charset characterEncoding) {
        return characterEncoding == null ? serverConfiguration.getDefaultCharset() : characterEncoding;
    }

    /**
     * @return Return true if the request is form data.
     */
    @Internal
    public final boolean isFormOrMultipartData() {
        MediaType ct = getContentType().orElse(null);
        return ct != null && (ct.equals(MediaType.APPLICATION_FORM_URLENCODED_TYPE) || ct.equals(MediaType.MULTIPART_FORM_DATA_TYPE));
    }

    @Override
    @Deprecated
    public io.netty.handler.codec.http.HttpRequest toHttpRequest() {
        return toHttpRequestWithoutBody();
    }

    @Override
    public Optional toHttpRequestDirect() {
        return Optional.of(new DelegateStreamedHttpRequest(nettyRequest, NettyByteBody.toByteBufs(byteBody()).map(DefaultHttpContent::new)));
    }

    @Override
    public ByteBody byteBodyDirect() {
        return byteBody();
    }

    @Override
    public io.netty.handler.codec.http.HttpRequest toHttpRequestWithoutBody() {
        if (nettyRequest instanceof FullHttpRequest) {
            // do not include body, the body is owned by us
            DefaultHttpRequest copy = new DefaultHttpRequest(
                nettyRequest.protocolVersion(),
                nettyRequest.method(),
                nettyRequest.uri(),
                nettyRequest.headers()
            );
            copy.setDecoderResult(nettyRequest.decoderResult());
            return copy;
        }
        return nettyRequest;
    }

    @Override
    public Optional getContentType() {
        return headers.contentType();
    }

    private BodyConvertor newBodyConvertor() {
        return new BodyConvertor() {

            @Override
            public Optional convert(ArgumentConversionContext conversionContext, Object value) {
                if (value == null) {
                    return Optional.empty();
                }
                if (Argument.OBJECT_ARGUMENT.equalsType(conversionContext.getArgument())) {
                    return Optional.of(value);
                }
                return convertFromNext(conversionService, conversionContext, value);
            }

        };
    }

    @Override
    public long getContentLength() {
        return headers.contentLength().orElse(-1);
    }

    @Override
    public boolean isFull() {
        return byteBody() instanceof AvailableNettyByteBody;
    }

    @Override
    public ByteBuffer contents() {
        if (byteBody() instanceof AvailableNettyByteBody immediate) {
            return toByteBuffer(immediate);
        }
        return null;
    }

    @Override
    public ExecutionFlow> bufferContents() {
        return InternalByteBody.bufferFlow(byteBody()).map(c -> toByteBuffer((AvailableNettyByteBody) c));
    }

    private static ByteBuffer toByteBuffer(AvailableNettyByteBody immediateByteBody) {
        // use delegate because we don't want to implement ReferenceCounted
        return new DelegateByteBuffer<>(NettyByteBufferFactory.DEFAULT.wrap(immediateByteBody.peek()));
    }

    /**
     * Mutable version of the request.
     */
    private final class NettyMutableHttpRequest implements MutableHttpRequest, NettyHttpRequestBuilder {

        private URI uri;
        @Nullable
        private MutableHttpParameters httpParameters;
        @Nullable
        private Object body;

        @Override
        public void setConversionService(ConversionService conversionService) {
            if (httpParameters != null) {
                httpParameters.setConversionService(conversionService);
            }
        }

        @Override
        public MutableHttpRequest cookie(Cookie cookie) {
            if (cookie instanceof NettyCookie nettyCookie) {
                String value = ClientCookieEncoder.LAX.encode(nettyCookie.getNettyCookie());
                headers.add(HttpHeaderNames.COOKIE, value);
            }
            return this;
        }

        @Override
        public MutableHttpRequest uri(URI uri) {
            this.uri = uri;
            if (uri.getQuery() != null) {
                // have to re-initialize parameters
                this.httpParameters = null;
            }
            return this;
        }

        @Override
        public  MutableHttpRequest body(T1 body) {
            this.body = body;
            return (MutableHttpRequest) this;
        }

        @Override
        public MutableHttpHeaders getHeaders() {
            return headers;
        }

        @NonNull
        @Override
        public MutableConvertibleValues getAttributes() {
            return NettyHttpRequest.this.getAttributes();
        }

        @NonNull
        @Override
        public Optional getBody() {
            if (body != null) {
                return Optional.of((T) body);
            }
            return NettyHttpRequest.this.getBody();
        }

        @NonNull
        @Override
        public Cookies getCookies() {
            return NettyHttpRequest.this.getCookies();
        }

        @Override
        public MutableHttpParameters getParameters() {
            MutableHttpParameters httpParameters = this.httpParameters;
            if (httpParameters == null) {
                synchronized (this) { // double check
                    httpParameters = this.httpParameters;
                    if (httpParameters == null) {
                        QueryStringDecoder queryStringDecoder = createDecoder(getUri());
                        httpParameters = new NettyHttpParameters(queryStringDecoder.parameters(), conversionService, null);
                        this.httpParameters = httpParameters;
                    }
                }
            }
            return httpParameters;
        }

        @NonNull
        @Override
        public HttpMethod getMethod() {
            return NettyHttpRequest.this.getMethod();
        }

        @NonNull
        @Override
        public URI getUri() {
            if (uri != null) {
                return uri;
            }
            return NettyHttpRequest.this.getUri();
        }

        @NonNull
        @Override
        @Deprecated
        public io.netty.handler.codec.http.FullHttpRequest toFullHttpRequest() {
            io.netty.handler.codec.http.HttpRequest nr = NettyHttpRequest.this.nettyRequest;
            if (nr instanceof io.netty.handler.codec.http.FullHttpRequest) {
                return (io.netty.handler.codec.http.FullHttpRequest) NettyHttpRequest.this.nettyRequest;
            } else {
                return new DefaultFullHttpRequest(
                    nr.protocolVersion(),
                    nr.method(),
                    nr.uri(),
                    Unpooled.EMPTY_BUFFER,
                    nr.headers(),
                    EmptyHttpHeaders.INSTANCE
                );
            }
        }

        @NonNull
        @Override
        @Deprecated
        public StreamedHttpRequest toStreamHttpRequest() {
            if (isStream()) {
                return (StreamedHttpRequest) NettyHttpRequest.this.nettyRequest;
            } else {
                io.netty.handler.codec.http.FullHttpRequest fullHttpRequest = toFullHttpRequest();
                DefaultStreamedHttpRequest request = new DefaultStreamedHttpRequest(
                    fullHttpRequest.protocolVersion(),
                    fullHttpRequest.method(),
                    fullHttpRequest.uri(),
                    true,
                    Publishers.just(new DefaultLastHttpContent(fullHttpRequest.content()))
                );
                request.headers().setAll(fullHttpRequest.headers());
                return request;
            }
        }

        @NonNull
        @Override
        @Deprecated
        public io.netty.handler.codec.http.HttpRequest toHttpRequest() {
            if (isStream()) {
                return toStreamHttpRequest();
            }
            return toFullHttpRequest();
        }

        @Override
        @Deprecated
        public boolean isStream() {
            return NettyHttpRequest.this.nettyRequest instanceof StreamedHttpRequest;
        }

        @Override
        public MutableHttpRequest mutate() {
            return new NettyMutableHttpRequest();
        }

        @Override
        public io.netty.handler.codec.http.HttpRequest toHttpRequestWithoutBody() {
            return NettyHttpRequest.this.toHttpRequestWithoutBody();
        }

        @Override
        public Optional toHttpRequestDirect() {
            return body != null ? Optional.empty() : NettyHttpRequest.this.toHttpRequestDirect();
        }

        @Override
        public ByteBody byteBodyDirect() {
            // if the body has been changed we can't return the byteBody directly
            return body != null ? null : NettyHttpRequest.this.byteBodyDirect();
        }
    }

    private abstract static class BodyConvertor {

        private BodyConvertor nextConvertor;

        public abstract Optional convert(ArgumentConversionContext conversionContext, T value);

        protected synchronized Optional convertFromNext(ConversionService conversionService, ArgumentConversionContext conversionContext, T value) {
            if (nextConvertor == null) {
                Optional conversion = conversionService.convert(value, conversionContext);
                nextConvertor = new BodyConvertor<>() {

                    @Override
                    public Optional convert(ArgumentConversionContext currentConversionContext, T value) {
                        if (currentConversionContext == conversionContext) {
                            return conversion;
                        }
                        if (currentConversionContext.getArgument().equalsType(conversionContext.getArgument())) {
                            conversionContext.getLastError().ifPresent(error -> {
                                error.getOriginalValue().ifPresentOrElse(
                                    originalValue -> currentConversionContext.reject(originalValue, error.getCause()),
                                    () -> currentConversionContext.reject(error.getCause())
                                );
                            });
                            return conversion;
                        }
                        return convertFromNext(conversionService, currentConversionContext, value);
                    }

                };
                return conversion;
            }
            return nextConvertor.convert(conversionContext, value);
        }

        public void cleanup() {
            nextConvertor = null;
        }

    }

}