Giter VIP home page Giter VIP logo

connect-swift's Introduction

Connect-Swift

Build Version Platform License

Connect-Swift is a small library (<200KB!) that provides support for using generated, type-safe, and idiomatic Swift APIs to communicate with your app's servers using Protocol Buffers (Protobuf). It works with the Connect, gRPC, and gRPC-Web protocols.

Imagine a world where you don't have to handwrite Codable models for REST/JSON endpoints and you can instead get right to building features by calling a generated API method that is guaranteed to match the server's modeling. Furthermore, imagine never having to worry about serialization again, and being able to easily write tests using generated mocks that conform to the same protocol that the real implementations do. All of this is possible with Connect-Swift.

Given a simple Protobuf schema, Connect-Swift generates idiomatic Swift protocol interfaces and client implementations:

Click to expand eliza.connect.swift
public protocol Eliza_V1_ChatServiceClientInterface: Sendable {
    func say(request: Eliza_V1_SayRequest, headers: Headers)
        async -> ResponseMessage<Eliza_V1_SayResponse>
}

public final class Eliza_V1_ChatServiceClient: Eliza_V1_ChatServiceClientInterface, Sendable {
    private let client: ProtocolClientInterface

    public init(client: ProtocolClientInterface) {
        self.client = client
    }

    public func say(request: Eliza_V1_SayRequest, headers: Headers = [:])
        async -> ResponseMessage<Eliza_V1_SayResponse>
    {
        return await self.client.unary(path: "connectrpc.eliza.v1.ElizaService/Say", request: request, headers: headers)
    }
}

This code can then be integrated with just a few lines:

final class MessagingViewModel: ObservableObject {
    private let elizaClient: Eliza_V1_ChatServiceClientInterface

    init(elizaClient: Eliza_V1_ChatServiceClientInterface) {
        self.elizaClient = elizaClient
    }

    @Published private(set) var messages: [Message] {...}

    func send(_ userSentence: String) async {
        let request = Eliza_V1_SayRequest.with { $0.sentence = userSentence }
        let response = await self.elizaClient.say(request: request, headers: [:])
        if let elizaSentence = response.message?.sentence {
            self.messages.append(Message(sentence: userSentence, author: .user))
            self.messages.append(Message(sentence: elizaSentence, author: .eliza))
        }
    }
}

That’s it! You no longer need to manually define Swift response models, add Codable conformances, type out URL(string: ...) initializers, or even create protocol interfaces to wrap service classes - all this is taken care of by Connect-Swift, and the underlying network transport is handled automatically.

Testing also becomes a breeze with generated mocks which conform to the same protocol interfaces as the production clients:

Click to expand eliza.mock.swift
open class Eliza_V1_ChatServiceClientMock: Eliza_V1_ChatServiceClientInterface, @unchecked Sendable {
    public var mockAsyncSay = { (_: Eliza_V1_SayRequest) -> ResponseMessage<Eliza_V1_Response> in .init(message: .init()) }

    open func say(request: Eliza_V1_SayRequest, headers: Headers = [:])
        async -> ResponseMessage<Eliza_V1_SayResponse>
    {
        return self.mockAsyncSay(request)
    }
}
func testMessagingViewModel() async {
    let client = Eliza_V1_ChatServiceClientMock()
    client.mockAsyncSay = { request in
        XCTAssertEqual(request.sentence, "hello!")
        return ResponseMessage(result: .success(.with { $0.sentence = "hi, i'm eliza!" }))
    }

    let viewModel = MessagingViewModel(elizaClient: client)
    await viewModel.send("hello!")

    XCTAssertEqual(viewModel.messages.count, 2)
    XCTAssertEqual(viewModel.messages[0].message, "hello!")
    XCTAssertEqual(viewModel.messages[0].author, .user)
    XCTAssertEqual(viewModel.messages[1].message, "hi, i'm eliza!")
    XCTAssertEqual(viewModel.messages[1].author, .eliza)
}

Quick Start

Head over to our quick start tutorial to get started. It only takes ~10 minutes to complete a working SwiftUI chat app that uses Connect-Swift!

Documentation

Comprehensive documentation for everything, including interceptors, mocking/testing, streaming, and error handling is available on the connectrpc.com website.

Example Apps

Example apps are available in the Examples directory and can be opened and built using Xcode. They demonstrate:

Contributing

We'd love your help making Connect better!

Extensive instructions for building the library and generator plugins locally, running tests, and contributing to the repository are available in our CONTRIBUTING.md guide. Please check it out for details.

Ecosystem

Status

This project is in beta, and we may make a few changes as we gather feedback from early adopters. Join us on Slack!

