Crash in NIOAsyncWriter.InternalClass.deinit

i’ve started seeing a puzzling runtime crash whenever i catch a NIOAsyncWriterError.alreadyFinished:

error: (HTTP/2) NIOAsyncWriterError.alreadyFinished

💣 Program crashed: Illegal instruction at 0x000055f1d8846c57

Thread 4 crashed:

0 0x000055f1d8846c57 NIOAsyncWriter.InternalClass.deinit + 55 in UnidocServer
1 0x00007f71c5c9c29b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
2 0x00007f71c5cbe7f0 void tuple_destroy<false, false>(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 47 in libswiftCore.so
3 0x00007f71c5c9c29b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
4 0x000055f1d866d713 objectdestroy.15Tm + 18 in UnidocServer
5 0x00007f71c5c9c29b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
6 0x00007f71c5c9c29b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
7 0x00007f71c610944d swift::runJobInEstablishedExecutorContext(swift::Job*) + 348 in libswift_Concurrency.so
8 0x00007f71c6109c7c swift_job_run + 91 in libswift_Concurrency.so

has anyone run into something similar?

okay, so something seems seriously wrong with NIOAsyncWriter’s internal state machine, because when i “sabotage” the response by ordering a manual call to finish,

writer.finish()
try await writer.send(message)

i see it is hitting the precondition failure in NIO:

debug: bound to :::8443
error: (HTTP/2) NIOAsyncWriterError.alreadyFinished
NIOCore/NIOAsyncWriter.swift:176: Fatal error: Deinited NIOAsyncWriter without calling finish()
Current stack trace:
0    libswiftCore.so                    0x00007fd8147f41c0 _swift_stdlib_reportFatalErrorInFile + 109
...

💣 Program crashed: Illegal instruction at 0x00007fd8144b8bf2

Thread 6 crashed:

.build/checkouts/swift-nio/Sources/NIOCore/AsyncSequences/NIOAsyncWriter.swift:176:17

   174│         deinit {
   175│             if !self._finishOnDeinit && !self._storage.isWriterFinished {
   176│                 preconditionFailure("Deinited NIOAsyncWriter without calling finish()")                                               
      │                 ▲
   177│             } else {
   178│                 // We need to call finish here to resume any suspended continuation.

this also happens if i exit NIOAsyncChannel.executeThenClose by returning instead of throwing:

writer.finish()
try? await writer.send(message)

in fact, any call to writer.finish() inside NIOAsyncChannel.executeThenClose crashes the application, even if i don’t send any message at all.

writer.finish()

if i do nothing at all and just return from the body of the closure, i also get the same crash.

try await stream.executeThenClose
{
    (
        remote:NIOAsyncChannelInboundStream<HTTP2Frame.FramePayload>,
        writer:NIOAsyncChannelOutboundWriter<HTTP2Frame.FramePayload>
    )   in
}

in fact, the only way i can avoid crashing is by successfully writing the entire sequence of HTTP2Frame.FramePayload before returning.

I have just tried to reproduce this and written a small test and so far wasn't able to hit the precognition. Below is the test case that I wrote. Could you check for any difference in how you set this up?

    func test() async throws {
        guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return }
        let serverChannel = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton).bind(
            host: "127.0.0.1",
            port: 0
        ) { channel in
            channel.eventLoop.makeCompletedFuture {
                try channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(mode: .server) { channel in
                    channel.eventLoop.makeCompletedFuture {
                        try NIOAsyncChannel<HTTP2Frame.FramePayload, HTTP2Frame.FramePayload>(wrappingChannelSynchronously: channel)
                    }
                }
            }
        }

        let clientMultiplexer = try await ClientBootstrap(group: MultiThreadedEventLoopGroup.singleton).connect(
            host: "127.0.0.1",
            port: serverChannel.channel.localAddress!.port!
        ) { channel in
            channel.eventLoop.makeCompletedFuture {
                try channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(mode: .client) { channel in
                    channel.eventLoop.makeCompletedFuture {
                        try NIOAsyncChannel<HTTP2Frame.FramePayload, HTTP2Frame.FramePayload>(wrappingChannelSynchronously: channel)
                    }
                }
            }
        }

        try await withThrowingTaskGroup(of: Void.self) { group in
            group.addTask {
                let stream = try await clientMultiplexer.openStream { channel in
                    channel.eventLoop.makeCompletedFuture {
                        try NIOAsyncChannel<HTTP2Frame.FramePayload, HTTP2Frame.FramePayload>(wrappingChannelSynchronously: channel)
                    }
                }

                try await stream.executeThenClose { inbound, outbound in
                    let headers = HTTP2Frame.FramePayload.headers(.init(headers: .basicRequestHeaders))
                    outbound.finish()
                    try await outbound.write(headers)
                }
            }

            try await serverChannel.executeThenClose { inbound in
                for try await multiplexer in inbound {
                    for try await stream in multiplexer.inbound {
                        try await stream.executeThenClose { inbound, outbound in
                            for try await payload in inbound {
                                print(payload)
                            }
                            outbound.finish()
                            try await outbound.write(.headers(.init(headers: .basicResponseHeaders)))
                        }
                    }
                }
            }

            try await group.next()
        }
    }

Are you by any chance re-wrapping the NIOAsyncChannelOutboundWriter into your own custom type and might deinit it by accident?

here is a small reproducer program:

import NIOCore
import NIOPosix
import NIOHTTP1
import NIOHPACK
import NIOHTTP2
import NIOSSL

final
class OutboundShimHandler
{
    init()
    {
    }
}
extension OutboundShimHandler:ChannelOutboundHandler
{
    typealias OutboundIn = HTTPPart<HTTPResponseHead, ByteBuffer>
    typealias OutboundOut = HTTPPart<HTTPResponseHead, IOData>

    func write(context:ChannelHandlerContext, data:NIOAny, promise:EventLoopPromise<Void>?)
    {
        let part:OutboundOut = switch self.unwrapOutboundIn(data)
        {
        case .head(let head):   .head(head)
        case .body(let body):   .body(.byteBuffer(body))
        case .end(let tail):    .end(tail)
        }

        context.write(self.wrapOutboundOut(part), promise: promise)
    }
}

func main() async throws
{
    let directory:String = "Local/Server/Certificates"
    let threads:MultiThreadedEventLoopGroup = .init(numberOfThreads: 2)
    let certificates:[NIOSSLCertificate] =
        try NIOSSLCertificate.fromPEMFile("\(directory)/fullchain.pem")

    let privateKey:NIOSSLPrivateKey =
        try .init(file: "\(directory)/privkey.pem", format: .pem)

    var configuration:TLSConfiguration = .makeServerConfiguration(
        certificateChain: certificates.map(NIOSSLCertificateSource.certificate(_:)),
        privateKey: .privateKey(privateKey))

        configuration.applicationProtocols = ["h2"]

    let niossl:NIOSSLContext = try .init(configuration: configuration)

    let bootstrap:ServerBootstrap = .init(group: threads)
        .serverChannelOption(ChannelOptions.backlog, value: 256)
        .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
        .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
        .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)

    let listener:
        NIOAsyncChannel<
            EventLoopFuture<NIONegotiatedHTTPVersion<
                NIOAsyncChannel<
                    HTTPPart<HTTPRequestHead, ByteBuffer>,
                    HTTPPart<HTTPResponseHead, ByteBuffer>>,
                (
                    NIOAsyncChannel<HTTP2Frame, HTTP2Frame>,
                    NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<
                        HTTP2Frame.FramePayload,
                        HTTP2Frame.FramePayload>>
                )>>,
            Never> = try await bootstrap.bind(
        host: "::",
        port: 8443)
    {
        (channel:any Channel) in

        channel.pipeline.addHandler(NIOSSLServerHandler.init(context: niossl))
            .flatMap
        {
            channel.configureAsyncHTTPServerPipeline
            {
                (connection:any Channel) in

                connection.eventLoop.makeCompletedFuture
                {
                    try connection.pipeline.syncOperations.addHandler(
                        OutboundShimHandler.init())

                    return try NIOAsyncChannel<
                        HTTPPart<HTTPRequestHead, ByteBuffer>,
                        HTTPPart<HTTPResponseHead, ByteBuffer>>.init(
                        wrappingChannelSynchronously: connection,
                        configuration: .init())
                }
            }
                http2ConnectionInitializer:
            {
                (connection:any Channel) in

                connection.eventLoop.makeCompletedFuture
                {
                    try NIOAsyncChannel<HTTP2Frame, HTTP2Frame>.init(
                        wrappingChannelSynchronously: connection,
                        configuration: .init())
                }
            }
                http2StreamInitializer:
            {
                (stream:any Channel) in

                stream.eventLoop.makeCompletedFuture
                {
                    try NIOAsyncChannel<
                        HTTP2Frame.FramePayload,
                        HTTP2Frame.FramePayload>.init(
                        wrappingChannelSynchronously: stream,
                        configuration: .init())
                }
            }
        }
    }

    try await listener.executeThenClose
    {
        for try await connection:EventLoopFuture<NIONegotiatedHTTPVersion<
            NIOAsyncChannel<
                HTTPPart<HTTPRequestHead, ByteBuffer>,
                HTTPPart<HTTPResponseHead, ByteBuffer>>,
            (
                NIOAsyncChannel<HTTP2Frame, HTTP2Frame>,
                NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<
                    HTTP2Frame.FramePayload,
                    HTTP2Frame.FramePayload>>
            )>> in $0
        {
            switch try await connection.get()
            {
            case .http1_1(let connection):
                try await connection.channel.close()

            case .http2((let connection, let streams)):
                do
                {
                    for try await stream:NIOAsyncChannel<
                        HTTP2Frame.FramePayload,
                        HTTP2Frame.FramePayload> in streams.inbound
                    {
                        try await stream.executeThenClose
                        {
                            (_, _) in
                        }
                    }
                }
                catch let error
                {
                    print(error)
                }

                try await connection.channel.close()
            }
        }
    }
}

