swiftconcurrency

swift 6 concurrency error in NIO mock server "Call to actor-isolated instance method 'getResponse(for:)' in a synchronous nonisolated context"


I'm trying to create a MockHTTPServer using NIO. Trying to make it swift 6 compatible. I'm stuck trying to figure out how to resolve Call to actor-isolated instance method 'getResponse(for:)' in a synchronous nonisolated context which occurs in the MockHTTPHandler. I'm hoping someone will tell me it's simple. Wrapping the offending code in a Task creates a different error: "Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race" and I couldn't figure out which function call is racing.

fileprivate actor MockHTTPServer {
    private let group: EventLoopGroup
    private let channel: Channel
    let port: Int
    
    private var responses: [String: (HTTPResponseStatus, String)] = [:]
    
    private init(group: EventLoopGroup, channel: Channel) {
        self.group = group
        self.channel = channel
        self.port = channel.localAddress?.port ?? -1
    }
    
    static func start(group: EventLoopGroup) async throws -> MockHTTPServer {
        let server = try await withCheckedThrowingContinuation { continuation in
            let bootstrap = ServerBootstrap(group: group)
                .serverChannelOption(ChannelOptions.backlog, value: 256)
                .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
                .childChannelInitializer { channel in
                    channel.pipeline.configureHTTPServerPipeline().flatMap { _ in
                        let handler = MockHTTPHandler()
                        return channel.pipeline.addHandler(handler)
                    }
                }
            
            bootstrap.bind(host: "localhost", port: 0).whenComplete { result in
                switch result {
                case .success(let channel):
                    let server = MockHTTPServer(group: group, channel: channel)
                    channel.pipeline.handler(type: MockHTTPHandler.self).whenSuccess { handler in
                        handler.server = server
                    }
                    continuation.resume(returning: server)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
        
        return server
    }
    
    func stop() async throws {
        try await channel.close()
    }
    
    func addResponse(for path: String, statusCode: HTTPResponseStatus, body: String) {
        responses[path] = (statusCode, body)
    }
    
    func getResponse(for path: String) -> (HTTPResponseStatus, String)? {
        return responses[path]
    }
    
    private final class MockHTTPHandler: ChannelInboundHandler {
        typealias InboundIn = HTTPServerRequestPart
        typealias OutboundOut = HTTPServerResponsePart
        
        weak var server: MockHTTPServer?
        
        func channelRead(context: ChannelHandlerContext, data: NIOAny) {
            let reqPart = unwrapInboundIn(data)
            
            guard case .head(let head) = reqPart else { return }
            
            guard let server = self.server else {
                respond(context: context, status: .internalServerError, body: "Server not found")
                return
            }
            
                if let (status, body) = server.getResponse(for: head.uri) {
                    respond(context: context, status: status, body: body)
                } else {
                    respond(context: context, status: .notFound, body: "Not Found")
                }
        }
        
        private func respond(context: ChannelHandlerContext, status: HTTPResponseStatus, body: String) {
            var headers = HTTPHeaders()
            headers.add(name: "Content-Type", value: "application/json")
            headers.add(name: "Content-Length", value: "\(body.utf8.count)")
            
            let head = HTTPResponseHead(version: .http1_1, status: status, headers: headers)
            context.write(wrapOutboundOut(.head(head)), promise: nil)
            
            var buffer = context.channel.allocator.buffer(capacity: body.utf8.count)
            buffer.writeString(body)
            context.write(wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil)
            context.writeAndFlush(wrapOutboundOut(.end(nil)), promise: nil)
        }
    }
}

Solution

  • You can read more about this error in this excellent blog. In your example, the compiler is raising error about property weak var server: MockHTTPServer? as It is mutable because var keyword which can introduce data races.