This is an automated email from the ASF dual-hosted git repository.

GitToTheHub pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new 00695c4c fix: `NSInternalInconsistencyException: "No response has been 
sent for this task"` - second attempt (#1640)
00695c4c is described below

commit 00695c4c29429dab11d6e0b27265f00481bc27ca
Author: Manuel Beck <[email protected]>
AuthorDate: Fri Apr 24 12:42:12 2026 +0200

    fix: `NSInternalInconsistencyException: "No response has been sent for this 
task"` - second attempt (#1640)
    
    * fix: don't call `[WKURLSchemeTask
    didFinish]` in `webView:stopURLSchemeTask:``
    
    - Calling `[WKURLSchemeTask
    didFinish]` in `webView:stopURLSchemeTask:` will raise the exception 
`NSInternalInconsistencyException - This task has already been stopped`
    - That an exception is raised in this situation is also documented in the 
Apple documentation for `WKURLSchemeTask
    didFinish`: 
https://developer.apple.com/documentation/webkit/wkurlschemetask/didfinish()?language=objc
    - Fixes https://github.com/apache/cordova-ios/issues/1638
    - Generated-By: GPT-5.3-Codex, GitHub Copilot Chat
    
    * fix(ios): Do less in background thread in `webView:startURLSchemeTask:`
    
    - Resolve file errors and range validation before background execution
    - Send the URL scheme response before queuing background reads
    - Exit early when task is no longer active and close the file handle
    
    * fix(ios): ensure WKURLSchemeTask callbacks are dispatched on the main 
thread to prevent race conditions
    
    - Generated-By: Claude Sonnet 4.6, GitHub Copilot Chat
---
 .../Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m | 165 +++++++++++---------
 tests/CordovaLibTests/CDVURLSchemeHandlerTest.m    | 171 +++++++++++++++++++++
 2 files changed, 261 insertions(+), 75 deletions(-)

diff --git 
a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m 
b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m
index 5b74555d..e031e9c0 100644
--- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m
+++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m
@@ -73,102 +73,118 @@ static const NSUInteger FILE_BUFFER_SIZE = 1024 * 1024 * 
4; // 4 MiB
     // We do this so that we can (in future) detect if the task is cancelled 
before we finished feeding it response data
     [self.handlerMap setObject:(id)[NSNull null] forKey:urlSchemeTask];
 
-    [self.viewController.commandDelegate runInBackground:^{
-        NSURL *fileURL = [self fileURLForRequestURL:req.URL];
-        NSError *error;
-
-        NSFileHandle *fileHandle = [NSFileHandle 
fileHandleForReadingFromURL:fileURL error:&error];
-        if (!fileHandle || error) {
-            if ([self taskActive:urlSchemeTask]) {
-                [urlSchemeTask didFailWithError:error];
-            }
+    NSURL *fileURL = [self fileURLForRequestURL:req.URL];
+    NSError *error;
 
-            @synchronized(self.handlerMap) {
-                [self.handlerMap removeObjectForKey:urlSchemeTask];
-            }
-            return;
+    NSFileHandle *fileHandle = [NSFileHandle 
fileHandleForReadingFromURL:fileURL error:&error];
+    if (!fileHandle || error) {
+        if ([self taskActive:urlSchemeTask]) {
+            [urlSchemeTask didFailWithError:error];
         }
 
-        NSInteger statusCode = 200; // Default to 200 OK status
-        NSString *mimeType = [self getMimeType:fileURL] ?: 
@"application/octet-stream";
-        NSNumber *fileLength;
-        [fileURL getResourceValue:&fileLength forKey:NSURLFileSizeKey 
error:nil];
-
-        NSNumber *responseSize = fileLength;
-        NSUInteger responseSent = 0;
+        @synchronized(self.handlerMap) {
+            [self.handlerMap removeObjectForKey:urlSchemeTask];
+        }
+        return;
+    }
 
-        NSMutableDictionary *headers = [NSMutableDictionary 
dictionaryWithCapacity:5];
-        headers[@"Content-Type"] = mimeType;
-        headers[@"Cache-Control"] = @"no-cache";
-        headers[@"Content-Length"] = [responseSize stringValue];
-
-        // Check for Range header
-        NSString *rangeHeader = [urlSchemeTask.request 
valueForHTTPHeaderField:@"Range"];
-        if (rangeHeader) {
-            NSRange range = NSMakeRange(NSNotFound, 0);
-
-            if ([rangeHeader hasPrefix:@"bytes="]) {
-                NSString *byteRange = [rangeHeader substringFromIndex:6];
-                NSArray<NSString *> *rangeParts = [byteRange 
componentsSeparatedByString:@"-"];
-                NSUInteger start = (NSUInteger)[rangeParts[0] integerValue];
-                NSUInteger end = rangeParts.count > 1 && ![rangeParts[1] 
isEqualToString:@""] ? (NSUInteger)[rangeParts[1] integerValue] : [fileLength 
unsignedIntegerValue] - 1;
-                range = NSMakeRange(start, end - start + 1);
-            }
+    NSInteger statusCode = 200; // Default to 200 OK status
+    NSString *mimeType = [self getMimeType:fileURL] ?: 
@"application/octet-stream";
+    NSNumber *fileLength;
+    [fileURL getResourceValue:&fileLength forKey:NSURLFileSizeKey error:nil];
+
+    NSNumber *responseSize = fileLength;
+    NSMutableDictionary *headers = [NSMutableDictionary 
dictionaryWithCapacity:5];
+    headers[@"Content-Type"] = mimeType;
+    headers[@"Cache-Control"] = @"no-cache";
+    headers[@"Content-Length"] = [responseSize stringValue];
+
+    // Check for Range header
+    NSString *rangeHeader = [urlSchemeTask.request 
valueForHTTPHeaderField:@"Range"];
+    if (rangeHeader) {
+        NSRange range = NSMakeRange(NSNotFound, 0);
+
+        if ([rangeHeader hasPrefix:@"bytes="]) {
+            NSString *byteRange = [rangeHeader substringFromIndex:6];
+            NSArray<NSString *> *rangeParts = [byteRange 
componentsSeparatedByString:@"-"];
+            NSUInteger start = (NSUInteger)[rangeParts[0] integerValue];
+            NSUInteger end = rangeParts.count > 1 && ![rangeParts[1] 
isEqualToString:@""] ? (NSUInteger)[rangeParts[1] integerValue] : [fileLength 
unsignedIntegerValue] - 1;
+            range = NSMakeRange(start, end - start + 1);
+        }
 
-            if (range.location != NSNotFound) {
-                // Ensure range is valid
-                if (range.location >= [fileLength unsignedIntegerValue] && 
[self taskActive:urlSchemeTask]) {
+        if (range.location != NSNotFound) {
+            // Ensure range is valid
+            if (range.location >= [fileLength unsignedIntegerValue]) {
+                if ([self taskActive:urlSchemeTask]) {
                     headers[@"Content-Range"] = [NSString 
stringWithFormat:@"bytes */%@", fileLength];
                     NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] 
initWithURL:req.URL statusCode:416 HTTPVersion:@"HTTP/1.1" 
headerFields:headers];
                     [urlSchemeTask didReceiveResponse:response];
                     [urlSchemeTask didFinish];
-
-                    @synchronized(self.handlerMap) {
-                        [self.handlerMap removeObjectForKey:urlSchemeTask];
-                    }
-                    return;
                 }
 
-                [fileHandle seekToFileOffset:range.location];
-                responseSize = [NSNumber 
numberWithUnsignedInteger:range.length];
-                statusCode = 206; // Partial Content
-                headers[@"Content-Range"] = [NSString stringWithFormat:@"bytes 
%lu-%lu/%@", (unsigned long)range.location, (unsigned long)(range.location + 
range.length - 1), fileLength];
-                headers[@"Content-Length"] = [NSString 
stringWithFormat:@"%lu", (unsigned long)range.length];
+                [fileHandle closeFile];
+
+                @synchronized(self.handlerMap) {
+                    [self.handlerMap removeObjectForKey:urlSchemeTask];
+                }
+                return;
             }
-        }
 
-        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] 
initWithURL:req.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" 
headerFields:headers];
-        if ([self taskActive:urlSchemeTask]) {
-            [urlSchemeTask didReceiveResponse:response];
+            [fileHandle seekToFileOffset:range.location];
+            responseSize = [NSNumber numberWithUnsignedInteger:range.length];
+            statusCode = 206; // Partial Content
+            headers[@"Content-Range"] = [NSString stringWithFormat:@"bytes 
%lu-%lu/%@", (unsigned long)range.location, (unsigned long)(range.location + 
range.length - 1), fileLength];
+            headers[@"Content-Length"] = [NSString stringWithFormat:@"%lu", 
(unsigned long)range.length];
         }
+    }
 
-        while ([self taskActive:urlSchemeTask] && responseSent < [responseSize 
unsignedIntegerValue]) {
+    if (![self taskActive:urlSchemeTask]) {
+        [fileHandle closeFile];
+        return;
+    }
+
+    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] 
initWithURL:req.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" 
headerFields:headers];
+    [urlSchemeTask didReceiveResponse:response];
+
+    [self.viewController.commandDelegate runInBackground:^{
+        NSError *readError;
+        NSUInteger responseSent = 0;
+
+        // Read file data on the background thread and dispatch each chunk to 
the
+        // main thread. Dispatching to the main thread serialises all
+        // WKURLSchemeTask callbacks with webView:stopURLSchemeTask:, which is
+        // also called on the main thread. This eliminates the race condition
+        // between the taskActive check and the actual callback invocation.
+        while (responseSent < [responseSize unsignedIntegerValue]) {
             @autoreleasepool {
-                NSData *data = [self readFromFileHandle:fileHandle 
upTo:FILE_BUFFER_SIZE error:&error];
-                if (!data || error) {
-                    if ([self taskActive:urlSchemeTask]) {
-                        [urlSchemeTask didFailWithError:error];
-                    }
+                NSData *data = [self readFromFileHandle:fileHandle 
upTo:FILE_BUFFER_SIZE error:&readError];
+                if (!data || readError || data.length == 0) {
                     break;
                 }
-
-                if ([self taskActive:urlSchemeTask]) {
-                    [urlSchemeTask didReceiveData:data];
-                }
-
                 responseSent += data.length;
+
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    if ([self taskActive:urlSchemeTask]) {
+                        [urlSchemeTask didReceiveData:data];
+                    }
+                });
             }
         }
 
         [fileHandle closeFile];
+        NSError *capturedError = readError;
 
-        if ([self taskActive:urlSchemeTask]) {
-            [urlSchemeTask didFinish];
-        }
+        dispatch_async(dispatch_get_main_queue(), ^{
+            if (capturedError && [self taskActive:urlSchemeTask]) {
+                [urlSchemeTask didFailWithError:capturedError];
+            } else if ([self taskActive:urlSchemeTask]) {
+                [urlSchemeTask didFinish];
+            }
 
-        @synchronized(self.handlerMap) {
-            [self.handlerMap removeObjectForKey:urlSchemeTask];
-        }
+            @synchronized(self.handlerMap) {
+                [self.handlerMap removeObjectForKey:urlSchemeTask];
+            }
+        });
     }];
 }
 