try await main()

the easiest way to run it is probably pasting it into an SPM snippet.

you’ll need TLS certificates to get this running, you can generate your own in the directory referenced from the snippet (Local/Server/Certificates), or you can use these dummy localhost certificates:

fullchain.pem

-----BEGIN CERTIFICATE-----
MIIEGzCCAoOgAwIBAgIQL62HjbQWmPjVsRAzHNeu3TANBgkqhkiG9w0BAQsFADBp
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHzAdBgNVBAsMFnVidW50
dUBrbG9zc3kgKGRpYW5uYSkxJjAkBgNVBAMMHW1rY2VydCB1YnVudHVAa2xvc3N5
IChkaWFubmEpMB4XDTIzMDkyMTIxNTQwNVoXDTI1MTIyMTIyNTQwNVowSjEnMCUG
A1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMR8wHQYDVQQLDBZ1
YnVudHVAa2xvc3N5IChkaWFubmEpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA1M4+fL98qtJHpVVJr5JudOnDY+qBHL8nzSgqR7ZwPZY6uHHtZQ8xwSdn
YL7t6XOHIrjlmA7fXrUAMCJ4ElRutRbT/+culGo4LL7jYdtAKkuiV1S86Y/1CM5C
heq2Mdgem7cI05xZQ4Mx0wZiao0SgXvXRfBmkCTVPcCrmPLqLkLIJJf+0G0qcBA5
/oqK99izxjZibF0/W9ehPKdvmTMef1845BnQDgoYR0iyLRhPCC2IZ2ze19zxxJ2R
OVurvITNcxEFMar+LdXloG0MLi2t8a1A9WCMbgGNrfOMAXiPLMI/XO7XoGsCQgb/
A7lDtE+Wag0QLghOXDOhQDdisqkqywIDAQABo14wXDAOBgNVHQ8BAf8EBAMCBaAw
EwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUQXvcrc8QC11X6Xh2VMSI
mkyRG8wwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBgQA6
suTTsFBRaYZITcJDS12/R5ZL/MPwTOZQzQbANtXRVO9t2gQlEqmJdCy80n89JAB4
WO6lhd73EFSe1GX4hKwNrky+cR759Har5pCu9LPtbWGQEfVQp9bjSlHHjPww1eap
KYCJ94aD8HyReRqhvZiYkqUWuGJvU7caIFEXKcsp+yPCbE6Fqdbf3dwWEkmsHStl
1rJBkRk2U/CwNDhDLb6Yna75Ffqgin7SazaJqZ3GrHmlX9GSN4OoWI5VrTzUpVqC
dHML8G3XbVe8FSaD9bcRcRv4R5ev6chcDO9S3mOfORy4kHPW73PlFYOw7fyXPpMW
hCuy6FR2nOYFGzPimwu26GIx6EV7kbf5JGBTr+sWQHeq5gpNhSelUIAr9tmrtd5y
1+SWMv9CCRO5yRD4+oPsFLO7hRQKmsnz+bz4o2F/YfDVdeXbgroWRtQUEgxeNKbQ
w3RfzWsE0BVz8esIs0KOUgokoMIxV610bz0t2zxDj9c40ISgMcjSBBHSjuDCIS8=
-----END CERTIFICATE-----

