Xcode 26 crash upon dealloc of `WKNavigationResponse` on Main Thread

Since Xcode 26 our tests are crashing due to the Main Thread not being able to deallocate WKNavigationResponse.

Following an example:

import Foundation
import WebKit

final class WKNavigationResponeMock: WKNavigationResponse {
    private let urlResponse: URLResponse

    override var response: URLResponse { urlResponse }

    init(urlResponse: URLResponse) {
        self.urlResponse = urlResponse

        super.init()
    }

    convenience init(httpUrlResponse: HTTPURLResponse) {
        self.init(urlResponse: httpUrlResponse)
    }

    convenience init?(url: URL, statusCode: Int) {
        guard let httpURLResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil) else {
            return nil
        }

        self.init(httpUrlResponse: httpURLResponse)
    }
}

import WebKit
import XCTest

final class ExampleTests: XCTestCase {

@MainActor func testAllocAndDeallocWKNavigationResponse() {
  let expectedURL = URL(string: "https://galaxus.ch/")!
  let expectedStatusCode = 404
  let instance = WKNavigationResponeMock()
// here it should dealloc/deinit `instance` automatically
}

Here the call stack:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   CoreFoundation                	       0x101f3dd54 CFRetain.cold.1 + 16
1   CoreFoundation                	       0x101e14860 CFRetain + 104
2   WebKit                        	       0x10864dd24 -[WKNavigationResponse dealloc] + 52

Following my Bug Report: FB20763245

Following my Bug Report: FB20763245

Thanks.

I took a look at that and ran into a few problems:

  • There’s no crash report.
  • And no sysdiagnose log from which I might extract a crash report.
  • You’ve attached what I suspect is a test project, but something went wrong with the attachment process because the zip archive is only 29 bytes long )-:
  • And the code you posted above doesn’t compile )-:

So, I created a new test project from the iOS > App templates and added your code to the test suite. With that I was able to trigger a crash, but it’s not clear that it’s the same crash you’re seeing. Specifically, the backtrace is quite different from the one you posted:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0 JavaScriptCore  … WTF::RunLoop::dispatch(WTF::Function<void ()>&&) + 48
1 WebCore         … WebCoreObjCScheduleDeallocateOnMainRunLoop(objc_class*, objc_object*) + 76
2 WebKit          … -[WKNavigationResponse dealloc] + 40
3 Test804615Tests … Test804615Tests.testAllocAndDeallocWKNavigationResponse() + 480 (Test804615Tests.swift:66)
4 Test804615Tests … @objc Test804615Tests.testAllocAndDeallocWKNavigationResponse() + 44

I’m not sure if that difference is significant or not.

Any chance you can update you Feedback Assistant report with your test project? That way I can be sure that I’m starting from the right place.


Oh, and just FYI, my crash is coming out WebKit code code that’s open source. Specifically, this code has decided it’s not on the main thread and thus it needs to bounce to the main thread, and it’s crashing doing that bounce. The weird part is that it is actually on the main thread. I can dig into that more once I’ve confirmed that we’re working from the same playbook.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for the feedback and the insight!

Weird that my code snippet was not properly provided via Feedback Assistant. Never less, I uploaded more attachments ;)

Regarding your investigation. After creating a fresh project with Xcode and attaching the same code snippet into it, I have also found a similar stacktrace as yours.

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   JavaScriptCore                	       0x1998806d4 WTF::RunLoop::dispatch(WTF::Function<void ()>&&) + 48
1   WebCore                       	       0x18f8a6820 WebCoreObjCScheduleDeallocateOnMainRunLoop(objc_class*, objc_object*) + 76
2   WebKit                        	       0x18d8ae10c -[WKNavigationResponse dealloc] + 40
3   WebKitCrashTests              	       0x1039edbb8 WebKitCrashTests.testAllocAndDeallocWKNavigationResponse() + 748 (WebKitCrashTests.swift:49)
4   WebKitCrashTests              	       0x1039edea4 @objc WebKitCrashTests.testAllocAndDeallocWKNavigationResponse() + 44


Thank you for pointing me toward the open-source WebKit repository and indicating where the crash might be occurring. I find it strange that this issue appears only after Xcode 26.0.0, especially since the line of code you referenced hasn’t changed in over three years. What could have changed internally to cause this new behavior?

And for anybody who's interested into running the same:

//
//  WebKitCrashTests.swift
//  WebKitCrashTests
//
//  Created by Luca Archidiacono on 27.10.2025.
//

import Foundation
import WebKit
 
final class WKNavigationResponeMock: WKNavigationResponse {
    private let urlResponse: URLResponse
 
    override var response: URLResponse { urlResponse }
 
    init(urlResponse: URLResponse) {
        self.urlResponse = urlResponse
 
        super.init()
    }
 
    convenience init(httpUrlResponse: HTTPURLResponse) {
        self.init(urlResponse: httpUrlResponse)
    }
 
    convenience init?(url: URL, statusCode: Int) {
        guard let httpURLResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil) else {
            return nil
        }
 
        self.init(httpUrlResponse: httpURLResponse)
    }
}
 
import WebKit
import XCTest
 
final class WebKitCrashTests: XCTestCase {
    
    @MainActor func testAllocAndDeallocWKNavigationResponse() {
        let expectedURL = URL(string: "https://galaxus.ch/")!
        let expectedStatusCode = 404
        guard let expectedResponse = WKNavigationResponeMock(url: expectedURL, statusCode: expectedStatusCode) else {
            assertionFailure("Should be not nil")
            return
        }
        
        XCTAssertEqual(expectedResponse.response.url, expectedURL)
    }
}

This should work now ;)

I find it strange that this issue appears only after Xcode 26.0.0

I don’t find it that strange. The code relies on detecting the main thread, and that’s has a tendency to be brittle [1]. If you look at the implementation of the isMainThread() method routine that it calls, you’ll find a bunch of special cases. It’s possible that code changed, but it’s also possible that Xcode’s testing infrastructure has changed to ‘break’ that code. The latter seems more likely given that we’re in the process of integrating Swift Testing into an Xcode that’s lived with XCTest for a long time.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Once one realises that it’s possible to have a process that has no main thread and also code running on the main queue that’s not running on the main thread, one starts to think that perhaps this whole detecting the main thread business is more complex than it seems and maybe one should structure one’s code to avoid the necessity to do that (-:

Xcode 26 crash upon dealloc of &#96;WKNavigationResponse&#96; on Main Thread
 
 
Q