@@ -183,10 +199,9 @@ static const NSUInteger FILE_BUFFER_SIZE = 1024 * 1024 * 
4; // 4 MiB
         [self.handlerMap removeObjectForKey:urlSchemeTask];
     }
 
-    if ([plugin isEqual:[NSNull null]]) {
-        // NSNull means we own this task, so we need to mark it as finished
-        [urlSchemeTask didFinish];
-    } else if ([plugin respondsToSelector:@selector(stopSchemeTask:)]) {
+    // After stopURLSchemeTask is invoked, WKURLSchemeTask must not receive
+    // additional callbacks from this handler.
+    if (![plugin isEqual:[NSNull null]] && [plugin 
respondsToSelector:@selector(stopSchemeTask:)]) {
         [plugin stopSchemeTask:urlSchemeTask];
     }
 }
diff --git a/tests/CordovaLibTests/CDVURLSchemeHandlerTest.m 
b/tests/CordovaLibTests/CDVURLSchemeHandlerTest.m
index 073b027a..b02eedc4 100644
--- a/tests/CordovaLibTests/CDVURLSchemeHandlerTest.m
+++ b/tests/CordovaLibTests/CDVURLSchemeHandlerTest.m
@@ -18,6 +18,8 @@
 */
 
 #import <XCTest/XCTest.h>
