ReactiveX/RxSwift

Using in a React Native Native Module crashes with outlined init with copy of Disposable

david-gettins opened this issue · 7 comments

Short description of the issue:

I am attempting to use this in an iOS Native Module for React Native. I am using the URLSession RXCocoa extension for making calls to an API. As soon as my function is called, the app crashes. The final line in the stack trace is outlined init with copy of Disposable.

Is this a bug, or have I not set things up correctly?

Expected outcome:

Does not crash.

What actually happens:

Crashes.

Self contained code example that reproduces the issue:

Here is the function:

public func request(
    path: String,
    method: String? = nil,
    headers: Dictionary<String, String>? = nil,
    body: Data? = nil
  ) -> Observable<Data> {
      guard let url = URL(string: "\(self.baseUrl)/\(path)") else {
        return Observable.create { observer in
          observer.onCompleted()
          return Disposables.create()
        }
      }

      var request = URLRequest(url: url)
      request.httpMethod = method ?? "GET"

      if let headers = headers {
        headers.forEach { key, value in
          request.setValue(value, forHTTPHeaderField: key)
        }
      }

      if let body = body {
        request.httpBody = body
      }
    
    return URLSession.shared.rx.data(request: request)
  }

Here is its usage:

return HttpClient(baseUrl: self.configuration.baseUrl)
      .request(
        path: "api/something"
        method: "POST",
        headers: [
          "Content-Type" : "application/json",
          "Authorization" : "Bearer \(self.configuration.apiKey)"
        ],
        body: data
      )
      .take(1)
      .map { try? JSONDecoder().decode(SomeObject.self, from: $0) }

While debugging, the error shows on the map.

Platform/Environment

  • iOS
  • macOS
  • tvOS
  • watchOS
  • playgrounds

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

  • easy, 100% repro
  • sometimes, 10%-100%
  • hard, 2% - 10%
  • extremely hard, %0 - 2%

Xcode version:

Version 15.3 (15E204a)

Installation method:

  • CocoaPods
  • Carthage
  • Git submodules
  • Swift package

I have multiple versions of Xcode installed:
(so we can know if this is a potential cause of your issue)

  • yes (which ones)
  • no

Level of RxSwift knowledge:
(this is so we can understand your level of knowledge
and formulate the response in an appropriate manner)

  • just starting
  • I have a small code base
  • I have a significant code base

There's nothing wrong with the code you posted. However, I think this is a bit cleaner:

guard let url = URL(string: "\(baseUrl)/\(path)") else {
    return .empty()
}

Use .empty() rather than Observable.create(_:) when you just want to emit a completed event.

Change that and see what happens.

Thank you that is a nice suggestion. Unfortunately it hasn't resolved the issue. Here is a screenshot of what I am seeing:

Screenshot 2024-04-27 at 13 07 04 Screenshot 2024-04-27 at 13 12 38
// HttpClient.swift

public func request(
  path: String,
  method: String? = nil,
  headers: Dictionary<String, String>? = nil,
  body: Data? = nil
) -> Observable<Data> {
  guard let url = URL(string: "\(self.baseUrl)/\(path)") else {
    return .empty()
  }

  var request = URLRequest(url: url)
  request.httpMethod = method ?? "GET"

  if let headers = headers {
    headers.forEach { key, value in
      request.setValue(value, forHTTPHeaderField: key)
    }
  }

  if let body = body {
    request.httpBody = body
  }
  
  return URLSession.shared.rx.data(request: request)
}
// SomeClient.swift request func

guard let data = try? JSONEncoder().encode(streamable) else {
  return .empty()
}

return httpClient
  .request(
    path: "api/something",
    method: "POST",
    headers: [
      "Content-Type" : "application/json",
      "Authorization" : "Bearer \(self.configuration.apiKey)"
    ],
    body: data
  )
  .take(1)
  .map { try JSONDecoder().decode(SomeObject.self, from: $0) }
// SomeReactNativeModule.swift

let sub = someClient
  .request(input: input)
  .map { $0.toWritable() }
  .subscribe(
    onNext: { writable in
      resolve(writable)
    },
    onError: { error in
      reject("SomeCode", error.localizedDescription, error)
      sub.dispose()
    },
    onCompleted: {
      sub.dispose()
    }
  )

Try this:

_ = someClient
    .request(input: input)
    .map { $0.toWritable() }
    .subscribe(
        onNext: { writable in
            resolve(writable)
        },
        onError: { error in
            reject("SomeCode", error.localizedDescription, error)
        }
    )

It is wholly unnecessary to store the disposable and call dispose() on an error or on completed because disposal is automatic in those instances.

Remember, a Disposable is not like a Combine Cancellable. You don't need to hold it in order to keep the subscription alive.

The information you provided is unfortunately not enough to truly help. The code you pasted seems to work fine in an empty project so I can only assume something in the RN project itself is causing it, and we do not officially support RN at the moment.

If you provide a small reproducible example it'll provide us a few more options to try and assist

Thanks both. I'll knock together a reproduction repo next week. @danielt1263 I'll give your suggestion a go, but i think I tried that already. None the less, if dispose is unnecessary in those circumstances I'll remove it anyway.

@freak4pc I wouldn't be surprised if you're right about it being down to it being in a React Native environment. In unit tests the code works completely fine.

@danielt1263 You were right. Thank you. I feel slightly embarrassed as I have worked with RXJS in the past and did not make such a silly mistake.

Thanks for your help, on a Saturday too.

@david-gettins Glad to hear it helped. Likely what was going on was that your manual dispose and the automatic dispose were executing on different threads and that caused some internal clash. This wouldn't show itself in a test harness unless you wrote the test in a very specific, unusual, way.