Legal

Offered under the Apache 2 license.

connect-swift's People

Contributors

buildbreaker avatar chrispine avatar dependabot[bot] avatar eseay avatar mbernson avatar pkwarren avatar rebello95 avatar rubensf avatar scottybobandy avatar smallsamantha avatar smaye81 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

connect-swift's Issues

cardinality violations should use error code "unimplemented"

The gRPC docs for error codes state that both client and server should use the unimplemented code for cardinality violations. See table at the bottom of this doc (you can search for “cardinality violation” in the doc): https://grpc.github.io/grpc/core/md_doc_statuscodes.html.

A cardinality violation is when a stream contains an incorrect number of messages. Specifically, when a response stream for a unary or client-stream RPC contains zero messages with an OK status or more than one message.

The client in this repo does not report an unimplemented error code to the application in these cases.

client should enforce configured timeout

In #247, the ability to set an optional timeout on the protocol config was added. But the timeout is just advisory: it is passed along to the server via a protocol-specific timeout header, but not the operation is not canceled by the client after the timeout elapses.

the target 'Connect' in product 'Connect' contains unsafe build flags

Hi!

My iOS app uses a private Swift Package, that contains my generated protobuf Swift code. This package has a dependency on Connect.

After I bumped up the Connect version to 0.10.0, I started getting some errors from the Swift Package Manager:

error: the target 'Connect' in product 'Connect' contains unsafe build flags
error: the target 'Connect' in product 'ConnectMocks' contains unsafe build flags
error: the target 'ConnectMocks' in product 'ConnectMocks' contains unsafe build flags

This is also not resolved when bumping the swift-tools-version to 5.9. The commit that introduced this seems to be this one.

Generated Mocks don't include import to connect interface definition

The generated mocks don't import the actual protocol they are implementing, making this unusable at compile time.

Taking the example from the docs:

import Combine
import Connect
import ConnectMocks
import Foundation
import SwiftProtobuf

/// Mock implementation of `Connectrpc_Eliza_V1_ElizaServiceClientInterface`.
open class Connectrpc_Eliza_V1_ElizaServiceClientMock: Connectrpc_Eliza_V1_ElizaServiceClientInterface, @unchecked Sendable {
   ...  
}

There should be an import for something that provides Connectrpc_Eliza_V1_ElizaServiceClientInterface

client reports incorrect error codes when server returns unexpected content-type, content-encoding

These details are not all fully described in the spec (at least not yet). But the conformance suite does have expectations, in an effort to bring consistency to implementations even before the spec updates are complete.

It expects the following:

  1. If the content-type has the wrong prefix ("application/" for Connect unary, "application/connect+" for Connect streaming, "application/grpc" for gRPC, and "application/grpc-web" for gRPC-Web), then it appears to be a web server that is not an RPC server. In that case, return an error whose code is derived from the HTTP status code. For gRPC and Connect streaming, where the status code is expected to be 200 OK, this will result in a derived error code of "unknown". This should also be done for any response to a Connect unary response with a non-200 HTTP status whose content-type is not "application/json".
  2. If the content-type has a correct prefix, but indicates an unexpected message-codec, the client should report an "internal" error, since the server appears to be an RPC server but sent an invalid response, which is interpreted by the client as an internal server error.
  3. If the server sends back a compression encoding that is not supported, the client should report an "internal" error. This is interpreted as an internal server error since a correct server will only use compression that the client is known to accept (via request headers indicating the accepted encodings).
  4. If no compression is in use (so no content encoding header or an encoding of "identity"), if a streaming protocol includes the compressed bit set for a message, it should report an "internal" error.

The latter two are consistent with the docs for compression handling with gRPC: https://github.com/grpc/grpc/blob/master/doc/compression.md.

The client implementation in this repo does not behave in the way described above for these scenarios.

Add leading slash to methodPath (while generating code)

The ProtocolClient provided as input to the generated <service>.connect.swift file has a host attribute in its initialization, and the underlying code ends up calling:

let url = URL(string: path, relativeTo: URL(string: self.config.host))!

Issue is that it does not allow for extra intermediary path elements to stand between host and path. So, for example, exposing the gRPC service under https://my.company.com works fine, but under https://my.company.com/services does not. When using the solely exposed host: "https://my.company.com/services" property to overcome the issue, unfortunately the underlying URL(string, relativeTo: URL) implementation "swallows" that extra /services path in the process.
The workaround, on top of the first workaround used to add the extra path to the host attribute, is to add an extra trailing slash to everything, so host: "https://my.company.com/services/" does work, the extra /services path keeps there and calls work just fine.

