grpc-swift icon indicating copy to clipboard operation
grpc-swift copied to clipboard

unavailable: "channel isn't ready"

Open piotrkowalczuk opened this issue 1 year ago • 10 comments

Describe the bug

I'm new to Swift so I'm aware that it might be something on my end. Nevertheless, I will try my luck and report it as bug.

I have a very simple setup where I make an attempt to make a call stright from main struct of iOS app. Without luck, I'm getting unavailable: "channel isn't ready".

  • what works
    • naked http using URLRequest
    • grpc-swift:1.23.1
    • curl
    • grpcurl
  • what did not work
    • grpc-swift:2.0.0-alpha.1 (both posix and niots)

To reproduce

Steps to reproduce the bug you've found:

  1. Update system to macos:15
  2. Install latest Xcode:16.0
  3. Create multi platform project
  4. Add snippet from bellow to @main struct constructor.
  5. Run and hope your server will recognize request.

Expected behaviour

Ideally the request should succeed. In bare minimum I would like to see some logs from the transport layer on the server side.

Additional information

Server

The Go server that runs on my localhost has various debug modes enabled.

GRPC_TRACE=all
GRPC_VERBOSITY=DEBUG
GODEBUG=http2debug=1
GRPC_GO_LOG_VERBOSITY_LEVEL=99
GRPC_GO_LOG_SEVERITY_LEVEL=info

Code

Task {
    try await withThrowingDiscardingTaskGroup { group in
        let client = GRPCClient(
            transport: try .http2NIOPosix(
                target: .ipv4(host: "127.0.0.1", port: 8081),
                config: .defaults(transportSecurity: .plaintext)
            )
        )

        group.addTask {
            try await client.run()
        }

        defer {
            client.beginGracefulShutdown()
        }

        let auth =
            Authserv_V1_AuthService_Client(
                wrapping: client)
        
        do {
            let res = try await auth.obtainRefreshToken(
                .with {
                    $0.userID = UUID().uuidString
                })
            print(res)
        }catch {
            print("error: \(error)")
        }
    }
}

Changing to NIOTS do not help, print("2") is never reached:

let transport = try HTTP2ClientTransport.TransportServices(
    target: .ipv4(host: "127.0.0.1", port: 8081),
    config: .defaults(transportSecurity: .plaintext)
)
print("1")
try await transport.connect()
print("2")

Dependencies

image

Settings

image

protoc-gen-grpc-swift

protoc-gen-grpc-swift --version
protoc-gen-grpc-swift 1.0.0-development

installed from source from https://github.com/grpc/grpc-swift-protobuf that depends on grpc-swift:2.0.0-alpha.1.

piotrkowalczuk avatar Oct 02 '24 19:10 piotrkowalczuk

Hi Piotr, thanks for filing this and trying out the alpha release!

We are certainly aware of some bugs but it's a bit surprising that this simple setup doesn't work...

Describe the bug

I'm new to Swift so I'm aware that it might be something on my end. Nevertheless, I will try my luck and report it as bug.

I have a very simple setup where I make an attempt to make a call stright from main struct of iOS app. Without luck, I'm getting unavailable: "channel isn't ready".

To clarify, is the server also running within your iOS app? I'd like to understand the setup better so I can reproduce it locally. As you noted there are no logs here: there are two logging systems in Swift (swift-log and OSLog), so we're currently evaluating how best to handle observability, which obviously makes things harder to debug at the moment.

GRPC_TRACE=all
GRPC_VERBOSITY=DEBUG
GODEBUG=http2debug=1
GRPC_GO_LOG_VERBOSITY_LEVEL=99
GRPC_GO_LOG_SEVERITY_LEVEL=info

For what it's worth: none of these env variables will be used.

Changing to NIOTS do not help, print("2") is never reached:

let transport = try HTTP2ClientTransport.TransportServices(
    target: .ipv4(host: "127.0.0.1", port: 8081),
    config: .defaults(transportSecurity: .plaintext)
)
print("1")
try await transport.connect()
print("2")

That's expected: connect() is long running, it won't return until the transport has been shutdown (or the task has been cancelled). You snippet above using the task group and client is the correct way to run a client and its transport.

glbrntt avatar Oct 03 '24 07:10 glbrntt

To clarify, is the server also running within your iOS app? I'd like to understand the setup better so I can reproduce it locally. As you noted there are no logs here: there are two logging systems in Swift (swift-log and OSLog), so we're currently evaluating how best to handle observability, which obviously makes things harder to debug at the moment.

The server is running on my localhost. It's a Go app.

A minimal setup that allows me to reproduce the issue:

debug/Sources/main.swift

import NIOTransportServices
import GRPCNIOTransportHTTP2
import MY_PACKGE
import Foundation

try await withThrowingDiscardingTaskGroup { group in
    let client = GRPCClient(
        transport: try .http2NIOPosix(
            target: .ipv4(host: "127.0.0.1", port: 8081),
            config: .defaults(transportSecurity: .plaintext)
        )
    )

    group.addTask {
        try await client.run()
    }

    defer {
        client.beginGracefulShutdown()
    }

    let auth =
        MY_CLIENT(
            wrapping: client)

    do {
        let res = try await auth.MY_METHOD(
            .with {
                $0.userID = UUID().uuidString
            })
        print(res)
    } catch {
        print("error: \(error)")
    }
}

debug/Package.swift

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "debug",
    platforms: [
        .macOS(.v15),
    ],
    products: [
        .executable(name: "debug", targets: ["debug"])
    ],
    dependencies: [
        .package(path: "../my_package"),
    ],
