sentry-cocoa icon indicating copy to clipboard operation
sentry-cocoa copied to clipboard

Custom URLSession Memory & CPU Usage

Open Memocana opened this issue 1 year ago • 5 comments

Platform

iOS

Environment

Develop, TestFlight

Installed

Swift Package Manager

Version

8.30.0

Xcode Version

15.2

Did it work on previous versions?

No response

Steps to Reproduce

Using the sample URLSession here: https://github.com/getsentry/sentry-cocoa/discussions/4047#discussioncomment-9739280

I added a button that sends an exception and an error in to my app. When I disable the URL Session, everything sends nicely. When I reenable it, I see two things happening: 1- I memory usage continuously increases with 100% CPU usage until it force shuts the app 2- The canonicalRequest function never gets called after the handshake with the backend, no matter how many times SentrySDK.capture(exception:) or SentrySDK.capture(error:) are called

I just have an empty screen with a button to test this and the app reliably always crashes.

Expected Result

No crashes and consistent memory usage similar to the default URLSession.

Actual Result

The app exhausts the memory image image

class CustomURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return URLProtocol.property(forKey: "Handled", in: request) == nil
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        guard let newUrl = URL(string: "xxx/forwardSentry") else { return request }
        var newRequest = URLRequest(url: newUrl)
        newRequest.httpBody = request.httpBody
        newRequest.httpBodyStream = request.httpBodyStream
        newRequest.httpMethod = request.httpMethod
        newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields

        // Breakpoint here never gets stopped after the first 5 times
        return newRequest
    }
    
    override func startLoading() {
        URLProtocol.setProperty(true, forKey: "Handled", in: request as! NSMutableURLRequest)
        let newTask = URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data { self.client?.urlProtocol(self, didLoad: data) }
            if let response = response { self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) }
            if let error = error { self.client?.urlProtocol(self, didFailWithError: error) }
            self.client?.urlProtocolDidFinishLoading(self)
        }
        newTask.resume()
    }
    
    override func stopLoading() {
        // Not implemented on purpose
    }
}
@main
struct TestApp: App {
    
    init() {
        let config = URLSessionConfiguration.default
        config.protocolClasses = [CustomURLProtocol.self]
        
        SentrySDK.start { options in
            options.dsn = "https://734ed0faf0e9b359e6490bf0272427bf@o4507339912904704.ingest.us.sentry.io/4507391647547392"
            options.urlSession = URLSession(configuration: config)
            options.debug = true // Enabled debug when first installing is always helpful
            // Enable tracing to capture 100% of transactions for tracing.
            // Use 'options.tracesSampleRate' to set the sampling rate.
            // We recommend setting a sample rate in production.
            options.enableTracing = true
            options.tracesSampleRate = 0.005
            options.profilesSampleRate = 0.1
            options.maxBreadcrumbs = 1
        }
    }

  var body: some Scene {
    WindowGroup {
      Button {
        let id = SentrySDK.capture(exception: NSException(name: .genericException, reason: "Login Failure"))
        print(id.description) //ID is correctly populated, but the backend never receives a request, nor the dashboard has an exception when the URL Session is on
      } label: {
        Text("Sign-in")
          .foregroundColor(.white)
          .padding(.horizontal, 24)
          .padding(.vertical, 12)
          .background(RoundedRectangle(cornerRadius: 10).fill(.cyan))
      }
    }
  }
}

Are you willing to submit a PR?

No response

Memocana avatar Jul 11 '24 21:07 Memocana

Hello @Memocana, thanks for reaching out. The code snippet provided at #4047 is a starting point where you can continue to write your own custom URLSession. However, we don't have the bandwidth to help you optimize it and handle all the edge cases. Users should be cautious when using a custom URLSession.

We will discuss about providing a safer alternative to implement proxies.

brustolin avatar Jul 12 '24 07:07 brustolin

We will try to repro the issue and follow up here.

kahest avatar Jul 17 '24 12:07 kahest

Hey there! I was able to identify the root cause of the issue on our end. It looks like when the backend sends 200 with body/headers that are missing/wrong the SDK goes in to a frozen state. We worked with our backend guys to make sure the responses are sent the correct way, but the SDK should still be able to handle these scenarios more gracefully.

Memocana avatar Jul 19 '24 18:07 Memocana

Can you give an example of wrong header/body that your server was returning?

brustolin avatar Jul 22 '24 05:07 brustolin

URLResponse:
{ URL: <tunnel_url> },
{ 
  Status Code: 200, 
  Headers {
    "Content-Length" =     (
        2
    );
    "Content-Type" =     (
        "application/json; charset=utf-8"
    );
    Date =     (
        "Fri, 19 Jul 2024 17:12:02 GMT"
    );
    Etag =     (
        "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\""
    );
    "Strict-Transport-Security" =     (
        "max-age=15552000; includeSubDomains"
    );
    "content-security-policy" =     (
        "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
    );
    "cross-origin-embedder-policy" =     (
        "require-corp"
    );
    "cross-origin-opener-policy" =     (
        "same-origin"
    );
    "cross-origin-resource-policy" =     (
        "same-origin"
    );
    "origin-agent-cluster" =     (
        "?1"
    );
    "referrer-policy" =     (
        "no-referrer"
    );
    "x-content-type-options" =     (
        nosniff
    );
    "x-dns-prefetch-control" =     (
        off
    );
    "x-download-options" =     (
        noopen
    );
    "x-frame-options" =     (
        SAMEORIGIN
    );
    "x-permitted-cross-domain-policies" =     (
        none
    );
    "x-xss-protection" =     (
        0
    );
  } 
}

This was the initial response we had, and it contained no body. We also tried modifying the body to return { "id": <id> } similarly to how Sentry API returns but the cpu issues persisted even after that until the headers were identical too.

Memocana avatar Jul 27 '24 16:07 Memocana

Closing due to inactivity. Please reopen if the problem persists.

philipphofmann avatar Mar 12 '25 13:03 philipphofmann