+#import <Cordova/CDVCommandDelegate.h>
+#import <Cordova/CDVSettingsDictionary.h>
 #import "CDVURLSchemeHandler.h"
 
 #import "CordovaApp-Swift.h"
@@ -26,6 +28,114 @@
 - (NSURL *)fileURLForRequestURL:(NSURL *)url;
 @end
 
+@interface CDVTestCommandDelegate : NSObject <CDVCommandDelegate>
+
+@property (nonatomic, strong) CDVSettingsDictionary* settings;
+@property (nonatomic, copy) void (^pendingBlock)(void);
+
+- (void)runPendingBlock;
+
+@end
+
+@implementation CDVTestCommandDelegate
+
+- (instancetype)init
+{
+    self = [super init];
+    if (self != nil) {
+        _settings = [[CDVSettingsDictionary alloc] init];
+    }
+    return self;
+}
+
+- (NSString *)pathForResource:(NSString *)resourcepath
+{
+    return resourcepath;
+}
+
+- (nullable CDVPlugin *)getCommandInstance:(NSString *)pluginName
+{
+    return nil;
+}
+
+- (void)sendPluginResult:(CDVPluginResult *)result callbackId:(NSString 
*)callbackId
+{
+}
+
+- (void)evalJs:(NSString *)js
+{
+}
+
+- (void)evalJs:(NSString *)js scheduledOnRunLoop:(BOOL)scheduledOnRunLoop
+{
+}
+
+- (void)runInBackground:(void (^)(void))block
+{
+    self.pendingBlock = block;
+}
+
+- (void)runPendingBlock
+{
+    void (^block)(void) = self.pendingBlock;
+    self.pendingBlock = nil;
+    if (block != nil) {
+        block();
+    }
+}
+
+@end
+
+@interface CDVTestURLSchemeTask : NSObject <WKURLSchemeTask>
+
+@property (nonatomic, strong) NSURLRequest* request;
+@property (nonatomic, strong) NSMutableArray<NSData *>* receivedData;
+@property (nonatomic, strong, nullable) NSURLResponse* response;
+@property (nonatomic, strong, nullable) NSError* error;
+@property (nonatomic) NSUInteger responseCount;
+@property (nonatomic) NSUInteger finishedCount;
+@property (nonatomic) NSUInteger failedCount;
+
+- (instancetype)initWithURL:(NSURL *)url;
+
+@end
+
+@implementation CDVTestURLSchemeTask
+
+- (instancetype)initWithURL:(NSURL *)url
+{
+    self = [super init];
+    if (self != nil) {
+        _request = [NSURLRequest requestWithURL:url];
+        _receivedData = [NSMutableArray array];
+    }
+    return self;
+}
+
+- (void)didReceiveResponse:(NSURLResponse *)response
+{
+    self.response = response;
+    self.responseCount += 1;
+}
+
+- (void)didReceiveData:(NSData *)data
+{
+    [self.receivedData addObject:data];
+}
+
+- (void)didFinish
+{
+    self.finishedCount += 1;
+}
+
+- (void)didFailWithError:(NSError *)error
+{
+    self.error = error;
+    self.failedCount += 1;
+}
+
+@end
+
 @interface CDVURLSchemeHandlerTest : XCTestCase
 
 @property AppDelegate* appDelegate;