targets: [
        .executableTarget(
            name: "debug",
            dependencies: [
                .product(name: "MY_PACKAGE", package: "my_package"),
            ]
        ),
    ]
)

my_package/Package.swift

        .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-alpha.1"),
        .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"),
        .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"),
        .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.27.2"), 

and it gives

swift run debug
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'debug' complete! (2.61s)
error: unavailable: "channel isn't ready"

piotrkowalczuk avatar Oct 03 '24 07:10 piotrkowalczuk

Okay, I just want to confirm your current setup because I think it's changed, you now have a Swift client running on the same machine as a Go server?

glbrntt avatar Oct 03 '24 09:10 glbrntt

Yes the same machine.

piotrkowalczuk avatar Oct 03 '24 10:10 piotrkowalczuk

I setup a Go server and used a Swift client and wasn't able to reproduce this.

However, what I did notice was that the Go server in the gRPC Go examples bound to an IPv6 address ("::1") – is your Go server also bound to an IPv6 address?

glbrntt avatar Oct 03 '24 13:10 glbrntt

I have the same problem in my iOS project with version 2.0.0-alpha.1

func gRPCEvents() async throws {
    let client = GRPCClient(
        transport: try .http2NIOPosix(
            target: .ipv4(host: "localhost", port: 6162),
            config: .defaults(transportSecurity: .plaintext)
        )
    )
    
    try await withThrowingDiscardingTaskGroup { group in
        group.addTask {
            try await client.run()
        }
        
        defer {
            client.beginGracefulShutdown()
        }
        
        let service = EventsService_Client(wrapping: client)
        
        let events = try await service
            .allEvents(Google_Protobuf_Empty())
        
        print(events.events.count)
    }
}

I have my own server on my machine "localhost" with vapor and 1.23.1 grpc version.

jcalderita avatar Oct 06 '24 05:10 jcalderita

However, what I did notice was that the Go server in the gRPC Go examples bound to an IPv6 address ("::1") – is your Go server also bound to an IPv6 address?

Changing the listener to bind to ip4 explicitly did not help.

piotrkowalczuk avatar Oct 06 '24 11:10 piotrkowalczuk

I have the same problem in my iOS project with version 2.0.0-alpha.1

func gRPCEvents() async throws {
    let client = GRPCClient(
        transport: try .http2NIOPosix(
            target: .ipv4(host: "localhost", port: 6162),
            config: .defaults(transportSecurity: .plaintext)
        )
    )
    
    try await withThrowingDiscardingTaskGroup { group in
        group.addTask {
            try await client.run()
        }
        
        defer {
            client.beginGracefulShutdown()
        }
        
        let service = EventsService_Client(wrapping: client)
        
        let events = try await service
            .allEvents(Google_Protobuf_Empty())
        
        print(events.events.count)
    }
}

I have my own server on my machine "localhost" with vapor and 1.23.1 grpc version.

@jcalderita "localhost" isn't an IPv4 address, so this should fail.

If you change .ipv4(host: "localhost", port: 6162) to .dns(host: "localhost", port: 6162) the DNS name resolver will be used and should resolve "localhost" to "127.0.0.1" and "::1" and try connecting to both.

glbrntt avatar Oct 07 '24 06:10 glbrntt

However, what I did notice was that the Go server in the gRPC Go examples bound to an IPv6 address ("::1") – is your Go server also bound to an IPv6 address?

Changing the listener to bind to ip4 explicitly did not help.

Could you provide a complete minimal example with a Go server and a Swift client? Ideally zipped up so I can download it and just run the client and server.

glbrntt avatar Oct 07 '24 06:10 glbrntt

I have the same problem in my iOS project with version 2.0.0-alpha.1

func gRPCEvents() async throws {
    let client = GRPCClient(
        transport: try .http2NIOPosix(
            target: .ipv4(host: "localhost", port: 6162),
            config: .defaults(transportSecurity: .plaintext)
        )
    )
    
    try await withThrowingDiscardingTaskGroup { group in
        group.addTask {
            try await client.run()
        }
        
        defer {
            client.beginGracefulShutdown()
        }
        
        let service = EventsService_Client(wrapping: client)
        
        let events = try await service
            .allEvents(Google_Protobuf_Empty())
        
        print(events.events.count)
    }
}

I have my own server on my machine "localhost" with vapor and 1.23.1 grpc version.

@jcalderita "localhost" isn't an IPv4 address, so this should fail.

If you change .ipv4(host: "localhost", port: 6162) to .dns(host: "localhost", port: 6162) the DNS name resolver will be used and should resolve "localhost" to "127.0.0.1" and "::1" and try connecting to both.

It works!!!! Thank you!!!

jcalderita avatar Oct 07 '24 06:10 jcalderita

Issue

unavailable: "channel isn't ready"

Solution

I encountered the same problem today. If anyone has the same problem, you may have forgotten to add the keys to the Info.plist file for mscOS/iOS.

Solution → https://stackoverflow.com/questions/63597760/not-allowed-to-make-request-to-local-host-swift

MacOS

com.apple.security.network.server - Boolean True com.apple.security.network.client - Boolean True

iOS

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/> 
</dict>

or

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>yourdomain.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

sergiusromanof avatar Dec 06 '24 10:12 sergiusromanof

I'm unable to reproduce the problem anymore. The existing code is not compatible with the pinned version of the alpha release. I regenerated the client code and rebuilt everything. Works both with .ip4 and .dns @glbrntt

piotrkowalczuk avatar Jan 16 '25 13:01 piotrkowalczuk