I was facing a similar problem (needed to connect to a local API with self-signed certificate) and after a ridiculous amount of research and experimentation I was finally able to find a solution. You will need to create a config plugin, which requires Expo SDK version 41+. Note that you will lose the ability to use Expo Go, but you will remain in the managed workflow (i.e. no need to change native code) and you can use EAS builds to build a custom dev client, which is basically a version of Expo Go tailored to your project.
Add the certificate to your device’s list of user certificates:
(This step is probably unnecessary if you include your (raw) certificate in the network config below, see this link.) Go to Settings -> Security -> Advanced -> Encryption & credentials -> Install a certificate
to import the certificate
Ensure that the certificate is actually the problem:
try {
const response = await axios.post(urlPath, payload);
} catch (error) {
console.error(error.request?._response);
}
You’ll know your app doesn’t trust the certificate if you get a network error and error.request._response
reads java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
Create the plugin:
You will now create a config plugin, which are basically JS functions which run during Expo’s prebuild
phase to modify native configuration such as the Android manifest before building the native project.
In your project root, create a plugins
folder with the following two files:
network_security_config.xml
:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
trust-local-certs.js
:
const {AndroidConfig, withAndroidManifest } = require('@expo/config-plugins');
const {Paths} = require('@expo/config-plugins/build/android');
const path = require('path');
const fs = require('fs');
const fsPromises = fs.promises;
const { getMainApplicationOrThrow} = AndroidConfig.Manifest
const withTrustLocalCerts = config => {
return withAndroidManifest(config, async config => {
config.modResults = await setCustomConfigAsync(config, config.modResults);
return config;
});
}
async function setCustomConfigAsync(
config,
androidManifest
) {
const src_file_pat = path.join(__dirname, "network_security_config.xml");
const res_file_path = path.join(await Paths.getResourceFolderAsync(config.modRequest.projectRoot),
"xml", "network_security_config.xml");
const res_dir = path.resolve(res_file_path, "..");
if (!fs.existsSync(res_dir)) {
await fsPromises.mkdir(res_dir);
}
try {
await fsPromises.copyFile(src_file_pat, res_file_path);
} catch (e) {
throw e;
}
const mainApplication = getMainApplicationOrThrow(androidManifest);
mainApplication.$["android:networkSecurityConfig"] = "@xml/network_security_config";
return androidManifest;
}
module.exports = withTrustLocalCerts;
Run expo prebuild
& link the plugin
In order to use the plugin, you have to a have a file called app.json
in the project root. I’m not 100% sure where I got the file from, but I believe it was created automatically when I first ran expo prebuild
. Note:
- I recommend upgrading the Expo SDK to the most current version (currently 44), because now the
prebuild
command (which apparently is the same aseject
) is now completely reversible (e.g. it doesn’t install most of the additional dependencies it used to). - Prebuild will create the native folders, but you can safely delete these folders once you are done setting up the plugin (You really should delete them, if you want to remain in the managed workflow! When you run an EAS build, EAS will assume the bare workflow if it sees the native folders, and presumably not run your plugin again).
- Run
expo prebuild
and follow the prompts -> This should create theapp.json
file. - Delete the generated native
android
folder (andios
, if it got created as well). - In
app.json
, add the following to the end of theexpo
key:"plugins": [ "./plugins/trust-local-certs.js" ]
- Run
expo prebuild --no-install
and check thatandroid/app/src/main/AndroidManifest.xml
contains a reference to your network config, and thatandroid/app/src/main/res/xml/network_security_config.xml
was correctly copied over from yourplugins
directory. - If all is well, your plugin is set up correctly and you should again delete the native folders (see note above).
Setup and run EAS build:
If you haven’t done so already, set up your project for EAS builds by following
these instructions (install EAS CLI, run eas build:configure
and configure a dev profile in eas.json
). Then, run eas build --profile development --platform android
. This will create a custom dev client in the cloud (running the plugin during the prebuild
phase), which you can install on your device and acts as a replacement for Expo Go. To start the metro server, run expo start --dev-client
. Your dev client should then be able to pick up the connection to the Metro server and if you run the axios request again, it should go through 🙂