Skip to content

Commit 5d41542

Browse files
bparrishMinesEgor
authored andcommitted
[webview_flutter] Add onReceivedError callback (flutter#2639)
1 parent 29dc54e commit 5d41542

File tree

8 files changed

+331
-10
lines changed

8 files changed

+331
-10
lines changed

packages/webview_flutter/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.20
2+
3+
* Added support for receiving web resource loading errors. See `WebView.onWebResourceError`.
4+
15
## 0.3.19+10
26

37
* Replace deprecated `getFlutterEngine` call on Android.

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44

55
package io.flutter.plugins.webviewflutter;
66

7+
import android.annotation.SuppressLint;
78
import android.annotation.TargetApi;
89
import android.graphics.Bitmap;
910
import android.os.Build;
1011
import android.util.Log;
1112
import android.view.KeyEvent;
13+
import android.webkit.WebResourceError;
1214
import android.webkit.WebResourceRequest;
1315
import android.webkit.WebView;
1416
import android.webkit.WebViewClient;
17+
import androidx.webkit.WebResourceErrorCompat;
1518
import androidx.webkit.WebViewClientCompat;
1619
import io.flutter.plugin.common.MethodChannel;
1720
import java.util.HashMap;
21+
import java.util.Locale;
1822
import java.util.Map;
1923

2024
// We need to use WebViewClientCompat to get
@@ -30,6 +34,47 @@ class FlutterWebViewClient {
3034
this.methodChannel = methodChannel;
3135
}
3236

37+
private static String errorCodeToString(int errorCode) {
38+
switch (errorCode) {
39+
case WebViewClient.ERROR_AUTHENTICATION:
40+
return "authentication";
41+
case WebViewClient.ERROR_BAD_URL:
42+
return "badUrl";
43+
case WebViewClient.ERROR_CONNECT:
44+
return "connect";
45+
case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE:
46+
return "failedSslHandshake";
47+
case WebViewClient.ERROR_FILE:
48+
return "file";
49+
case WebViewClient.ERROR_FILE_NOT_FOUND:
50+
return "fileNotFound";
51+
case WebViewClient.ERROR_HOST_LOOKUP:
52+
return "hostLookup";
53+
case WebViewClient.ERROR_IO:
54+
return "io";
55+
case WebViewClient.ERROR_PROXY_AUTHENTICATION:
56+
return "proxyAuthentication";
57+
case WebViewClient.ERROR_REDIRECT_LOOP:
58+
return "redirectLoop";
59+
case WebViewClient.ERROR_TIMEOUT:
60+
return "timeout";
61+
case WebViewClient.ERROR_TOO_MANY_REQUESTS:
62+
return "tooManyRequests";
63+
case WebViewClient.ERROR_UNKNOWN:
64+
return "unknown";
65+
case WebViewClient.ERROR_UNSAFE_RESOURCE:
66+
return "unsafeResource";
67+
case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME:
68+
return "unsupportedAuthScheme";
69+
case WebViewClient.ERROR_UNSUPPORTED_SCHEME:
70+
return "unsupportedScheme";
71+
}
72+
73+
final String message =
74+
String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode);
75+
throw new IllegalArgumentException(message);
76+
}
77+
3378
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
3479
private boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
3580
if (!hasNavigationDelegate) {
@@ -79,6 +124,14 @@ private void onPageFinished(WebView view, String url) {
79124
methodChannel.invokeMethod("onPageFinished", args);
80125
}
81126

127+
private void onWebResourceError(final int errorCode, final String description) {
128+
final Map<String, Object> args = new HashMap<>();
129+
args.put("errorCode", errorCode);
130+
args.put("description", description);
131+
args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode));
132+
methodChannel.invokeMethod("onWebResourceError", args);
133+
}
134+
82135
private void notifyOnNavigationRequest(
83136
String url, Map<String, String> headers, WebView webview, boolean isMainFrame) {
84137
HashMap<String, Object> args = new HashMap<>();
@@ -123,6 +176,20 @@ public void onPageFinished(WebView view, String url) {
123176
FlutterWebViewClient.this.onPageFinished(view, url);
124177
}
125178

179+
@TargetApi(Build.VERSION_CODES.M)
180+
@Override
181+
public void onReceivedError(
182+
WebView view, WebResourceRequest request, WebResourceError error) {
183+
FlutterWebViewClient.this.onWebResourceError(
184+
error.getErrorCode(), error.getDescription().toString());
185+
}
186+
187+
@Override
188+
public void onReceivedError(
189+
WebView view, int errorCode, String description, String failingUrl) {
190+
FlutterWebViewClient.this.onWebResourceError(errorCode, description);
191+
}
192+
126193
@Override
127194
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
128195
// Deliberately empty. Occasionally the webview will mark events as having failed to be
@@ -154,6 +221,22 @@ public void onPageFinished(WebView view, String url) {
154221
FlutterWebViewClient.this.onPageFinished(view, url);
155222
}
156223

224+
// This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is
225+
// enabled. The deprecated method is called when a device doesn't support this.
226+
@SuppressLint("RequiresFeature")
227+
@Override
228+
public void onReceivedError(
229+
WebView view, WebResourceRequest request, WebResourceErrorCompat error) {
230+
FlutterWebViewClient.this.onWebResourceError(
231+
error.getErrorCode(), error.getDescription().toString());
232+
}
233+
234+
@Override
235+
public void onReceivedError(
236+
WebView view, int errorCode, String description, String failingUrl) {
237+
FlutterWebViewClient.this.onWebResourceError(errorCode, description);
238+
}
239+
157240
@Override
158241
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
159242
// Deliberately empty. Occasionally the webview will mark events as having failed to be

packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
import 'dart:async';
66
import 'dart:convert';
7+
import 'dart:io';
78
import 'dart:typed_data';
89

910
import 'package:flutter/foundation.dart';
1011
import 'package:flutter/services.dart';
1112
import 'package:flutter/widgets.dart';
1213
import 'package:flutter_test/flutter_test.dart';
14+
import 'package:webview_flutter/platform_interface.dart';
1315
import 'package:webview_flutter/webview_flutter.dart';
1416
import 'package:e2e/e2e.dart';
1517

@@ -576,6 +578,52 @@ void main() {
576578
expect(currentUrl, 'https://www.google.com/');
577579
});
578580