I noticed the plugin code below does not include a leading slash to the generated methodPath, what would pretty much solve the issue. Note that this leading slash is indeed added to the Go generated code, but not for the Swift and Kotlin generated code using Connect plugins.

Go generated code:

const (
	MyService_MyMethod_FullMethodName = "/path.to.MySergice/MyMethod"
)

Connect plugin code generator not including the leading slash:
https://github.com/bufbuild/connect-swift/blob/61b79789c5a3179c6cd06b62f41a22e91bceea87/Plugins/ConnectSwiftPlugin/ConnectClientGenerator.swift#L191

Options here are:

  • In the plugin, add a leading slash to the generated code.
  • In ProtocolClient, expose an extra initialization attribute for extra path.
  • In ProtocolClient, change the host initialization attribute name to baseUrl and call let url = URL(baseUrl+path) and make sure there's a slash between baseUrl and path.

Inconsistent Timeout Behavior in gRPC with NIOHTTPClient

Hi,

We are encountering an issue with the timeout setting in NIOHTTPClient when used with gRPC. We have configured a timeout of 30 seconds for requests, expecting that any request exceeding this duration should automatically timeout. However, this behavior is inconsistent and hard to reproduce.

Details:

Environment: The issue occurs on iOS 16 and iOS 17, across both iPhone and iPad devices.
Version: Connect 0.12.0.
Issue: Requests occasionally do not timeout after 30 seconds as expected.
Current Workaround: Implemented a manual timeout mechanism that cancels the request at 35 seconds, with these incidents specifically logged.
Configuration: Using .grpc for networkProtocol and JSONCodec() for codec.

Question: Are there any known issues with request timeouts in gRPC using NIOHTTPClient that could be causing this behavior? Could you provide guidance on ensuring the timeout setting is consistently respected? Is there a specific configuration tweak required?

Thank you for any help and let me know if there's other info I can provide.

GET Support Through AWS ALB -> gRPC Target Group

First off, I have to say thanks for the amazing project! Connect is proving to be an amazing technology allowing us to bring gRPC to our frontend technology is a very scalable way.

Intro

We're now exploring the possibility of switching from POST to GET requests with the approach laid out in the Get Requests and Caching docs. I've managed to get our Go server up and running with this as well as a Swift client where I'm modifying the request structure the same way the Go client is.

Is there a particular reason this feature is missing from the Connect-Swift library? If not, I would be willing to help pitch in to add it if that would be preferred.

