Skip to content

Stream callbacks are triggered at StreamClose even when StreamStart failed synchronously #6223

Description

Describe the bug

If StreamStart is called after the connection has received shutdown-complete, StreamStart fails with 0x80004004, yet the application still receives callbacks that were stated to never be delivered.

Documentation explicitly indicates no callbacks should be delivered for such streams,

Important - No events are delivered on the stream until the app calls StreamStart (because of the race conditions that could occur) and it succeeds. This means that if the parent connection is shutdown (e.g. idle timeout or peer initiated) before calling StreamStart then the QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE will not be delivered. So, apps that rely on that event to trigger clean up of the stream must handle the case where StreamStart is either not ever called or fails and clean up directly.

StreamStart function

Affected OS

  • Windows
  • Linux
  • macOS
  • Other (specify below)

Additional OS information

Windows Version 25H2 OS Build 26200.8893

MsQuic version

v2.5.6

Steps taken to reproduce bug

Asked AI to generate a quick example repro.:
note using QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAIL does not change the behavior,

#include <windows.h>
#include <wincrypt.h>
#include <ncrypt.h>

#include <atomic>
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

#include "msquic.h"

namespace {

constexpr char kAlpnValue[] = "stream-close-repro";
constexpr auto kWaitTimeout = std::chrono::seconds(15);

std::mutex gLogLock;

void Log(const char* format, ...)
{
    std::lock_guard<std::mutex> lock(gLogLock);
    std::printf("[thread %lu] ", GetCurrentThreadId());

    va_list arguments;
    va_start(arguments, format);
    std::vprintf(format, arguments);
    va_end(arguments);

    std::printf("\n");
    std::fflush(stdout);
}

const char* StreamEventName(QUIC_STREAM_EVENT_TYPE type)
{
    switch (type) {
    case QUIC_STREAM_EVENT_START_COMPLETE: return "START_COMPLETE";
    case QUIC_STREAM_EVENT_RECEIVE: return "RECEIVE";
    case QUIC_STREAM_EVENT_SEND_COMPLETE: return "SEND_COMPLETE";
    case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: return "PEER_SEND_SHUTDOWN";
    case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: return "PEER_SEND_ABORTED";
    case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: return "PEER_RECEIVE_ABORTED";
    case QUIC_STREAM_EVENT_SEND_SHUTDOWN_COMPLETE: return "SEND_SHUTDOWN_COMPLETE";
    case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: return "SHUTDOWN_COMPLETE";
    case QUIC_STREAM_EVENT_IDEAL_SEND_BUFFER_SIZE: return "IDEAL_SEND_BUFFER_SIZE";
    case QUIC_STREAM_EVENT_PEER_ACCEPTED: return "PEER_ACCEPTED";
    case QUIC_STREAM_EVENT_CANCEL_ON_LOSS: return "CANCEL_ON_LOSS";
    default: return "UNKNOWN";
    }
}

class RuntimeCertificate final {
public:
    RuntimeCertificate() = default;
    RuntimeCertificate(const RuntimeCertificate&) = delete;
    RuntimeCertificate& operator=(const RuntimeCertificate&) = delete;

    ~RuntimeCertificate()
    {
        Reset();
    }