privkey.pem

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUzj58v3yq0kel
VUmvkm506cNj6oEcvyfNKCpHtnA9ljq4ce1lDzHBJ2dgvu3pc4ciuOWYDt9etQAw
IngSVG61FtP/5y6UajgsvuNh20AqS6JXVLzpj/UIzkKF6rYx2B6btwjTnFlDgzHT
BmJqjRKBe9dF8GaQJNU9wKuY8uouQsgkl/7QbSpwEDn+ior32LPGNmJsXT9b16E8
p2+ZMx5/XzjkGdAOChhHSLItGE8ILYhnbN7X3PHEnZE5W6u8hM1zEQUxqv4t1eWg
bQwuLa3xrUD1YIxuAY2t84wBeI8swj9c7tegawJCBv8DuUO0T5ZqDRAuCE5cM6FA
N2KyqSrLAgMBAAECggEACTcdMZ3BMkyE2b4FwNqgeeOdmHgRO0Nz38h7fDuERMZ6
qH4Wf6fWybyBF4ltGAzuryw+lQUf2yQPbAYyGOkbGjBw4cYLGFY5NIbXpecusiYw
U4PR4nNfcxArhU0SsrnfKXMVqMQ+gVPvFmpSXLbbNEw+mEK+zkMqENCFHcx3I6Wg
b0WYjWeQIE9jomWU9muu799H+Ybpeds81oAz5AvQ/JPBsGTrRSW30E0wPZPW+qPv
oCmPRKCtWkrYXqoOy9lk4vhjiVQg2XYeiPxl9WrbMSq2iW82t4Sljh5R03FZWQpj
wQgfMlxDOWtr6uvuSFe1RdZUnmaTOmnNsFY4hK52EQKBgQD5ALIIU8gYiuA5l+Z/
sUknGbQ8E9HxTg1/txSOf2CRdm6ziPR9bpBEWF+8WEGd2Mb6SoMKigJfu5w8EKg2
rZvfkK0LTEqgLe0csLfh6DQRLlc5fuK/IOqPznaqvpOlwifKCosMl97yuWmQkxHz
GRBbDf0AgsdJxhjNdhGDAtnxPwKBgQDaySZidiPCWLFcEWDPMwSxVkDbCuNvLgSz
fvwz4tL/vUf+weyGNr/rkKVwQoUY4Hq8pkcm6he7FLbDnQasoe0DI8OMDXUZHtos
D7x6B9dHm6eZwtOpsdC3Q6A+vAXa8tXr5w7My0HFDbLHh/Ch8jRxKZWFxU44oqPt
wX8ZVg7XdQKBgBE0KxjIMRsA/Vz9Ub+g0B0TeZBtDiRN8EDStWjjBBkIxb1BySKh
cPZH5NVug5oUUCsa2tLvlhpnK/Q6cmTUueBIbqxJKR7IDYnd69Z/5JkLSpt+WMw7
yfkFms1RPYJGV9ltwQ2tsIm0pcaHYsYZBThFTyWp43sFZNFNRwh2OfihAoGBANQd
nyRo68R53wKnKpfYG92fBWQYy2Y4VIB+RiA78lvV9J4u/5UkMbA+XddX9timkvih
sWwuG3Ha5FMEw7rNhw+7NdRsG7KOMfH0E8SwI20eoUC3HiVw6y0y2ILaIkcjlnmP
W8775TkaTdGbn5YzT9rC+V9naq4IKSzSo9o5kEwdAoGAQRMjcCKxzI3DTvYeyqCO
U52A2Ejw9XgjRlo/7KT+PHNace+XskuULPvMkWTJilUkbCsWxqBWhVMu5/WH1V+P
+5igKwyRy8tmCtGKK7MHJGPFuh410QqAfyBJFKqpxOuAEH0/eUvTvoUcnD0v8NhV
AkaHhmNosnEH28wUaxViRLk=
-----END PRIVATE KEY-----

