#include "proxy.h"
#include "transfer.h"

#include <balancer/kernel/http2/server/common/http2_common.h>

namespace NModProxy {
    TString TModuleProxy::LogPrefix(const NSrvKernel::TConnDescr& descr, TStringBuf host, ui16 port, NModProxy::ETransferType type) noexcept {
        if (NModProxy::NeedDebugLog(descr)) {
            return TStringBuilder() << "upgraded backend [" << host << ":" << port << "] " <<
                                    (NModProxy::ETransferType::ClientToBackend == type ? "CtoB " : "BtoC ");
        } else {
            return {};
        }
    }

    TModuleProxy::TModuleProxy(const TProxyConfig& proxyConfig): NAME{proxyConfig.Name}, ProxyConfig_{proxyConfig} {}

    TError TModuleProxy::Run(const TConnDescr& descr, TTls& tls) const noexcept {
        auto error = Proxy(descr, tls);
        if (descr.RequestType == NSrvKernel::ERequestType::Hedged) {
            return error;
        }
        if (descr.AttemptsHolder && descr.AttemptsHolder->IsTimedOut()) {
            descr.ExtraAccessLog.SetSummary(NAME, "backend timeout");
            return Y_MAKE_ERROR(TBackendError{Y_MAKE_ERROR(TSystemError{ETIMEDOUT})});
        }
        return error;
    }

    TError TModuleProxy::HandleSrcrwr(const TConnDescr& descr, THostInfo& info) const {
        info.IsSrcRwr = true;
        if (descr.Properties->SrcrwrAddrs->GetNextAddr(info.Host, info.CachedIp, info.Port)) {
            LOG_ERROR(TLOG_INFO, descr, "srcrwr result " << (info.Host ?: info.CachedIp) << ":" << info.Port);
            return {};
        } else {
            LOG_ERROR(TLOG_ERR, descr, "failed to handle as srcrwr " << info.Host << ":" << info.Port);
            return Y_MAKE_ERROR(TNetworkResolutionError{EAI_FAIL} << "Invalid srcrwr host " << info.Host << ":" << info.Port);
        }
    }

    bool TModuleProxy::IsCancelledTransfer(const TError& error) noexcept {
        if (const auto* se = error.GetAs<TSystemError>()) {
            return se->Status() == ECANCELED;
        } else if (const auto* be = error.GetAs<TBackendError>()) {
            return IsCancelledTransfer(be->InnerError());
        } else {
            return false;
        }
    }

    void TModuleProxy::CollectIgnoredErrors(TTransfer& backendToClient, TTransfer& clientToBackend, TIgnoredErrors& errors) const noexcept {
        if (backendToClient.BackendError()) {
            errors.Add({std::move(backendToClient.BackendError()), "backend read", true});
        }
        if (clientToBackend.BackendError()) {
            errors.Add({std::move(clientToBackend.BackendError()), "backend write", true});
        }
        if (backendToClient.ClientError()) {
            errors.Add({std::move(backendToClient.ClientError()), "client write", false});
        }
        if (clientToBackend.ClientError()) {
            errors.Add({std::move(clientToBackend.ClientError()), "client read", false});
        }
    }

    void TModuleProxy::UpdateErrorStats(const TErrorWithComment& error, const TConnDescr& descr, const TErrorCtx& ctx) const noexcept {
        const auto* backendError = error.Error.GetAs<TBackendError>();
        const TError& realError = backendError ? backendError->InnerError() : error.Error;
        const auto* se = realError.GetAs<TSystemError>();
        bool isTimedout = se && se->Status() == ETIMEDOUT;

        if (error.Flags & TErrorWithComment::BackendError) {
            ++descr.Properties->ConnStats.BackendError;
            if (isTimedout) {
                ++descr.Properties->ConnStats.BackendTimeout;
            }
            if (error.Flags & TErrorWithComment::WriteError) {
                ++descr.Properties->ConnStats.BackendWriteError;
                if (ctx.TransferedWholeResponse) {
                    ++descr.Properties->ConnStats.BackendShortReadAnswer;
                }
            }
        } else {
            ++descr.Properties->ConnStats.ClientError;
            if (isTimedout) {
                ++descr.Properties->ConnStats.ClientTimeout;
            }
        }
    }

