Reading empty UDP datagrams

Hi Cory and everyone,

I'm trying to implement a UDP based protocol and everything has been working fine except when I get a datagram with no data. When that happens, my server crashes with an assertion failure.

For the protocol I'm implementing, an empty datagram has a specific meaning. Is there a way to make it work?

This is the error I get:

NIOPosix/SocketChannel.swift:628: Assertion failed

This is what tcpdump shows immediate before the error occurs:

14:13:00.931507 IP 206.189.113.124.52962 > 159.65.88.158.9999: UDP, length 0

Here's the failing assertion in swift-nio:

Here's my application code:

import NIOCore
import NIOPosix

@main
public struct SwiftProtohackers {
    public static func main() throws {
        let unusualDatabaseMessageHandler = UnusualDatabaseMessageHandler()

        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let bootstrap = DatagramBootstrap(group: group)
            .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .channelInitializer { channel in
                channel.pipeline.addHandler(UnusualDatabaseMessageDecoder()).flatMap { v in
                    channel.pipeline.addHandler(unusualDatabaseMessageHandler)
                }
            }

        defer {
            try! group.syncShutdownGracefully()
        }
        
        let channel = try bootstrap.bind(host: "0.0.0.0", port: 9999).wait()
        
        print("Server started and listening on \(channel.localAddress!)")
        
        try channel.closeFuture.wait()
        
        print("Server closed")
    }
}

actor UnusualDatabase {
    enum Operation {
        case insert(key: String, value: String)
        case retrieve(key: String)
        
        init(rawValue s: String) {
            if let eqIdx = s.firstIndex(of: "=") {
                self = .insert(key: String(s[..<eqIdx]), value: String(s[eqIdx...].dropFirst()))
            } else {
                self = .retrieve(key: s)
            }
        }
    }

    private var data: [String: String] = ["version": "SwiftProtohackers 1.0"]
    
    func perform(_ op: Operation) -> String? {
        switch op {
        case let .insert(key, value):
            if key != "version" {
                data[key] = value
            }
            return nil
        case let .retrieve(key):
            return "\(key)=\(data[key] ?? "")"
        }
    }
}

final class UnusualDatabaseMessageDecoder: ChannelInboundHandler {
    typealias InboundIn = AddressedEnvelope<ByteBuffer>
    typealias InboundOut = AddressedEnvelope<UnusualDatabase.Operation>
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let envelope = unwrapInboundIn(data)
        var buffer = envelope.data
        
        guard let message = buffer.readString(length: buffer.readableBytes)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
            print("Error: invalid string received")
            return
        }
        
        let operation = UnusualDatabase.Operation(rawValue: message)
        
        context.fireChannelRead(
            wrapInboundOut(AddressedEnvelope(remoteAddress: envelope.remoteAddress, data: operation))
        )
    }
    
    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("error:", error)
        context.close(promise: nil)
    }
}

final class UnusualDatabaseMessageHandler: ChannelInboundHandler {
    typealias InboundIn = AddressedEnvelope<UnusualDatabase.Operation>
    typealias OutboundOut = AddressedEnvelope<ByteBuffer>
    
    let database = UnusualDatabase()
    
    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        let request = unwrapInboundIn(data)
        let channel = context.channel

        Task {
            if let response = await database.perform(request.data), channel.isWritable {
                let buffer = channel.allocator.buffer(string: response)
                let envelope = AddressedEnvelope(
                    remoteAddress: request.remoteAddress,
                    data: buffer
                )
                channel.writeAndFlush(envelope, promise: nil)
            }
        }
    }

    func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("error:", error)
        context.close(promise: nil)
    }
}

As an aside for the curious, I'm using Protohackers exercises to learn Swift NIO. This one is an implementation of problem 4. My solutions are on GitHub. I'm doing one commit per problem and force pushing when I change things, so the code might look different on the repo.

I think that assertion is simply incorrect! If you're up for it, I'd recommend adding a quick unit test to SwiftNIO that sends a zero-length datagram, and then remove the assertion, and upload that as a PR. We'd happily merge it.

1 Like

Awesome! On it.

Done Allow writing and reading empty datagrams by hashemi · Pull Request #2341 · apple/swift-nio · GitHub

3 Likes