to cause the crash, run the snippet and navigate to https://localhost:8443.

$ swift run NIOCrash
Building for debugging...
[6/6] Linking NIOCrash
Build complete! (2.90s)
StreamClosed(streamID: HTTP2StreamID(15), errorCode: HTTP2ErrorCode<0x8 Cancel>, location: "NIOHTTP2/HTTP2StreamChannel.swift:865")
NIOCore/NIOAsyncWriter.swift:176: Fatal error: Deinited NIOAsyncWriter without calling finish()
Current stack trace:
0    libswiftCore.so                    0x00007f9b4291e1c0 _swift_stdlib_reportFatalErrorInFile + 109
1    libswiftCore.so                    0x00007f9b425e3d05 <unavailable> + 1461509
2    libswiftCore.so                    0x00007f9b425e3b27 <unavailable> + 1461031
3    libswiftCore.so                    0x00007f9b425e2a90 _assertionFailure(_:_:file:line:flags:) + 342
4    NIOCrash                           0x000055a105b25b28 <unavailable> + 10144552
5    NIOCrash                           0x000055a105b2613d <unavailable> + 10146109
6    libswiftCore.so                    0x00007f9b428878e0 <unavailable> + 4229344
7    libswiftCore.so                    0x00007f9b4288829b <unavailable> + 4231835
8    NIOCrash                           0x000055a105b122a5 <unavailable> + 10064549
9    libswiftCore.so                    0x00007f9b428aa7f0 <unavailable> + 4372464
10   NIOCrash                           0x000055a105e87d4a <unavailable> + 13692234
11   NIOCrash                           0x000055a105c3a76a <unavailable> + 11278186
12   NIOCrash                           0x000055a105c34164 <unavailable> + 11252068
13   NIOCrash                           0x000055a105c347f5 <unavailable> + 11253749
14   libswiftCore.so                    0x00007f9b428878e0 <unavailable> + 4229344
15   libswiftCore.so                    0x00007f9b4288829b <unavailable> + 4231835
16   NIOCrash                           0x000055a105c8d8e0 <unavailable> + 11618528
17   libswift_Concurrency.so            0x00007f9b42cb244d <unavailable> + 300109
18   libswift_Concurrency.so            0x00007f9b42cb2c20 swift_job_run + 92
19   libdispatch.so                     0x00007f9b41f2df89 <unavailable> + 176009
20   libdispatch.so                     0x00007f9b41f2dd9f <unavailable> + 175519
21   libdispatch.so                     0x00007f9b41f38dc6 <unavailable> + 220614
22   libpthread.so.0                    0x00007f9b4111644b <unavailable> + 29771
23   libc.so.6                          0x00007f9b404b24f0 clone + 63