    void TModuleProxy::LogErrors(const TIgnoredErrors& ignoredErrors, const TErrorWithComment& error, const TConnDescr& descr, const TErrorCtx& ctx) const noexcept {
        if (error) {
            UpdateErrorStats(error, descr, ctx);
        }

        descr.ExtraAccessLog << ' ' << ctx.RespDuration << '/' << ctx.SessDuration;
        descr.ExtraAccessLog << "/connect=" << ctx.ConnDuration;
        descr.ExtraAccessLog << ' ' << ctx.BodyFromClient << '/';
        descr.ExtraAccessLog << ctx.RespSize << ' ' << ctx.RawFromBackend - ctx.RespSize;
        descr.ExtraAccessLog << "/sc=" << ctx.StatusCode;

        bool hadErrors = false;
        for (const auto& err : ignoredErrors.Errors) {
            if (hadErrors) {
                descr.ExtraAccessLog << " |";
            }
            hadErrors = true;
            LogErrorSection(err, descr, ctx);
        }
        if (hadErrors) {
            descr.ExtraAccessLog << " |";
        }
        if (error) {
            TString reason;
            LogErrorSection(error, descr, ctx, &reason);
            descr.ExtraAccessLog.SetSummary(NAME, std::move(reason));
        } else {
            descr.ExtraAccessLog.SetSummary(NAME, "success");
        }
    }

    void TModuleProxy::LogErrorSection(const TErrorWithComment& error, const TConnDescr& descr, const TErrorCtx& ctx, TString* reason) const noexcept {
        const auto* backendError = error.Error.GetAs<TBackendError>();
        const TError& err = backendError ? backendError->InnerError() : error.Error;

        TStringBuilder builder;
        builder << error.Comment;
        if (const auto* hpe = err.GetAs<THttpParseError>()) {
            builder << " http_parse_error " << hpe->Code();
            LOG_ERROR(TLOG_ERR, descr,
                error.Comment << " error " << ctx.Ip << ", " << hpe->Code() << ", " << hpe->Chunks());
        } else if (const auto* he = err.GetAs<THttpError>()) {
            builder << " http_error " << he->Code();
            if (he->Reason()) {
                builder << ' ' << he->Reason();
            }
            LOG_ERROR(TLOG_ERR, descr,
                error.Comment << " error " << ctx.Ip << ", " << he->Code() << ", " << he->what());
        } else if (const auto* se = err.GetAs<TSystemError>()) {
            builder << " system_error " << TErrno(se->Status());
            LOG_ERROR(TLOG_ERR, descr,
                error.Comment << " error " << ctx.Ip << ", " << TErrno(se->Status()) << ", " << se->what());
        } else if (err.GetAs<TSslError>()) {
            builder << " ssl_error";
            LOG_ERROR(TLOG_ERR, descr,
                error.Comment << " error " << ctx.Ip << ", " << err->what() << ", ssl error");
        } else if (err.GetAs<TOnStatusCodeError>()) {
            builder << " matched_status";
        } else {
            builder << " unknown_error";
            LOG_ERROR(TLOG_ERR, descr,
                error.Comment << " error " << ctx.Ip << ", unknown error");
        }
        descr.ExtraAccessLog << ' ' << builder;
        if (reason) {
            *reason = std::move(builder);
        }
    }

    void TModuleProxy::LogHttp2RequestSummary(const TConnDescr& descr, const NBalancerClient::THttp2RequestSummary& requestSummary) const noexcept {
        Y_UNUSED(requestSummary);

        descr.ExtraAccessLog.SetSummary(NAME, "http2 backend request");
    }

    bool TModuleProxy::IsValidKeepAlive(TKeepAliveData* keepAlive, const TConnDescr& descr) const noexcept {
        if (!keepAlive) {
            return false;
        }
        ESocketReadStatus status = HasSocketDataToRead(keepAlive->Sock());
        if (status == ESocketReadStatus::SocketClosed || (ProxyConfig_.KeepAliveCheckForUnexpectedData && status == ESocketReadStatus::HasData)) {
            return false;
        }
        return keepAlive->CompatibleSniHost(*descr.Request);
    }

    TErrorOr<THolder<TKeepAliveData>> TModuleProxy::GetKeepAliveConnection(
        const TConnDescr& descr,
        TTls& tls,
        const THostInfo& hostInfo,
        const TInstant start,
        TDuration& connectDuration,
        TInstant& effectiveSessionDeadline
    ) const noexcept
    {
        THolder<TKeepAliveData> keepAlive = tls.GetKeepAliveConnection(descr, hostInfo);

        if (!IsValidKeepAlive(keepAlive.Get(), descr)) {
            TInstant connectDeadline;
            Y_PROPAGATE_ERROR(EstablishConnection(
                    descr, hostInfo, ProxyConfig_.BackendConfig, PM_EDGE_TRIGGERED, start, connectDuration, connectDeadline, effectiveSessionDeadline).AssignTo(keepAlive));
        } else {
            ++descr.Properties->ConnStats.BackendKeepaliveReused;
        }

        keepAlive->SetCanStore(tls.CanStoreKeepaliveConnection(descr, hostInfo));

        return keepAlive;
    }