581+
testWidgets('onWebResourceError', (WidgetTester tester) async {
582+
final Completer<WebResourceError> errorCompleter =
583+
Completer<WebResourceError>();
584+
585+
await tester.pumpWidget(
586+
Directionality(
587+
textDirection: TextDirection.ltr,
588+
child: WebView(
589+
key: GlobalKey(),
590+
initialUrl: 'https://www.notawebsite..com',
591+
onWebResourceError: (WebResourceError error) {
592+
errorCompleter.complete(error);
593+
},
594+
),
595+
),
596+
);
597+
598+
final WebResourceError error = await errorCompleter.future;
599+
expect(error, isNotNull);
600+
601+
if (Platform.isIOS) expect(error.domain, isNotNull);
602+
if (Platform.isAndroid) expect(error.errorType, isNotNull);
603+
});
604+
605+
testWidgets('onWebResourceError is not called with valid url',
606+
(WidgetTester tester) async {
607+
final Completer<WebResourceError> errorCompleter =
608+
Completer<WebResourceError>();
609+
610+
await tester.pumpWidget(
611+
Directionality(
612+
textDirection: TextDirection.ltr,
613+
child: WebView(
614+
key: GlobalKey(),
615+
initialUrl:
616+
'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
617+
onWebResourceError: (WebResourceError error) {
618+
errorCompleter.complete(error);
619+
},
620+
),
621+
),
622+
);
623+
624+
expect(errorCompleter.future, doesNotComplete);
625+
});
626+
579627
testWidgets('can block requests', (WidgetTester tester) async {
580628
final Completer<WebViewController> controllerCompleter =
581629
Completer<WebViewController>();

packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
#import "FLTWKNavigationDelegate.h"
66

77
@implementation FLTWKNavigationDelegate {
8-
FlutterMethodChannel* _methodChannel;
8+
FlutterMethodChannel *_methodChannel;
99
}
1010

11-
- (instancetype)initWithChannel:(FlutterMethodChannel*)channel {
11+
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
1212
self = [super init];
1313
if (self) {
1414
_methodChannel = channel;
@@ -18,18 +18,18 @@ - (instancetype)initWithChannel:(FlutterMethodChannel*)channel {
1818

1919
#pragma mark - WKNavigationDelegate conformance
2020

21-
- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation {
21+
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
2222
[_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}];
2323
}
2424

25-
- (void)webView:(WKWebView*)webView
26-
decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction
25+
- (void)webView:(WKWebView *)webView
26+
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
2727
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
2828
if (!self.hasDartNavigationDelegate) {
2929
decisionHandler(WKNavigationActionPolicyAllow);
3030
return;
3131
}
32-
NSDictionary* arguments = @{
32+
NSDictionary *arguments = @{
3333
@"url" : navigationAction.request.URL.absoluteString,
3434
@"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame)
3535
};
@@ -56,13 +56,52 @@ - (void)webView:(WKWebView*)webView
5656
decisionHandler(WKNavigationActionPolicyAllow);
5757
return;
5858
}
59-
NSNumber* typedResult = result;
59+
NSNumber *typedResult = result;
6060
decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow
6161
: WKNavigationActionPolicyCancel);
6262
}];
6363
}
6464

65-
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation {
65+
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
6666
[_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}];
6767
}
68+
69+
+ (id)errorCodeToString:(NSUInteger)code {
70+
switch (code) {
71+
case WKErrorUnknown:
72+
return @"unknown";
73+
case WKErrorWebContentProcessTerminated:
74+
return @"webContentProcessTerminated";
75+
case WKErrorWebViewInvalidated:
76+
return @"webViewInvalidated";
77+
case WKErrorJavaScriptExceptionOccurred:
78+
return @"javaScriptExceptionOccurred";
79+
case WKErrorJavaScriptResultTypeIsUnsupported:
80+
return @"javaScriptResultTypeIsUnsupported";
81+
}
82+
83+
return [NSNull null];
84+
}
85+
86+
- (void)onWebResourceError:(NSError *)error {
87+
[_methodChannel invokeMethod:@"onWebResourceError"
88+
arguments:@{
89+
@"errorCode" : @(error.code),
90+
@"domain" : error.domain,
91+
@"description" : error.description,
92+
@"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code],
93+
}];
94+
}
95+
96+
- (void)webView:(WKWebView *)webView
97+
didFailNavigation:(WKNavigation *)navigation
98+
withError:(NSError *)error {
99+
[self onWebResourceError:error];
100+
}
101+
102+
- (void)webView:(WKWebView *)webView
103+
didFailProvisionalNavigation:(WKNavigation *)navigation
104+
withError:(NSError *)error {
105+
[self onWebResourceError:error];
106+
}
68107
@end

0 commit comments

Comments
 (0)