💣 Program crashed: Illegal instruction at 0x00007f9b425e2bf2

Thread 1 crashed:

 0 0x00007f9b425e2bf2 _assertionFailure(_:_:file:line:flags:) + 354 in libswiftCore.so
 1 NIOAsyncWriter.InternalClass.deinit + 247 in NIOCrash at /swift/swift-unidoc/.build/checkouts/swift-nio/Sources/NIOCore/AsyncSequences/NIOAsyncWriter.swift:176:17

   174│         deinit {
   175│             if !self._finishOnDeinit && !self._storage.isWriterFinished {
   176│                 preconditionFailure("Deinited NIOAsyncWriter without calling finish()")                                               
      │                 ▲
   177│             } else {
   178│                 // We need to call finish here to resume any suspended continuation.

 2 0x00007f9b428878e0 _swift_release_dealloc + 15 in libswiftCore.so
 3 0x00007f9b4288829b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
 4 0x00007f9b428aa7f0 void tuple_destroy<false, false>(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 47 in libswiftCore.so
 5 EventLoopFuture.deinit + 163 in NIOCrash at /swift/swift-unidoc/.build/checkouts/swift-nio/Sources/NIOCore/EventLoopFuture.swift:431:5

   429│             }
   430│         }
   431│     }                                                                                                                                 
      │    ▲
   432│ }
   433│

 6 0x00007f9b428878e0 _swift_release_dealloc + 15 in libswiftCore.so
 7 0x00007f9b4288829b bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 122 in libswiftCore.so
 8 closure #2 in main() + 159 in NIOCrash at /swift/swift-unidoc/Snippets/NIOCrash.swift:159:9

   157│                 try await connection.channel.close()
   158│             }
   159│         }                                                                                                                             
      │        ▲
   160│     }
   161│ }

 9 closure #1 in NIOAsyncChannel.executeThenClose<A>(_:) in NIOCrash at /swift/swift-unidoc/.build/checkouts/swift-nio/Sources/NIOCore/AsyncChannel/AsyncChannel.swift:303

   301│     ) async throws -> Result where Outbound == Never {
   302│         try await self.executeThenClose { inbound, _ in
   303│             try await body(inbound)                                                                                                   
      │             ▲
   304│         }
   305│     }

