androidflutterwebview-flutter

How to send a custom header in a POST request in Android using webview_flutter


This documentation states that it cannot be done, so I'm looking for an alternative solution:

Currently, setting custom headers when making a post request with the WebViewController's loadRequest method is not supported on Android. If you require this functionality, a workaround is to make the request manually, and then load the response data using loadHtmlString instead.

The workaround they provide comes with its own issues. The loadHtmlString method only displays an unstyled, unresponsive page in the webview element with the following console errors:

I/chromium(19539): [INFO:CONSOLE(286)] "Uncaught ReferenceError: $ is not defined", source: about:blank (286)
I/chromium(19539): [INFO:CONSOLE(636)] "Uncaught ReferenceError: jQuery is not defined", source: about:blank (636)

Here is my implementation of the loadHtmlString version. I've tried loading jQuery to solve the unresponsive issue, but I have no messages related to the ignored stylesheets:

class _PortalWebviewState extends State<PortalWebview> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted);
    _loadRequestWithAuthHeader();
    controller.addJavaScriptChannel(
      'Flutter',
      onMessageReceived: (JavaScriptMessage message) {
        print('JavaScript Error: ${message.message}');
     },
   );
  }

  Future<void> injectJQuery() async {
  await controller.runJavaScript('''
    var script = document.createElement('script');
    script.src = 'https://code.jquery.com/jquery-3.6.0.min.js';
    script.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(script);
    ''');
  }

  Future<void> _loadRequestWithAuthHeader() async {
    const storage = FlutterSecureStorage();
    final token = await storage.read(key: 'token');
    if (token != null) {
      print("Token found in keychain.");
      final url = Uri.parse('${Config.domain}/mobile_index.php');
      final headers = {
        'Authorization': 'Bearer $token',
        'Content-Type': 'application/json',
      };

      try {
        final response = await http.post(url, headers: headers);
        print('Portal response status code: ${response.statusCode}');

        if (response.statusCode == 200) {
          await injectJQuery();
          await controller.loadHtmlString(response.body);
        } else {
          print('Error: ${response.statusCode}');
          // Handle error cases here
        }
      } catch (e) {
        print('Error: $e');
        // Handle exceptions here
      }
    } else {
      print('Token not found');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: true,
      appBar: AppBar(
        title: const Text('COMPANY NAME'),
        actions: [
          NavigationControls(controller: controller),
          // Menu(controller: controller),
        ],
      ),
      body: PortalWebviewStack(controller: controller),
    );
  }

It returns unstyled HTML with broken links that load blank pages - my server logs don't show that any requests are received after clicking links in the webview.

I believe that sending the auth header as part of a GET request might be an easier solution, but I like the security of POST and believe that this workaround should be possible in my use case since it's referenced in the docs above and in this Github issue.


Solution

  • I decided to use LoadRequestMethod.get to skip the Android custom POST header limitation:

    class _InvoiceWebviewState extends State<InvoiceWebview> {
      late final WebViewController controller;
    
      @override
      void initState() {
        super.initState();
        controller = WebViewController();
        _loadRequestWithAuthHeader();
      }
    
      Future<void> _loadRequestWithAuthHeader() async {
        const storage = FlutterSecureStorage();
        final token = await storage.read(key: 'token');
    
        if (token != null) {
          final headers = {'Authorization': 'Bearer $token'};
    
          await controller.loadRequest(
            Uri.parse('${Config.domain}/invoices/new'),
            method: LoadRequestMethod.get,
            headers: headers,
          );
        } else {
          print('Token not found');
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          resizeToAvoidBottomInset: true,
          appBar: AppBar(
            title: const Text('COMPANY NAME'),
            actions: [
              NavigationControls(controller: controller),
              // Menu(controller: controller),
            ],
          ),
          body: InvoiceWebviewStack(controller: controller),
        );
      }
    }
    

    I gave up on the custom POST header since using loadHtmlString would require reformatting my CSS setup and solving the jQuery problem. Instead, I'm sending the authorization header in a GET request. Headers aren't visible in the query string, I still get the benefit of https encryption, and, most importantly, it works in iOS and Android.