    void TModuleProxy::SetupBackendIo(
        const TConnDescr& descr,
        IIoInput* input,
        IIoOutput* output,
        TMaybe<TFromBackendDecoder>& fromBackendDecoder,
        TMaybe<TToBackendEncoder>& toBackendEncoder,
        TMaybe<TBackendInput>& backendInput,
        TMaybe<TBackendOutput>& backendOutput
    ) const noexcept
    {
        if (descr.Request) {
            THttpParseOptions options;
            options.KeepAllHeaders = ProxyConfig_.KeepAllHeaders;
            fromBackendDecoder.ConstructInPlace(input, *descr.Request, options);
            backendInput.ConstructInPlace(fromBackendDecoder.Get());

            toBackendEncoder.ConstructInPlace(output);
            backendOutput.ConstructInPlace(toBackendEncoder.Get());
        } else {
            backendInput.ConstructInPlace(input);
            backendOutput.ConstructInPlace(output);
        }
    }

    void TModuleProxy::PrepareRequestHeadersAndProps(
        const TConnDescr& descr
    ) const noexcept
    {
        TRequest* const request = descr.Request;
        if (request) {
            auto& requestHeaders = request->Headers();
            auto& requestProps = request->Props();

            if (ProxyConfig_.AllowConnectionUpgradeWithoutConnectionHeader && !requestProps.UpgradeRequested) {
                Y_ASSERT(ProxyConfig_.AllowConnectionUpgrade);
                if (requestHeaders.FindValues("Upgrade") != requestHeaders.end()) {
                    requestProps.UpgradeRequested = true;
                }
            }

            RemoveHeaders(&requestHeaders);

            if (!ProxyConfig_.AllowConnectionUpgrade) {
                requestProps.UpgradeRequested = false;
                requestHeaders.Delete(TUpgradeFsm::Instance());
            }
        }
    }

    TModuleProxy::TErrorWithComment TModuleProxy::WaitTransfers(
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        TCoroSingleCondVar& oneOfTransfersEnded
    ) const noexcept
    {
        while (clientToBackend.Running() && backendToClient.Running()) {
            const int ret = oneOfTransfersEnded.wait(RunningCont()->Executor());
            if (ret != EWAKEDUP) {
                return TErrorWithComment{
                    Y_MAKE_ERROR(TBackendError{Y_MAKE_ERROR(TSystemError{ret})}),
                    "backend",
                    TErrorWithComment::BackendError
                };
            }
        }
        return {{}, {}};
    }

    void TModuleProxy::FinishTransfers(
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        bool alwaysCancel
    ) const noexcept
    {
        if (clientToBackend.Running() && (alwaysCancel || backendToClient.AnyError())){
            clientToBackend.Cancel();
        }
        if (backendToClient.Running() && (alwaysCancel || clientToBackend.AnyError())) {
            backendToClient.Cancel();
        }
        clientToBackend.Join();
        backendToClient.Join();
    }