I have a local setup where everything is working flawlessly. The goal for us (and most I'd assume) is to be able to leverage both our CDN and client side caches (URLCache in this case) without all the hurdles POST request types involve.

Issue

The roadblock we're running in to is that our AWS ALB is throwing a 464 when attempting to use a GET instead of a POST. From what we can tell thus far, this is all by design from AWS. We're fairly certain we're running in to the last condition, but was hoping you could help confirm.

The request protocol is an HTTP/2 and the request is not POST, while target group protocol version is a gRPC.

After digging around for a bit, I came across a few helpful tips, and they seem to be geared towards gRPC and not Connect, and this is where I could use some help.

Ask

Most of those threads leave me to believe that credential channels may be the solution here, but I can't find any documentation about them in the Connect protocol. Does the group have any guidance here? Is there an obvious problem here that I'm missing? Is this a gap in Connect? Any advice you could provide here would be most appreciated.

Cheers. 🍻

Can we prepend onto the path to allow for how the server is set up?

I have a service like example.v1.ExampleService/SayHello.

Which gives me a full url like... https://our.server.com/example.v1.ExampleService/SayHello.

But the actual path in the server has been configured to be like... https://our.server.com/some/api/path/example.v1.ExampleService/SayHello.

I'm not sure how to configure this as it doesn't seem to be something I have access to?

If I add the path onto the host when creating the config it is truncated by the time the request is made.

Is this something that we're able to do?

Thanks

classification of "trailers-only" responses for gRPC and gRPC-Web is not quite right

The logic in the client looks for a "grpc-status" key in response headers to decide if the response is a "trailers-only" response. But the gRPC spec classifies this more rigorously as a headers frame that terminates the HTTP/2 stream. Expanding this to HTTP 1.1, it would be a response that has headers but no response body and no trailers. The current mechanism would misinterpret a response as "trailers-only" if it had an erroneous "grpc-status" header key, even if the response had both a response body and subsequent trailers. To be more compliant with the spec, this client should only consider a response to be "trailers-only" when it observes the end of the response and there was no body and were no trailers. Only then should it query for a "grpc-status" key from the headers.

Please allow async interceptors

Could you please allow asynchronous interceptors?

Right now the only way I managed to implement authentication was to generate my auth headers asynchronously (as they may be refreshed by gtmappauth):

@Sendable func authHeaders() async throws -> Connect.Headers {
      let idToken = try await session.token()
      return ["authorization": ["Bearer \(token)"]]
}
    
 let response = await client.info(request: req, headers: try authHeaders())

Use port number from URL, if present

The NIOHTTPClient takes the host URL, and also an optional port number as arguments.

However, the URL is also able to have a port number in it, for example: https://server.example.com:8080.

It is important for anyone who is already using the port argument to continue to be able to set it, but if it isn't explicitly set, we should use the port number from the URL, if present.

Currently, the initializer sets the port like this:

self.port = port ?? (useSSL ? 443 : 80)

Checking if the host URL has a port should be able to be done like this:

self.port = port ?? baseURL.port ?? (useSSL ? 443 : 80)

This uses:

  1. The explicit port number, if present
  2. The port number from the URL, if present
  3. The default port (80 or 443)

Conflicting gRPC Response

  • Searched the repo's previous issues for similar reports.
  • Searched the internet for any similar reports.

Issue Summary

I am encountering an unusual issue with the response object for a gRPC message. While the statusCode indicates ok and result returns success, the headers provide contradictory information. Specifically, the headers contain a status code of 2, and the grpc-message reports an error.

This conflicting information is making it challenging to determine whether an operation was successful or not.

Detailed Description

I've confirmed that this issue originates from the client side by using Postman to access the same endpoint, which returns the correct information.

iOS Response:

In the iOS response, the following points illustrate the inconsistency:

  1. The status code is displayed as ok/0.
  2. The headers show a statusCode of 2/unknown error.
  3. The grpc-message in the headers also reports an error.

CleanShot 2023-09-13 at 15 22 30@2x

Postman Response:

Contrastingly, when using Postman, I receive the correct response with no contradictory information.

CleanShot 2023-09-13 at 15 28 31@2x

Possible Cause

Through my investigation, I believe I've identified a potential cause of this issue. In the ProtocolClient, specifically in the unary(path:request:headers:completion:) function, the onResponse block checks response.code != .ok to throw an error. However, in this case, response.code is 0, while the headers indicate 2. Consequently, success is returned instead of raising a ConnectError with .failure.

urlSession warning in xcode 15 beta

I see this when using swift connect in the latest xcode:

The request of a upload task should not contain a body or a body stream, use `upload(for:fromFile:)`, `upload(for:from:)`, or supply the body stream through the `urlSession(_:needNewBodyStreamForTask:)` delegate method.
Screenshot 2023-08-04 at 12 40 15 AM Screenshot 2023-08-04 at 1 40 51 AM

Road to v1.0 🥇

This issue is to track our progress towards tagging a stable v1.0 release of Connect-Swift.

At this time, we've mostly completed the key tasks that we see as prerequisites for a v1.0:

  • Support for the Connect, gRPC, and gRPC-Web protocols
  • Interceptors which allow for mutating both typed models and raw data, as well as performing async work
  • Support for Connect Unary GET requests
  • Compliance with the conformance test suite
  • Have several companies using Connect-Swift in production at scale
  • Gather more feedback from the community on Connect's usability and APIs so that we can keep them backward-compatible once v1.0 is reached

At this time, our goal is to tag a v1.0 around July 1, 2024. This timeline will allow us to scale adoption, gather feedback, and react to any big news coming out of WWDC with the goal of minimizing the risk of needing to introduce breaking changes shortly after tagging a v1.0. We plan to tag release candidates in the months leading up to this date.

In the meantime, we encourage the community to stay up-to-date with the latest Connect-Swift version and to share feedback through GitHub issues so that we can improve the library over the coming months before tagging a stable release.

We'll to keep this issue updated as we make progress! 🚀

Is it possible to see the underlying request?

I'm getting some errors setting up this SDK for the first time with a new back end too.

But, I'm not sure where exactly the errors are coming from or why.

Is it possible to see the full underlying request (i.e. path, method, body, host, headers, etc...) so that I can try to debug what I'm doing wrong?

Thanks

Cannot compile on Linux target - "no such module 'os.log'"

I have my generated Swift clients and the project builds in macOS. When I build on Linux I get the following issue:

[112/150] Compiling Connect ResponseCallbacks.swift
/root/project/clients/swift/proto-common/.build/checkouts/connect-swift/Libraries/Connect/Public/Implementation/Clients/ProtocolClient.swift:16:8: error: no such module 'os.log'
import os.log

I believe this is because os.log is only defined for macOS and not Linux. As far as I can tell the gRPC generators do not have this limitation.

v0.10.1: Missing argument for parameter idempotencyLevel in call...

I get a bunch of these Missing argument for parameter idempotencyLevel in call errors in my service.connect I generated with the latest buf.

What do I have to do to use 0.10.1? Thanks

version: v1
plugins:
  - plugin: go
    out: gen
    opt: paths=source_relative
  - plugin: connect-go
    out: gen
    opt: paths=source_relative
  - plugin: buf.build/bufbuild/connect-swift
    opt: >
      GenerateAsyncMethods=true,
      GenerateCallbackMethods=true,
      Visibility=Public
    out: Sources
  - plugin: buf.build/apple/swift
    opt: Visibility=Public
    out: Sources%    

Thread Performance Warning Due to Priority Inversion when Initializing NIO Client

Description:

When initializing the Connect client, the following warning arises:

Thread Performance Checker: Thread running at User-initiated quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions.
PID: 7210, TID: 8927

This warning appears to be related to this issue in the swift-nio repository. It seems connect-swift might be impacted by the underlying problem with swift-nio.

Expected Behavior: No thread performance warnings should be present when initializing the Connect client.

Steps to Reproduce:

  1. Initialize the Connect client in a Swift project.
  2. Observe warnings during runtime.

Environment Details:

  • Connect-Swift version: 0.8.0
  • Xcode 15

connect-swift can't compile for watchOS

Hey team 👋

We're working on adopting connect-swift and are running into some issues compiling connect-swift for our watchOS app.

For context, we are attempting to perform unary requests from a watchOS app using the URLSessionHTTPClient and grpc-web protocol. However, connect-swift imports CFNetwork (only 1 explicit import here AFAIK) which is unavailable on watchOS, so we can't actually compile for our watchOS target.

In addition, Technote TN3135 describes the 3 specific circumstances when low-level networking is allowed on watchOS. watchOS blocks low-level networking outside of these specific circumstances, impacting aspects of URLSession including URLSessionStreamTask and URLSessionWebSocketTask.

With this limitation on low-level networking, it's likely stream requests will fail even if we can compile and run on watchOS. This is OK for our immediate use-case as we only need to execute unary requests, but a consideration for the future.

Is watchOS support, or at a minimum, the ability to compile and execute unary requests, a reasonable prerequisite for a stable v1.0 release?

No indication of a failing stream message sending in case of internet unavailability

When internet becomes unavailable, there is no indication message sending through stream's send was unsuccessful

  • Searched for similar issues in the repo
  • Searched for similar issues in the internet

Problem description

When launching a stream, there is no way to handle unsuccessful stream requests due to internet unavailability. Initially I thought that it is handled by throwing an error on send directly. but since send is not asynchronous it can't be handled there. Then I thought that this might be indicated as a complete case in streams results, which unfortunately is also not the case.

Because of those challenges I currently need to use pre-flight internet availability checks to not lose data and store it locally if it can't be sent through the stream. Normally I would remove data locally when I know that it is delivered to the server. In this case, it however is not possible.

In my particular case we are using client only async stream to transfer data to the server. We could of course try to use bidirectional stream to await for server confirmations for each sent event. There would also need to be a handler/manager that would check for response time from the server and indicate that the connection is down when the timeout is reached.

Question

Might I be doing something wrong with the setup that I don't receive a complete result in results stream? Or is it indeed the expected behaviour

Set up

We use a very simple set up of a protocol client:

ProtocolClient(
            httpClient: NIOHTTPClient(
                host: "our host",
                port: "our port"
            ),
            config: .init(
                host: "our host",
                networkProtocol: .grpc,
                codec: ProtoCodec(),
                interceptors: [our interceptors]
            )
        )

Publish plugin artifact for external plugin usage

(Based on connectrpc/connect-kotlin#4)

Currently, the plugin is only published to the BSR. It would be nice for users of Connect-Swift to also be able to consume the plugin independently from the BSR.

Unlike Android which has multiple options for publishing the plugin (via maven or GH releases), Swift doesn't have a central repository, so I think GH releases are the most logical way to do it.

Asynchronous Interceptors

Brought this up to @rebello95. It looks like interceptors are currently synchronous, but it would be great if they could be async if possible. A common use case is using an interceptor to potentially re-authenticate an expired token before continuing on with the request.

tvOS support

It looks like the Package.swift and Connect-Swift.podspec files only specify iOS and macOS. Is there any reason why tvOS support is excluded, and if not, could it be added?

Create 0.11.0 release

Hey folks,

Could someone from the team create a new 0.11.0 release, since watch support has been added? 🙏

I haven't got permissions to draft releases for this repo.

Thanks, and happy new year!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.