Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter web integration test CORS XMLHttpRequest error

I'm running a Flutter integration test locally for web. This is an example integration test where the only thing I'm trying to do is press a button, ping https://google.com, and then complete after receiving a response.

When I run this integration test locally, I receive an XMLHttpRequest error. This is probably a CORS error, though I'm not sure that's the case.

How can I issue HTTP requests for websites I don't own from within an integration test?

Flutter version:

Flutter 2.2.0 • channel stable • https://github.com/flutter/flutter.git
Framework • revision b22742018b (12 days ago) • 2021-05-14 19:12:57 -0700
Engine • revision a9d88a4d18
Tools • Dart 2.13.0

Error:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞═════════════════
The following ClientException was thrown running a test:
XMLHttpRequest error.

When the exception was thrown, this was the stack:
dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 909:28 get current
...
like image 540
SuperDeclarative Avatar asked May 26 '21 21:05

SuperDeclarative


Video Answer


1 Answers

Why does this happen?

As you have stated, you are trying to ping, i.e. make an HTTP request, to https://www.google.com in a web integration tests.

When running anything on web, web security applies. In the web security model, security is enforced by browser engines using the Same-origin policy, which essentially involves browser engines blocking access by frontend JavaScript to responses from cross-origin requests. But that blocking can be overcome by using Cross-Origin Resource Sharing (CORS), which is a way for servers to tell browsers that they explicitly permit cross-origin access.
This is usually done using the Access-Control-Allow-Origin header.

Since your integration tests will always run on localhost, any external HTTP request will be cross-origin.

CORS example

Let us check this for https://www.google.com, which is your example:

google.com GET request

As you can see, you cannot see it. Right, there is no Access-Control-Allow-Origin response header sent from google.com.

→ this means according to web security, you are not allowed to make this request from a different domain (cross-origin).

Does it have anything to do with web integration testing?

Now, the question might come up "why does the same integration test work on mobile".
Yes, that is a good question and the answer is trivial. CORS policies only exist on web, i.e. they actually only exist inside of browsers. This is because anyone can inject any code on web (essentially). On mobile, however, requests are safe, which is why you can make whatever requests you want - on web, you cannot.


The Flutter web integration tests will run inside of a Chrome instance and this makes sense. The point of an integration test is to emulate real behavior, i.e. seeing if components work together and in the case of Flutter's integration_testing (which is closer to automated e2e testing in some way), this means testing if the app will successfully work on the web platform.

→ think about it, it also makes most sense to integration test in this way because the same request will also fail in the real web app.

Does it have anything to do with Flutter?

No, absolutely not! In fact, there is no framework, no way to avoid this because it is intended. The web platform in general has these security policies in place and you will have to deal with them.

This means that no web app can make an HTTP request (as of May 27, 2021) to https://www.google.com unless it is actually running on the same domain.


We can quickly visualize this using Chrome's JS console and ping https://www.google.com from the console, once while being on a random website and once actually being on Chrome.

Pinging google.com

As you can see, the request even fails in the JS console. You might think the console has privileged rights, but even there the request fails unless you are on the same origin (left side).
Note that a different policy / header is named in the error message. This will differ based on the browser used and does not matter for the argument / unterstanding.

Solutions

Now that we have established that this is actually expected behavior, how do we go about this correctly, how do we make it work?

Using the right resources

Your test is failing because it should fail. On web, you cannot access the domain you want. But what if you used one that actually allowed it instead?

Of course, there are many sites that actually do allow CORS, i.e. do allow to be accessed from an external source. Here you can see the response headers for https://i.imgur.com/MQdD3lg.png:

i.imgur request

Imgur serves images for sharing and therefore they logically want to allow these images to be embedded in any website or web app. This is why you can see:

access-control-allow-origin: *

It means that the image can be requested from anywhere, from any domain.

→ I propose you use this to ping in your integration test instead ;)

This is not a bug

To emphasize that this is 100% expected behavior, we need to answer the question of how this problem is usually solved.

Well, if you are hosting resources, you will be the one setting the response headers. And what is done for debugging and testing purposes is specifying localhost ports that are allowed to access.

Adding CORS headers

E.g. if you want to run your local debugging and tests, you will want to specify a port to run on. In Flutter, this is done via the --web-port argument. You can run your tests on localhost:4200 using --web-port 4200.
Now, you need to add this localhost port to your allowed origins in the response header. An example of how to do this can be found in this Google Cloud article.

Disabling web security in Chrome

What I do not recommend doing (because it will not resemble the real scenario that your integration test should cover) is a way to disable web security altogether. If you do this, you can ignore all web security policies and run any requests you want.

As the Flutter web integration tests are run on Chrome, you can use --disable-web-security on the Chrome device to disable web security.

like image 176
creativecreatorormaybenot Avatar answered Sep 30 '22 11:09

creativecreatorormaybenot