    TModuleProxy::TErrorWithComment TModuleProxy::CheckBackendError(TTransfer& transfer, TStringBuf comment, uint64_t flags) const noexcept {
        if (transfer.BackendError()) {
            return TErrorWithComment{
                WrapIntoBackendError(std::move(transfer.BackendError())),
                comment,
                flags | TErrorWithComment::BackendError
            };
        }
        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::CheckClientError(TTransfer& transfer, TStringBuf comment, uint64_t flags) const noexcept {
        if (transfer.ClientError()) {
            return TErrorWithComment{
                    std::move(transfer.ClientError()),
                    comment,
                    flags
            };
        }
        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::CheckTransfers(
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        bool checkBackendReadError,
        bool& transferedWholeResponse
    ) const noexcept
    {
        transferedWholeResponse = backendToClient.Eof() && !backendToClient.AnyError();
        if (clientToBackend.Cancelled()) {
            Y_PROPAGATE_ERROR(CheckClientError(backendToClient, "client write", TErrorWithComment::WriteError));
            Y_PROPAGATE_ERROR(CheckBackendError(backendToClient, "backend read", 0));
            return {{}, {}};
        }
        if (backendToClient.Cancelled()) {
            Y_PROPAGATE_ERROR(CheckClientError(clientToBackend, "client read", 0));
            Y_PROPAGATE_ERROR(CheckBackendError(clientToBackend, "backend write", TErrorWithComment::WriteError));
            return {{}, {}};
        }
        Y_PROPAGATE_ERROR(CheckClientError(clientToBackend, "client read", 0));
        Y_PROPAGATE_ERROR(CheckClientError(backendToClient, "client write", TErrorWithComment::WriteError));
        if (checkBackendReadError) {
            Y_PROPAGATE_ERROR(CheckBackendError(backendToClient, "backend read", 0));
        }
        Y_PROPAGATE_ERROR(CheckBackendError(clientToBackend, "backend write", TErrorWithComment::WriteError));
        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::DoRunTcpTransfers(
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        TCoroSingleCondVar& oneOfTransfersEnded
    ) const noexcept
    {
        backendToClient.Start("backendToClientTcp", RunningCont()->Executor());
        clientToBackend.Start("clientToBackendTcp", RunningCont()->Executor());

        Y_PROPAGATE_ERROR(WaitTransfers(backendToClient, clientToBackend, oneOfTransfersEnded));

        FinishTransfers(backendToClient, clientToBackend, true);

        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::DoRunWebsocketTransfers(
        const TConnDescr& descr,
        TFromBackendDecoder* fromBackendDecoder,
        TToBackendEncoder* toBackendEncoder,
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        TCoroSingleCondVar& oneOfTransfersEnded,
        TInstant& effectiveSessionDeadline
    ) const noexcept
    {
        // We have backend_timeout valid for HTTP protocol, but after switching
        // to WebSocket this timeout could be so small that WebSocket session will
        // terminate before all frames will be transferred.
        effectiveSessionDeadline = Now() + ProxyConfig_.SwitchedBackendTimeout;

        backendToClient.SetSessionDeadline(effectiveSessionDeadline);
        clientToBackend.SetSessionDeadline(effectiveSessionDeadline);

        if (descr.HttpDecoder) {
            descr.HttpDecoder->SwitchProtocols();
        }

        fromBackendDecoder->SwitchProtocols();
        toBackendEncoder->SwitchProtocols();
        backendToClient.Start("backendToClientSwitchedProtocols", RunningCont()->Executor());
        clientToBackend.Start("clientToBackendSwitchedProtocols", RunningCont()->Executor());
        Y_PROPAGATE_ERROR(WaitTransfers(backendToClient, clientToBackend, oneOfTransfersEnded));

        FinishTransfers(backendToClient, clientToBackend, true);

        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::StartPassHttpRequest(
        const TConnDescr& descr,
        TToBackendEncoder* toBackendEncoder,
        TTransfer& clientToBackend,
        TInstant effectiveSessionDeadline,
        TKeepAliveData* keepAlive
    ) const noexcept {
        LOG_ERROR(TLOG_INFO, descr, "encoding request");
        TMaybe<TChunkList> body;
        Y_TRY(TError, error) {
            if (descr.HaveFullBody) {
                Y_PROPAGATE_ERROR(descr.Input->FillBuffer(Max<size_t>(), TInstant::Zero()).ReleaseError());
                body = descr.Input->RecvBuffered();
            }
            return {};
        } Y_CATCH {
            return TErrorWithComment{
                std::move(error),
                "client read",
                0
            };
        }
        Y_TRY(TError, error) {
            return toBackendEncoder->WriteRequest(
                *descr.Request,
                ProxyConfig_.KeepAliveCount && !descr.Properties->Parent.SkipKeepalive,
                std::move(body),
                effectiveSessionDeadline
            );
        } Y_CATCH {
            keepAlive->SetCanStore(false);
            return TErrorWithComment{
                Y_MAKE_ERROR(TBackendError{std::move(error)}),
                "backend write",
                TErrorWithComment::BackendError | TErrorWithComment::WriteError
            };
        }
        LOG_ERROR(TLOG_INFO, descr, "encoded request");

        if (descr.HaveFullBody) {
            clientToBackend.Finish();
        } else if (!descr.Request->Props().UpgradeRequested) {
            clientToBackend.Start("clientToBackend", RunningCont()->Executor());
        }

        return{{}, {}};
    }

    bool TModuleProxy::TryRecvResponse(TFromBackendDecoder* fromBackendDecoder) const noexcept {
        while (true) {
            TChunkList lst;
            if (TError error = fromBackendDecoder->Recv(lst, TInstant::Now())) {
                return false;
            }
            if (lst.Empty()) {
                return true;
            }
        }
        return false;
    }

    TModuleProxy::TErrorWithComment TModuleProxy::HandleHttpResponse(
        const TConnDescr& descr,
        TTls& tls,
        TFromBackendDecoder* fromBackendDecoder,
        IHttpOutput* const toClientOut,
        TResponse& response,
        bool& upgradeCompleted,
        const TInstant start,
        TDuration& responseDuration,
        const TInstant effectiveSessionDeadline,
        NSrvKernel::TCountInput& countBackendInput,
        size_t* responseSize,
        TKeepAliveData* keepAlive
    ) const noexcept {
        bool need100ContinueProcessing = false;
        bool mayPass100Continue = false;

        while (true) {
            LOG_ERROR(TLOG_INFO, descr, "decoding response");
            if (auto error = fromBackendDecoder->ReadResponse(response, effectiveSessionDeadline)) {
                keepAlive->SetCanStore(false);
                if (const auto* parseError = error.GetAs<THttpParseError>()) {
                    if (!error.GetAs<THttpParseIncompleteInputError>()) {
                        ++tls.HttpCompleteResponseParseError;
                    }
                    ++tls.HttpParseError;
                    LOG_ERROR(TLOG_ERR, descr, "http response parse error: " << parseError->Code() <<
                        " " << ToString(parseError->Chunks()));
                }
                return TErrorWithComment{
                    WrapIntoBackendError(std::move(error)),
                    "backend read",
                    TErrorWithComment::BackendError
                };
            }
            LOG_ERROR(TLOG_INFO, descr, "decoded response");
            *responseSize = countBackendInput.Readed();

            responseDuration = Now() - start;
            auto status = response.ResponseLine().StatusCode;
            descr.Properties->ConnStats.OnStatusCode(status);

            if (response.Props().ChunkedTransfer) {
                if (response.Props().ContentLength) {
                    LOG_ERROR(TLOG_ERR, descr,
                              "backend warning: both Content-Length "
                              "and chunked Transfer-Encoding present");
                    keepAlive->SetCanStore(false);
                }
                if (response.Props().Version == 0) {
                    LOG_ERROR(TLOG_ERR, descr,
                              "backend warning: chunked Transfer-Encoding "
                              "response to HTTP/1.0 request");
                    keepAlive->SetCanStore(false);
                }
            } else if (!response.Props().ContentLength) {
                if (EMethod::HEAD != descr.Request->RequestLine().Method
                    && !NullResponseBody(response.ResponseLine().StatusCode)) {
                    keepAlive->SetCanStore(false);
                }
            }

            if (!response.Props().KeepAlive) {
                keepAlive->SetCanStore(false);
            }
            if (descr.Request->Props().Version == 0
                && response.Props().Version == 1
                && !response.Props().ExplicitKeepAliveHeader) {
                keepAlive->SetCanStore(false);
            }

            if (ProxyConfig_.StatusCodeReactions.IsBad(status)) {
                if (keepAlive->CanStore() && !(descr.Request->Props().TransferedWholeRequest && TryRecvResponse(fromBackendDecoder))) {
                    keepAlive->SetCanStore(false);
                }
                return TErrorWithComment{
                    Y_MAKE_ERROR(TBackendError{Y_MAKE_ERROR(THttpError(status))}),
                    "backend",
                    TErrorWithComment::BackendError
                };
            }

            if (status == HTTP_SWITCHING_PROTOCOLS && !ProxyConfig_.AllowConnectionUpgrade) {
                keepAlive->SetCanStore(false);
                LOG_ERROR(TLOG_ERR, descr,
                          "backend warning: switching protocols "
                          "while allow_connection_upgrade = false");
                return TErrorWithComment{
                    Y_MAKE_ERROR(TBackendError{Y_MAKE_ERROR(TForceStreamClose{})}),
                    "backend",
                    TErrorWithComment::BackendError
                };
            }

            if (status == HTTP_CONTINUE) {
                if (!need100ContinueProcessing) {
                    need100ContinueProcessing = true;
                    mayPass100Continue = MayPass100Continue(descr.Request);
                }

                if (mayPass100Continue) {
                    Y_TRY(TError, error) {
                        Y_PROPAGATE_ERROR(toClientOut->SendHead(std::move(response), false, effectiveSessionDeadline));
                        Y_PROPAGATE_ERROR(toClientOut->SendEof(effectiveSessionDeadline));
                        return {};
                    } Y_CATCH {
                        keepAlive->SetCanStore(false);
                        if (error.GetAs<TBackendError>()) {
                            // The above module treated the answer as backend fail
                            return TErrorWithComment{
                                    std::move(error),
                                    "backend",
                                    TErrorWithComment::BackendError
                            };
                        }
                        return TErrorWithComment{
                            std::move(error),
                            "client write",
                            TErrorWithComment::WriteError
                        };
                    }
                }

                response = {};
                continue;
            }

            if (status == HTTP_SWITCHING_PROTOCOLS) {
                upgradeCompleted = true;
                keepAlive->SetCanStore(false);
            }

            Y_TRY(TError, error) {
                // TODO(tender-bum): move response to Encode
                Y_PROPAGATE_ERROR(toClientOut->SendHead(std::move(response), false, effectiveSessionDeadline));
                return {};
            } Y_CATCH {
                if (keepAlive->CanStore() && !(descr.Request->Props().TransferedWholeRequest && TryRecvResponse(fromBackendDecoder))) {
                    keepAlive->SetCanStore(false);
                }
                if (error.GetAs<TBackendError>()) {
                    // The above module treated the answer as backend fail
                    return TErrorWithComment{
                        std::move(error),
                        "backend",
                        TErrorWithComment::BackendError
                    };
                }
                return TErrorWithComment{
                    std::move(error),
                    "client write",
                    TErrorWithComment::WriteError
                };
            }

            break;
        }

        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::DoRunHttpTransfers(
        const TConnDescr& descr,
        TTransfer& backendToClient,
        TTransfer& clientToBackend,
        bool watchClientClose
    ) const noexcept
    {
        Y_UNUSED(descr);
        backendToClient.Run(RunningCont()->Executor());

        if (watchClientClose) {
            clientToBackend.FromBackendTransferFinished();
            if (clientToBackend.Running() && clientToBackend.Eof()) {
                clientToBackend.Cancel();
            }
        }

        clientToBackend.Join();

        return {{}, {}};
    }

    TModuleProxy::TErrorWithComment TModuleProxy::DoRunTransfers(
        const TConnDescr& descr,
        TTls& tls,
        const THostInfo& hostInfo,
        TCountInput& countBackendInput,
        TCountInput& countClientInput,
        TKeepAliveData* keepAlive,
        TResponse& response,
        const TInstant& start,
        TInstant& effectiveSessionDeadline,
        TDuration& responseDuration,
        TIgnoredErrors& ignoredErrors,
        size_t* responseSize,
        bool& transferedWholeResponse
    ) const noexcept
    {
        TRequest* const request = descr.Request;

        TMaybe<TFromBackendDecoder> fromBackendDecoder;
        TMaybe<TToBackendEncoder> toBackendEncoder;
        TMaybe<TBackendInput> backendInput;
        TMaybe<TBackendOutput> backendOutput;
        SetupBackendIo(
            descr,
            &countBackendInput,
            &keepAlive->Output(),
            fromBackendDecoder,
            toBackendEncoder,
            backendInput,
            backendOutput
        );

        TBufferingOutput toClientBuffer{descr.Output};
        IHttpOutput* const toClientOut = ProxyConfig_.Buffering ? static_cast<IHttpOutput*>(&toClientBuffer)
                                                                : static_cast<IHttpOutput*>(descr.Output);
        bool watchClientClose = ProxyConfig_.WatchClientClose
            && request // not tcp proxy
            && descr.Properties->SocketIo; // not http2

        PrepareRequestHeadersAndProps(descr);

        TCoroSingleCondVar oneOfTransfersEnded;

        TTransfer backendToClient{
            descr,
            backendInput.Get(),
            toClientOut,
            ProxyConfig_.BackendReadTimeout,
            ProxyConfig_.ClientWriteTimeout,
            effectiveSessionDeadline,
            &oneOfTransfersEnded,
            &keepAlive->SocketIo(),
            LogPrefix(descr, hostInfo.Host, hostInfo.Port, ETransferType::BackendToClient),
            RunningCont(),
            false,
            false
        };
        TTransfer clientToBackend{
            descr,
            &countClientInput,
            backendOutput.Get(),
            ProxyConfig_.ClientReadTimeout,
            ProxyConfig_.BackendWriteTimeout,
            effectiveSessionDeadline,
            &oneOfTransfersEnded,
            descr.Properties->SocketIo,
            LogPrefix(descr, hostInfo.Host, hostInfo.Port, ETransferType::ClientToBackend),
            RunningCont(),
            watchClientClose,
            true
        };

        Y_DEFER {
            CollectIgnoredErrors(backendToClient, clientToBackend, ignoredErrors);
        };

        if (!request) {
            Y_PROPAGATE_ERROR(DoRunTcpTransfers(backendToClient, clientToBackend, oneOfTransfersEnded));
            return CheckTransfers(backendToClient, clientToBackend, true, transferedWholeResponse);
        }

        Y_PROPAGATE_ERROR(StartPassHttpRequest(descr, toBackendEncoder.Get(), clientToBackend, effectiveSessionDeadline, keepAlive));
        bool upgradeCompleted = false;
        Y_TRY(TErrorWithComment, error) {
            Y_TRY(TErrorWithComment, err) {
                return HandleHttpResponse(
                    descr,
                    tls,
                    fromBackendDecoder.Get(),
                    toClientOut,
                    response,
                    upgradeCompleted,
                    start,
                    responseDuration,
                    effectiveSessionDeadline,
                    countBackendInput,
                    responseSize,
                    keepAlive
                );
            }
            Y_CATCH {
                if (clientToBackend.ClientError()) {
                    ignoredErrors.Add(std::move(err));
                    return CheckClientError(clientToBackend, "client read", 0);
                }
                return err;
            };
            if (!upgradeCompleted) {
                Y_DEFER {
                    if (keepAlive->CanStore()) {
                        if (!descr.Request->Props().TransferedWholeRequest
                            || !(transferedWholeResponse || TryRecvResponse(fromBackendDecoder.Get()))) {
                            keepAlive->SetCanStore(false);
                        }
                    }
                };
                Y_PROPAGATE_ERROR(DoRunHttpTransfers(descr, backendToClient, clientToBackend, watchClientClose));
                return CheckTransfers(backendToClient, clientToBackend, true, transferedWholeResponse);
            }
            return {{}, {}};
        } Y_CATCH {
            if (watchClientClose && clientToBackend.WatchReadSocketError()) {
                ignoredErrors.Add(std::move(error));
                return TErrorWithComment{
                    std::move(clientToBackend.WatchReadSocketError()),
                    "client watch",
                    0
                };
            }

            clientToBackend.Cancel();
            clientToBackend.Join();

            if (IsCancelledTransfer(error.Error) && RunningCont()->Cancelled()) {
                ignoredErrors.Add(std::move(error));
                return TErrorWithComment{
                    Y_MAKE_ERROR(TSystemError{ECANCELED} << "main cont canceled"),
                    "client cancel",
                    0
                };
            }
            return error;
        }

        if (upgradeCompleted) {
            Y_PROPAGATE_ERROR(DoRunWebsocketTransfers(
                descr,
                fromBackendDecoder.Get(),
                toBackendEncoder.Get(),
                backendToClient,
                clientToBackend,
                oneOfTransfersEnded,
                effectiveSessionDeadline
            ));
            return CheckTransfers(backendToClient, clientToBackend, !clientToBackend.Eof(), transferedWholeResponse);
        }

        return {{}, {}};
    }

    TError TModuleProxy::Proxy(const TConnDescr& oldDescr, TTls& tls) const noexcept {
        TConnDescr descr = oldDescr.Copy();

        ++descr.Properties->ConnStats.BackendAttempt;

        THostInfo hostInfo{ProxyConfig_.BackendConfig.host(), ProxyConfig_.BackendConfig.cached_ip(), ProxyConfig_.BackendConfig.port(), false};
        if (descr.Request && descr.Properties && descr.Properties->SrcrwrAddrs) {
            TError error = HandleSrcrwr(descr, hostInfo);
            if (error) {
                descr.ExtraAccessLog.SetSummary(NAME, "srcrwr error");
                return error;
            }
        }

        descr.ExtraAccessLog << ' ' << hostInfo.Host << ':' << hostInfo.Port;

        if (descr.AttemptsHolder) {
            descr.AttemptsHolder->ModifyRequest(*descr.Request, hostInfo);
        }

        // if GetKeepAliveConnection failed, set host:port for AttemptsHolder RequestEndpoint_
        if (descr.AttemptsHolder) {
            descr.AttemptsHolder->SetEndpoint(hostInfo.Host, hostInfo.Port, descr.RequestType == ERequestType::Hedged);
        }

        if (descr.ClientRequest) {
            descr.ExtraAccessLog.SetSummary(NAME, "client request");
            return descr.ClientRequest->Run(descr, tls.State, hostInfo);
        }

        // Create Http2Request if ClientRequest is not defined
        if (ProxyConfig_.Http2Backend || (ProxyConfig_.UseSameHttpVersion && IsHTTP2(descr.Request))) {
            // "TE: Trailers" not supported in http/1.1
            if (descr.Request && !IsHTTP2(descr.Request)) {
                descr.Request->Headers().Delete(TStringBuf{NSrvKernel::NHTTP2::REQ_HEADER_TE});
            }

            //TODO: proper logging for http2
            NBalancerClient::THttp2Request request;
            auto error = request.Run(descr, tls.State, hostInfo);
            
            auto requestSummary = request.GetSummary();

            if (requestSummary) {
                LogHttp2RequestSummary(descr, requestSummary.GetRef());
            } else {
                descr.ExtraAccessLog.SetSummary(NAME, "http2 request error");
                LOG_ERROR(TLOG_ERR, descr, "no summary from http2 request");
            }

            return error;
        }

        const TInstant start = Now();

        TInstant effectiveSessionDeadline = Now() + ProxyConfig_.BackendConfig.backend_timeout();
        TDuration connectDuration = TDuration::Zero(); // 0 in logs = reused keep-alive
        THolder<TKeepAliveData> keepAlive;

        if (TError error = GetKeepAliveConnection(descr, tls, hostInfo, start, connectDuration, effectiveSessionDeadline).AssignTo(keepAlive)) {
            descr.ExtraAccessLog.SetSummary(NAME, "connect error");
            return error;
        }
        tls.ConnectionAccessTimes.AddValue((Now() - start).MicroSeconds());
        const TString usedAddr = keepAlive->AddrStr();

        LOG_ERROR(TLOG_INFO, descr, "backend run");

        if (descr.AttemptsHolder) {
            descr.AttemptsHolder->SetEndpoint(usedAddr, descr.RequestType == ERequestType::Hedged);
        }

        TCountInput countClientInput{descr.Input};
        TCountInput countBackendInput{&keepAlive->Input()};

        size_t responseSize = 0;
        TResponse response;
        TDuration responseDuration;
        bool transferedWholeResponse = false;
        TIgnoredErrors ignoredErrors;
        TErrorWithComment result = DoRunTransfers(
            descr, tls, hostInfo, countBackendInput, countClientInput, keepAlive.Get(), response,
            start, effectiveSessionDeadline, responseDuration, ignoredErrors, &responseSize, transferedWholeResponse
        );

        TErrorWithComment clientFlushError{{}, {}};
        TCoroutine clientFlushCoro;
        if (!result && descr.Properties->SocketIo) {
            clientFlushCoro = TCoroutine(ECoroType::Service, "client_flush", RunningCont()->Executor(),
                                         [this, &descr, &transferedWholeResponse, &clientFlushError](){
                                             Y_TRY(TError, error) {
                                                 return descr.Properties->SocketIo->Out().Flush(TSocketOut::FlushAll, ProxyConfig_.ClientWriteTimeout.ToDeadLine());
                                             } Y_CATCH {
                                                 transferedWholeResponse = false;
                                                 clientFlushError = {std::move(error), "client flush", TErrorWithComment::WriteError};
                                             }
            });
        }

        if (!result || keepAlive->CanStore()) {
            auto flushDeadline = Min(ProxyConfig_.BackendWriteTimeout.ToDeadLine(), effectiveSessionDeadline);
            Y_TRY(TError, error) {
                return keepAlive->SocketIo().Out().Flush(TSocketOut::FlushAll, flushDeadline);
            } Y_CATCH {
                keepAlive->SetCanStore(false);
                TErrorWithComment err{
                    Y_MAKE_ERROR(TBackendError{std::move(error)}),
                    "backend flush",
                    TErrorWithComment::BackendError | TErrorWithComment::WriteError
                };
                if (!result) {
                    result = std::move(err);
                } else {
                    ignoredErrors.Add(std::move(err));
                }
            }
        }

        if (keepAlive->CanStore()) {
            tls.StoreKeepaliveConnection(std::move(keepAlive));
        }

        ui16 statusCode = countBackendInput.Readed() ? response.ResponseLine().StatusCode : 0;

        clientFlushCoro.Join();
        if (clientFlushError) {
            if (result) {
                ignoredErrors.Add(std::move(result));
            }
            result = std::move(clientFlushError);
        }

        LogErrors(ignoredErrors, result, descr, {
            .Ip = usedAddr,
            .ConnDuration = connectDuration,
            .RespDuration = responseDuration,
            .SessDuration = Now() - start,
            .BodyFromClient = countClientInput.Readed(),
            .RawFromBackend = countBackendInput.Readed(),
            .RespSize = responseSize,
            .TransferedWholeResponse = transferedWholeResponse,
            .StatusCode = statusCode,
        });
        if (result.Error) {
            return std::move(result.Error);
        }

        descr.ExtraAccessLog << " succ " << statusCode;

        LOG_ERROR(TLOG_INFO, descr, "backend succeeded");
        return {};
    }
} // namespace NModProxy