10 NIOAsyncChannel.executeThenClose<A>(_:) in NIOCrash at /swift/swift-unidoc/.build/checkouts/swift-nio/Sources/NIOCore/AsyncChannel/AsyncChannel.swift:271

   269│         let result: Result
   270│         do {
   271│             result = try await body(self._inbound, self._outbound)                                                                    
      │             ▲
   272│         } catch let bodyError {
   273│             do {

11 NIOAsyncChannel.executeThenClose<A>(_:) in NIOCrash at /swift/swift-unidoc/.build/checkouts/swift-nio/Sources/NIOCore/AsyncChannel/AsyncChannel.swift:302

   300│         _ body: (_ inbound: NIOAsyncChannelInboundStream<Inbound>) async throws -> Result
   301│     ) async throws -> Result where Outbound == Never {
   302│         try await self.executeThenClose { inbound, _ in                                                                               
      │         ▲
   303│             try await body(inbound)
   304│         }

12 main() in NIOCrash at /swift/swift-unidoc/Snippets/NIOCrash.swift:121

   119│     }
   120│ 
   121│     try await listener.executeThenClose                                                                                               
      │     ▲
   122│     {
   123│         for try await connection:EventLoopFuture<NIONegotiatedHTTPVersion<

Illegal instruction (core dumped)

i currently have no reason to believe these two issues are connected (particularly because i don’t think NIO has adopted the new 5.9 ownership specifiers yet), but one possible symptom of a compiler bug i just isolated could be the swift runtime calling deinit prematurely.

Just from taking a Quick Look you are just dropping the NIOAsyncChannel for the http1 connection and http2 connection on the ground. You only consume the channels of the http2 streams. This might be the reason for the deinit preconditions.
I will run your reproducer tomorrow.

1 Like

i’m not sure what you mean by “dropping the connection on the ground”, as i am just closing the HTTP/1 connection instantly, which i feel like, should be able to close the connection without crashing?

I didn’t say you drop the connection on the ground but rather the NIOAsyncChannel. In fact it looks like you wrap those channels into NIOAsyncChannels but never use them. That’s most likely the problem here.
You don’t have to necessarily do anything with those channels, you can just not wrap them and return the bare Channel from the initializer closures.

1 Like

thanks, as it turns out, in the original code base i also am never using the HTTP/2 connection-level NIOAsyncChannel, i am only ever using the NIOHTTP2Handler.AsyncStreamMultiplexer from the second tuple element.

(although, i don’t think creating an unused NIOAsyncChannel should end in a crash. the individual stream channels could also end up never being used, due to task cancellation.)

i was cargo-culting the examples from when the new APIs were released, and when i did as you suggested the crash stopped occurring regardless of any writer.finish() calls on the streams. thanks!

am i correct in understanding that failing to perform any operations on a NIOAsyncChannel before deiniting it results in a crash? should i always be performing a ceremonial read/write on the NIOAsyncChannelInboundStream/NIOAsyncChannelOutboundWriter before exiting a scope that takes one as a parameter? how does this interact with task cancellation?

1 Like