@@ -68,4 +178,65 @@
     XCTAssertEqualObjects([result absoluteString], expected);
 }
 
+- (void)testStartURLSchemeTaskSendsResponseBeforeBackgroundWork
+{
+    CDVTestCommandDelegate *commandDelegate = [[CDVTestCommandDelegate alloc] 
init];
+    [self.viewController setValue:commandDelegate forKey:@"commandDelegate"];
+
+    CDVURLSchemeHandler *handler = [[CDVURLSchemeHandler alloc] 
initWithViewController:self.viewController];
+    CDVTestURLSchemeTask *task = [[CDVTestURLSchemeTask alloc] 
initWithURL:[NSURL URLWithString:@"app://localhost/index.html"]];
+
+    [handler webView:nil startURLSchemeTask:task];
+
+    XCTAssertEqual(task.responseCount, 1u);
+    XCTAssertNotNil(task.response);
+    XCTAssertEqual(task.failedCount, 0u);
+    XCTAssertEqual(task.finishedCount, 0u);
+    XCTAssertNotNil(commandDelegate.pendingBlock);
+}
+
+- (void)testStopURLSchemeTaskDoesNotFinishStoppedTask
+{
+    CDVTestCommandDelegate *commandDelegate = [[CDVTestCommandDelegate alloc] 
init];
+    [self.viewController setValue:commandDelegate forKey:@"commandDelegate"];
+
+    CDVURLSchemeHandler *handler = [[CDVURLSchemeHandler alloc] 
initWithViewController:self.viewController];
+    CDVTestURLSchemeTask *task = [[CDVTestURLSchemeTask alloc] 
initWithURL:[NSURL URLWithString:@"app://localhost/index.html"]];
+
+    [handler webView:nil startURLSchemeTask:task];
+    [handler webView:nil stopURLSchemeTask:task];
+
+    XCTAssertEqual(task.responseCount, 1u);
+    XCTAssertEqual(task.finishedCount, 0u);
+    XCTAssertEqual(task.failedCount, 0u);
+
+    [commandDelegate runPendingBlock];
+
+    // The background block dispatches its final callbacks onto the main queue.
+    // Drain the main queue so those callbacks have a chance to run before
+    // we assert, making the test verify the stopped task stays silent.
+    XCTestExpectation *drained = [self expectationWithDescription:@"main queue 
drained"];
+    dispatch_async(dispatch_get_main_queue(), ^{ [drained fulfill]; });
+    [self waitForExpectationsWithTimeout:1.0 handler:nil];
+
+    XCTAssertEqual(task.finishedCount, 0u);
+    XCTAssertEqual(task.failedCount, 0u);
+}
+
+- (void)testMissingFileFailsBeforeBackgroundWork
+{
+    CDVTestCommandDelegate *commandDelegate = [[CDVTestCommandDelegate alloc] 
init];
+    [self.viewController setValue:commandDelegate forKey:@"commandDelegate"];
+
+    CDVURLSchemeHandler *handler = [[CDVURLSchemeHandler alloc] 
initWithViewController:self.viewController];
+    CDVTestURLSchemeTask *task = [[CDVTestURLSchemeTask alloc] 
initWithURL:[NSURL URLWithString:@"app://localhost/does-not-exist.html"]];
+
+    [handler webView:nil startURLSchemeTask:task];
+
+    XCTAssertEqual(task.responseCount, 0u);
+    XCTAssertEqual(task.failedCount, 1u);
+    XCTAssertNotNil(task.error);
+    XCTAssertNil(commandDelegate.pendingBlock);
+}
+
 @end


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to