Repository: apple/swift-nio
File: Sources/NIOPosix/BaseStreamSocketChannel.swift (Swift)

Code before:
                    self.close0(error: error, mode: .all, promise: promise)
                    return
                }
                self.unregisterForWritable()

                let outboundCloseState = self.pendingWrites.close(promise)
                switch outboundCloseState {
                case .open:
                    preconditionFailure("Close resulted in an open state, this should never happen")
                case .pending:
                    ()  // nothing to do
                case .readyForClose(let closePromise):
                    assert(promise == closePromise)
                    // Shutdown the socket only when the pending writes are dealt with
                    do {
                        try self.shutdownSocket(mode: mode)
                        closePromise?.succeed(())
                    } catch let err {
                        closePromise?.fail(err)
                    }
                    self.pendingWrites.outboundCloseState = .closed
                case .closed:
                    ()  // nothing to do
                }

                self.pipeline.fireUserInboundEventTriggered(ChannelEvent.outputClosed)
            case .input:
                if self.inputShutdown {
                    promise?.fail(ChannelError._inputClosed)
                    return
                }
                if self.outputShutdown {
                    // Escalate to full closure
                    self.close0(error: error, mode: .all, promise: promise)
                    return
                }
                switch error {
                case ChannelError.eof:
                    // No need to explicit call socket.shutdown(...) as we received an EOF and the call would only cause
                    // ENOTCON
                    self.inputShutdown = true
                    break
                default:
                    try self.shutdownSocket(mode: mode)
                }
                self.unregisterForReadable()
                promise?.succeed(())

                self.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
            case .all:
                if let timeout = self.connectTimeoutScheduled {

Review comment (bug):
This user event now triggers unconditionally, and at the wrong time. It should trigger only after the shutdown. Also, don't forget the unregister for writable

Code after:
                    self.close0(error: error, mode: .all, promise: promise)
                    return
                }

                let result = self.pendingWrites.closeOutbound(promise)
                switch result {
                case .pending:
                    ()  // promise is stored in `pendingWrites` state for completing later

                case .readyForClose(let closePromise):
                    // Shutdown the socket only when the pending writes are dealt with
                    do {
                        try self.shutdownSocket(mode: mode)
                        closePromise?.succeed(())
                    } catch let err {
                        closePromise?.fail(err)
                    }
                    self.unregisterForWritable()
                    self.pipeline.fireUserInboundEventTriggered(ChannelEvent.outputClosed)

                case .closed(let closePromise):
                    closePromise?.succeed(())

                case .errored(let err, let closePromise):
                    assertionFailure("Close errored: \(err)")
                    closePromise?.fail(err)

                    // Escalate to full closure
                    // promise is nil here because we have used the supplied promise to convey failure of the half-close
                    self.close0(error: err, mode: .all, promise: nil)
                }

            case .input:
                if self.inputShutdown {
                    promise?.fail(ChannelError._inputClosed)
                    return
                }
                if self.outputShutdown {
                    // Escalate to full closure
                    self.close0(error: error, mode: .all, promise: promise)
                    return
                }
                switch error {
                case ChannelError.eof:
                    // No need to explicit call socket.shutdown(...) as we received an EOF and the call would only cause
                    // ENOTCON
                    self.inputShutdown = true
                    break
                default:
                    try self.shutdownSocket(mode: mode)
                }