    bool Create()
    {
        wchar_t keyName[96]{};
        _snwprintf_s(
            keyName,
            _countof(keyName),
            _TRUNCATE,
            L"MsQuicStreamCloseRepro-%lu-%llu",
            GetCurrentProcessId(),
            GetTickCount64());
        KeyName = keyName;

        SECURITY_STATUS securityStatus =
            NCryptOpenStorageProvider(&Provider, MS_KEY_STORAGE_PROVIDER, 0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptOpenStorageProvider failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        securityStatus =
            NCryptCreatePersistedKey(
                Provider,
                &Key,
                NCRYPT_RSA_ALGORITHM,
                KeyName.c_str(),
                0,
                0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptCreatePersistedKey failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        DWORD keyLength = 2048;
        securityStatus =
            NCryptSetProperty(
                Key,
                NCRYPT_LENGTH_PROPERTY,
                reinterpret_cast<PBYTE>(&keyLength),
                sizeof(keyLength),
                0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptSetProperty(length) failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        DWORD keyUsage = NCRYPT_ALLOW_SIGNING_FLAG;
        securityStatus =
            NCryptSetProperty(
                Key,
                NCRYPT_KEY_USAGE_PROPERTY,
                reinterpret_cast<PBYTE>(&keyUsage),
                sizeof(keyUsage),
                0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptSetProperty(usage) failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        DWORD exportPolicy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
        securityStatus =
            NCryptSetProperty(
                Key,
                NCRYPT_EXPORT_POLICY_PROPERTY,
                reinterpret_cast<PBYTE>(&exportPolicy),
                sizeof(exportPolicy),
                0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptSetProperty(export policy) failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        securityStatus = NCryptFinalizeKey(Key, 0);
        if (securityStatus != ERROR_SUCCESS) {
            Log("NCryptFinalizeKey failed: 0x%08X", static_cast<unsigned>(securityStatus));
            return false;
        }

        DWORD subjectLength = 0;
        if (!CertStrToNameW(
                X509_ASN_ENCODING,
                L"CN=localhost",
                CERT_X500_NAME_STR,
                nullptr,
                nullptr,
                &subjectLength,
                nullptr)) {
            Log("CertStrToNameW(size) failed: %lu", GetLastError());
            return false;
        }

        std::vector<BYTE> subject(subjectLength);
        if (!CertStrToNameW(
                X509_ASN_ENCODING,
                L"CN=localhost",
                CERT_X500_NAME_STR,
                nullptr,
                subject.data(),
                &subjectLength,
                nullptr)) {
            Log("CertStrToNameW(data) failed: %lu", GetLastError());
            return false;
        }

        CERT_NAME_BLOB subjectBlob{};
        subjectBlob.cbData = subjectLength;
        subjectBlob.pbData = subject.data();

        CRYPT_KEY_PROV_INFO keyProviderInfo{};
        keyProviderInfo.pwszContainerName = KeyName.data();
        keyProviderInfo.pwszProvName = const_cast<LPWSTR>(MS_KEY_STORAGE_PROVIDER);
        keyProviderInfo.dwKeySpec = AT_KEYEXCHANGE;

        CRYPT_ALGORITHM_IDENTIFIER signatureAlgorithm{};
        signatureAlgorithm.pszObjId = const_cast<LPSTR>(szOID_RSA_SHA256RSA);

        Context =
            CertCreateSelfSignCertificate(
                Key,
                &subjectBlob,
                0,
                &keyProviderInfo,
                &signatureAlgorithm,
                nullptr,
                nullptr,
                nullptr);
        if (Context == nullptr) {
            Log("CertCreateSelfSignCertificate failed: %lu", GetLastError());
            return false;
        }

        Log("created temporary self-signed server certificate (no files or store install)");
        return true;
    }

    PCCERT_CONTEXT Get() const
    {
        return Context;
    }

private:
    void Reset()
    {
        if (Context != nullptr) {
            CertFreeCertificateContext(Context);
            Context = nullptr;
        }

        if (Key != 0) {
            NCryptDeleteKey(Key, NCRYPT_SILENT_FLAG);
            Key = 0;
        }

        if (Provider != 0) {
            NCryptFreeObject(Provider);
            Provider = 0;
        }
    }

    NCRYPT_PROV_HANDLE Provider{0};
    NCRYPT_KEY_HANDLE Key{0};
    PCCERT_CONTEXT Context{nullptr};
    std::wstring KeyName;
};

struct ReproState final {
    const QUIC_API_TABLE* Api{nullptr};
    HQUIC ServerConfiguration{nullptr};
    std::atomic<HQUIC> ServerConnection{nullptr};
    std::atomic<bool> ShutdownRequested{false};
    std::atomic<bool> ClientConnected{false};
    std::atomic<bool> ClientShutdownComplete{false};
    std::atomic<bool> ServerShutdownComplete{false};
    std::atomic<bool> InsideStreamClose{false};
    std::atomic<uint32_t> StreamCallbackCount{0};
    std::atomic<uint32_t> CallbacksInsideStreamClose{0};
};

QUIC_STATUS QUIC_API ServerStreamCallback(
    HQUIC stream,
    void* context,
    QUIC_STREAM_EVENT* event)
{
    auto* state = static_cast<ReproState*>(context);
    Log("server stream %p: %s", stream, StreamEventName(event->Type));

    if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE &&
        !event->SHUTDOWN_COMPLETE.AppCloseInProgress) {
        state->Api->StreamClose(stream);
    }

    return QUIC_STATUS_SUCCESS;
}

QUIC_STATUS QUIC_API ServerConnectionCallback(
    HQUIC connection,
    void* context,
    QUIC_CONNECTION_EVENT* event)
{
    auto* state = static_cast<ReproState*>(context);

    switch (event->Type) {
    case QUIC_CONNECTION_EVENT_CONNECTED:
        Log("server connection %p: CONNECTED", connection);
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
        Log(
            "server connection %p: SHUTDOWN_INITIATED_BY_TRANSPORT status=0x%08X",
            connection,
            static_cast<unsigned>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status));
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
        Log(
            "server connection %p: SHUTDOWN_INITIATED_BY_PEER error=%llu",
            connection,
            static_cast<unsigned long long>(event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode));
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
        Log("server connection %p: SHUTDOWN_COMPLETE", connection);
        state->ServerShutdownComplete.store(true, std::memory_order_release);
        break;
    case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
        Log("server connection %p: PEER_STREAM_STARTED %p", connection, event->PEER_STREAM_STARTED.Stream);
        state->Api->SetCallbackHandler(
            event->PEER_STREAM_STARTED.Stream,
            reinterpret_cast<void*>(ServerStreamCallback),
            state);
        break;
    default:
        break;
    }

    return QUIC_STATUS_SUCCESS;
}

QUIC_STATUS QUIC_API ListenerCallback(
    HQUIC listener,
    void* context,
    QUIC_LISTENER_EVENT* event)
{
    auto* state = static_cast<ReproState*>(context);

    if (event->Type != QUIC_LISTENER_EVENT_NEW_CONNECTION) {
        return QUIC_STATUS_NOT_SUPPORTED;
    }

    HQUIC connection = event->NEW_CONNECTION.Connection;
    Log("listener %p: NEW_CONNECTION %p", listener, connection);
    state->ServerConnection.store(connection, std::memory_order_release);
    state->Api->SetCallbackHandler(
        connection,
        reinterpret_cast<void*>(ServerConnectionCallback),
        state);
    return state->Api->ConnectionSetConfiguration(connection, state->ServerConfiguration);
}

QUIC_STATUS QUIC_API ClientStreamCallback(
    HQUIC stream,
    void* context,
    QUIC_STREAM_EVENT* event)
{
    auto* state = static_cast<ReproState*>(context);
    const bool insideClose = state->InsideStreamClose.load(std::memory_order_acquire);
    state->StreamCallbackCount.fetch_add(1, std::memory_order_relaxed);
    if (insideClose) {
        state->CallbacksInsideStreamClose.fetch_add(1, std::memory_order_relaxed);
    }

    if (event->Type == QUIC_STREAM_EVENT_START_COMPLETE) {
        Log(
            "control stream %p callback: %s status=0x%08X inside StreamClose=%s",
            stream,
            StreamEventName(event->Type),
            static_cast<unsigned>(event->START_COMPLETE.Status),
            insideClose ? "yes" : "no");
    } else if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE) {
        Log(
            "control stream %p callback: %s app-close=%u connection-shutdown=%u inside StreamClose=%s",
            stream,
            StreamEventName(event->Type),
            event->SHUTDOWN_COMPLETE.AppCloseInProgress,
            event->SHUTDOWN_COMPLETE.ConnectionShutdown,
            insideClose ? "yes" : "no");
    } else {
        Log(
            "control stream %p callback: %s inside StreamClose=%s",
            stream,
            StreamEventName(event->Type),
            insideClose ? "yes" : "no");
    }

    return QUIC_STATUS_SUCCESS;
}

QUIC_STATUS QUIC_API ClientConnectionCallback(
    HQUIC connection,
    void* context,
    QUIC_CONNECTION_EVENT* event)
{
    auto* state = static_cast<ReproState*>(context);

    switch (event->Type) {
    case QUIC_CONNECTION_EVENT_CONNECTED:
        Log("client connection %p: CONNECTED", connection);
        state->ClientConnected.store(true, std::memory_order_release);
        if (!state->ShutdownRequested.exchange(true, std::memory_order_acq_rel)) {
            Log("client connection %p: requesting shutdown while control stream is still unstarted", connection);
            state->Api->ConnectionShutdown(connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
        }
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
        Log(
            "client connection %p: SHUTDOWN_INITIATED_BY_TRANSPORT status=0x%08X",
            connection,
            static_cast<unsigned>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status));
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
        Log(
            "client connection %p: SHUTDOWN_INITIATED_BY_PEER error=%llu",
            connection,
            static_cast<unsigned long long>(event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode));
        break;
    case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
        Log("client connection %p: SHUTDOWN_COMPLETE; toggling atomic flag", connection);
        state->ClientShutdownComplete.store(true, std::memory_order_release);
        break;
    default:
        break;
    }

    return QUIC_STATUS_SUCCESS;
}

bool YieldUntil(
    const char* description,
    const std::atomic<bool>& flag)
{
    const auto deadline = std::chrono::steady_clock::now() + kWaitTimeout;
    uint64_t yieldCount = 0;

    while (!flag.load(std::memory_order_acquire)) {
        if (std::chrono::steady_clock::now() >= deadline) {
            Log("timed out yielding for %s", description);
            return false;
        }
        ++yieldCount;
        std::this_thread::yield();
    }

    Log("observed %s after %llu yields", description, static_cast<unsigned long long>(yieldCount));
    return true;
}

bool CheckStatus(const char* operation, QUIC_STATUS status)
{
    if (QUIC_FAILED(status)) {
        Log("%s failed: 0x%08X", operation, static_cast<unsigned>(status));
        return false;
    }
    return true;
}

} // namespace

int main()
{
    int result = 1;
    const QUIC_API_TABLE* api = nullptr;
    HQUIC registration = nullptr;
    HQUIC serverConfiguration = nullptr;
    HQUIC clientConfiguration = nullptr;
    HQUIC listener = nullptr;
    HQUIC clientConnection = nullptr;
    HQUIC controlStream = nullptr;
    RuntimeCertificate certificate;
    ReproState state;

    do {
        if (!CheckStatus("MsQuicOpen2", MsQuicOpen2(&api))) {
            break;
        }
        state.Api = api;

        const QUIC_REGISTRATION_CONFIG registrationConfig{
            "msquic-stream-close-repro",
            QUIC_EXECUTION_PROFILE_LOW_LATENCY};
        if (!CheckStatus(
                "RegistrationOpen",
                api->RegistrationOpen(&registrationConfig, &registration))) {
            break;
        }

        if (!certificate.Create()) {
            break;
        }

        QUIC_BUFFER alpn{
            static_cast<uint32_t>(sizeof(kAlpnValue) - 1),
            reinterpret_cast<uint8_t*>(const_cast<char*>(kAlpnValue))};

        QUIC_SETTINGS serverSettings{};
        serverSettings.IdleTimeoutMs = 5000;
        serverSettings.IsSet.IdleTimeoutMs = TRUE;
        serverSettings.PeerBidiStreamCount = 1;
        serverSettings.IsSet.PeerBidiStreamCount = TRUE;
        if (!CheckStatus(
                "ConfigurationOpen(server)",
                api->ConfigurationOpen(
                    registration,
                    &alpn,
                    1,
                    &serverSettings,
                    sizeof(serverSettings),
                    nullptr,
                    &serverConfiguration))) {
            break;
        }
        state.ServerConfiguration = serverConfiguration;

        QUIC_CREDENTIAL_CONFIG serverCredential{};
        serverCredential.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_CONTEXT;
        serverCredential.CertificateContext =
            const_cast<CERT_CONTEXT*>(certificate.Get());
        if (!CheckStatus(
                "ConfigurationLoadCredential(server)",
                api->ConfigurationLoadCredential(serverConfiguration, &serverCredential))) {
            break;
        }

        QUIC_SETTINGS clientSettings{};
        clientSettings.IdleTimeoutMs = 5000;
        clientSettings.IsSet.IdleTimeoutMs = TRUE;
        if (!CheckStatus(
                "ConfigurationOpen(client)",
                api->ConfigurationOpen(
                    registration,
                    &alpn,
                    1,
                    &clientSettings,
                    sizeof(clientSettings),
                    nullptr,
                    &clientConfiguration))) {
            break;
        }

        QUIC_CREDENTIAL_CONFIG clientCredential{};
        clientCredential.Type = QUIC_CREDENTIAL_TYPE_NONE;
        clientCredential.Flags = static_cast<QUIC_CREDENTIAL_FLAGS>(
            QUIC_CREDENTIAL_FLAG_CLIENT |
            QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION);
        if (!CheckStatus(
                "ConfigurationLoadCredential(client/no-validation)",
                api->ConfigurationLoadCredential(clientConfiguration, &clientCredential))) {
            break;
        }

        if (!CheckStatus(
                "ListenerOpen",
                api->ListenerOpen(registration, ListenerCallback, &state, &listener))) {
            break;
        }

        QUIC_ADDR listenAddress{};
        QuicAddrSetFamily(&listenAddress, QUIC_ADDRESS_FAMILY_INET);
        QuicAddrSetToLoopback(&listenAddress);
        QuicAddrSetPort(&listenAddress, 0);
        if (!CheckStatus(
                "ListenerStart",
                api->ListenerStart(listener, &alpn, 1, &listenAddress))) {
            break;
        }

        QUIC_ADDR boundAddress{};
        uint32_t boundAddressLength = sizeof(boundAddress);
        if (!CheckStatus(
                "GetParam(listener local address)",
                api->GetParam(
                    listener,
                    QUIC_PARAM_LISTENER_LOCAL_ADDRESS,
                    &boundAddressLength,
                    &boundAddress))) {
            break;
        }
        const uint16_t port = QuicAddrGetPort(&boundAddress);
        Log("local listener started on 127.0.0.1:%u", port);

        if (!CheckStatus(
                "ConnectionOpen(client)",
                api->ConnectionOpen(
                    registration,
                    ClientConnectionCallback,
                    &state,
                    &clientConnection))) {
            break;
        }

        if (!CheckStatus(
                "StreamOpen(control)",
                api->StreamOpen(
                    clientConnection,
                    QUIC_STREAM_OPEN_FLAG_NONE,
                    ClientStreamCallback,
                    &state,
                    &controlStream))) {
            break;
        }
        Log("control stream %p opened and intentionally left unstarted", controlStream);

        if (!CheckStatus(
                "ConnectionStart(client)",
                api->ConnectionStart(
                    clientConnection,
                    clientConfiguration,
                    QUIC_ADDRESS_FAMILY_INET,
                    "127.0.0.1",
                    port))) {
            break;
        }

        Log("yielding until client QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE");
        if (!YieldUntil("client connection shutdown-complete atomic", state.ClientShutdownComplete)) {
            break;
        }

        Log("calling StreamStart(%p) after connection shutdown completed", controlStream);
        const QUIC_STATUS startStatus =
            api->StreamStart(controlStream, QUIC_STREAM_START_FLAG_NONE);
        Log("StreamStart returned 0x%08X", static_cast<unsigned>(startStatus));

        Log("calling StreamClose(%p); control-stream callbacks may be inline", controlStream);
        state.InsideStreamClose.store(true, std::memory_order_release);
        api->StreamClose(controlStream);
        state.InsideStreamClose.store(false, std::memory_order_release);
        controlStream = nullptr;

        const uint32_t inlineCallbackCount =
            state.CallbacksInsideStreamClose.load(std::memory_order_acquire);
        Log(
            "StreamClose returned; callbacks=%u, callbacks inside StreamClose=%u",
            state.StreamCallbackCount.load(std::memory_order_acquire),
            inlineCallbackCount);

        if (inlineCallbackCount == 0) {
            Log("REPRO NOT OBSERVED: StreamClose did not deliver a control-stream callback inline");
            result = 2;
            break;
        }

        Log("REPRO OBSERVED: StreamClose synchronously delivered a control-stream callback");
        result = 0;
    } while (false);

    if (controlStream != nullptr && api != nullptr) {
        state.InsideStreamClose.store(true, std::memory_order_release);
        api->StreamClose(controlStream);
        state.InsideStreamClose.store(false, std::memory_order_release);
    }
    if (clientConnection != nullptr && api != nullptr) {
        api->ConnectionClose(clientConnection);
    }
    if (api != nullptr) {
        HQUIC serverConnection =
            state.ServerConnection.exchange(nullptr, std::memory_order_acq_rel);
        if (serverConnection != nullptr) {
            api->ConnectionClose(serverConnection);
        }
    }
    if (listener != nullptr && api != nullptr) {
        api->ListenerClose(listener);
    }
    if (clientConfiguration != nullptr && api != nullptr) {
        api->ConfigurationClose(clientConfiguration);
    }
    if (serverConfiguration != nullptr && api != nullptr) {
        api->ConfigurationClose(serverConfiguration);
    }
    if (registration != nullptr && api != nullptr) {
        api->RegistrationClose(registration);
    }
    if (api != nullptr) {
        MsQuicClose(api);
    }

    return result;
}

Expected behavior

Stream callbacks should not be delivered on streams that was never started successfully.

Actual outcome

Application receives callbacks for the stream being closed even when the stream was never started successfully.

[thread 14012] created temporary self-signed server certificate (no files or store install)
[thread 14012] local listener started on 127.0.0.1:56282
[thread 14012] control stream 0000019771174560 opened and intentionally left unstarted
[thread 14012] yielding until client QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE
[thread 77208] listener 000001977110DE80: NEW_CONNECTION 00000197711880D0
[thread 70732] client connection 0000019771172F00: CONNECTED
[thread 70732] client connection 0000019771172F00: requesting shutdown while control stream is still unstarted
[thread 77208] server connection 00000197711880D0: CONNECTED
[thread 77208] server connection 00000197711880D0: SHUTDOWN_INITIATED_BY_PEER error=0
[thread 70732] client connection 0000019771172F00: SHUTDOWN_COMPLETE; toggling atomic flag
[thread 14012] observed client connection shutdown-complete atomic after 95261 yields
[thread 14012] calling StreamStart(0000019771174560) after connection shutdown completed
[thread 14012] StreamStart returned 0x80004004
[thread 14012] calling StreamClose(0000019771174560); control-stream callbacks may be inline
[thread 70732] control stream 0000019771174560 callback: SEND_SHUTDOWN_COMPLETE inside StreamClose=yes
[thread 70732] control stream 0000019771174560 callback: SHUTDOWN_COMPLETE app-close=1 connection-shutdown=1 inside StreamClose=yes
[thread 14012] StreamClose returned; callbacks=2, callbacks inside StreamClose=2
[thread 14012] REPRO OBSERVED: StreamClose synchronously delivered a control-stream callback
[thread 77208] server connection 00000197711880D0: SHUTDOWN_COMPLETE

Additional details

Please also clarify if QUIC_STREAM_EVENT_START_COMPLETE would ever be delivered when StreamStart fails synchronously. The wording today reads to me as though it should not be delivered.

No events are delivered on the stream until the app calls StreamStart (because of the race conditions that could occur) and it succeeds.

If the start operation fails, the only event that will be delivered is QUIC_STREAM_EVENT_START_COMPLETE with the failure status code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions