How to Set Proxy on Android With a Username and Password
How to set proxy on Android when Wi-Fi settings have no password field: whitelist your IP, run a local auth forwarder, or use adb's global proxy, then verify.
How to set proxy on Android sounds like a two-minute job until you open the Wi-Fi proxy screen and find a hostname box, a port box, a bypass list, and nowhere to type the username and password your proxy provider gave you. That's not a hidden menu or a vendor skin hiding the field. Android's proxy model has no slot for credentials at all. This guide covers the two ways around it (whitelist the network's public IP, or run a small forwarder that adds the credentials for you), then the adb global proxy that QA testers use, the apps that ignore the setting entirely, and how to prove which IP your traffic actually leaves from.
Why the Wi-Fi Proxy Screen Has No Password Field
On stock Android, the path is Settings > Network & internet > Internet, tap the network, tap the edit (pencil) icon, open Advanced options, then set Proxy to Manual. Zebra's Android 11 and 13 reference guide lists the fields you get: "Proxy hostname", "Proxy port" and "Bypass proxy for". Samsung and other skins move the menus around, but the fields are the same, because they all write the same platform object.
That object is android.net.ProxyInfo. Its public API (reference page last updated 2026-08-03) has buildDirectProxy(host, port, exclList), buildPacProxy(pacUrl), getHost(), getPort(), getExclusionList() and getPacFileUrl(). There is no username, password or credential method anywhere on the class. The PAC option doesn't help either: a PAC script only chooses a proxy, it can't answer a login challenge.
So when you point the Wi-Fi proxy at a gateway that wants user:pass, every request gets 407 Proxy Authentication Required, and most apps just fail. Typing user:pass@gateway.sparkproxy.io into the hostname box doesn't sneak the credentials in. The hostname validator in AOSP's ProxyUtils accepts only letters, digits, dots and hyphens, so the @, : and // are rejected and Settings shows its own error string: "The hostname you typed isn't valid."
Android's own UI also warns you about a second limit, which matters later in this guide. The AOSP Settings string proxy_warning_limited_support reads: "The HTTP proxy is used by the browser but may not be used by the other apps."
Choose a Route Before You Touch Settings
There are three ways to get a phone onto an authenticated proxy. Pick one based on whose network the phone is on and whether you have a USB cable and a laptop.
| Route | Credentials live | Works off your home/office Wi-Fi? | Catches apps that ignore the proxy? | Best for |
|---|---|---|---|---|
| IP whitelist, phone points at the gateway | Nowhere on the device | No, only on networks whose public IP you whitelisted | No | A personal phone on a fixed home or office line |
| Forwarder (on a laptop or in Termux) | On the machine running the forwarder | Yes, if the forwarder runs on the phone | No | Coffee-shop Wi-Fi, dynamic IPs, shared networks |
| adb global proxy + forwarder on the laptop | On the laptop only | Applies to every network the phone joins | No | QA benches, test devices, CI rigs |
| Emulator `-http-proxy` flag | In the emulator launch command | n/a | Yes, for TCP | Emulator-only test runs |
The last row is the outlier. Google's emulator proxy documentation (updated 2026-03-12) says the flag accepts http:// and "routes all of the emulated device's TCP traffic through the specified proxy." That's why an emulator run can pass while the same app on a physical phone leaks around the proxy. Our post on proxies for mobile app testing teams covers where the emulator fits in a QA setup; this guide is about real devices.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Route 1: Whitelist the Network's Public IP
If the phone sits on one Wi-Fi network you control, the simplest fix is to make the gateway stop asking for a password. SparkProxy supports both user:pass and IP whitelist authentication. Once your network's public IP is on the whitelist, the gateway accepts connections from it with no credentials, and Android's hostname-and-port screen is all you need.
- On the phone, with no proxy set, open
https://api.ipify.orgin the browser. Write down the address. That's your router's public IP, shared by every device on that Wi-Fi. - Add that IP to the whitelist in your SparkProxy dashboard.
- Back in the Wi-Fi proxy screen, set Proxy hostname to
gateway.sparkproxy.ioand Proxy port to11000for Random Rotation, or11002for Sticky Sessions, where the exit IP holds for 5 minutes. - Put local addresses in Bypass proxy for, comma-separated with no spaces, for example
localhost,192.168.1.1,intranet.sparkproxy.io. - Save, then run the checks in the verification section below.
Three catches come with this route. The Wi-Fi proxy is stored per network, so the moment the phone drops to mobile data or joins another Wi-Fi, it connects directly with no warning. Many home connections get a new public IP after a router restart, which turns into sudden 407s until you update the whitelist. And anyone else on that Wi-Fi can now use your proxy plan, because the gateway can't tell your phone from the smart TV behind the same NAT. Our explainer on IP whitelisting for proxies goes deeper on the NAT and dynamic-IP trade-offs.
For app testing, prefer port 11002. An app opens many parallel connections while it starts up, and on Random Rotation each one can leave from a different IP. A backend that ties a login session to an address may treat that as suspicious and force a logout.
Route 2: Run a Forwarder That Adds Credentials
When whitelisting isn't practical, put a tiny proxy in front of the real one. The phone talks to the forwarder with no authentication. The forwarder adds a Proxy-Authorization header and relays everything to gateway.sparkproxy.io. The credentials never touch Android's settings.
Below is a complete forwarder using only the Python standard library. It handles both HTTPS (CONNECT tunnels, which is nearly all app traffic) and plain HTTP. It doesn't decrypt anything: TLS passes straight through, so you don't need to install a certificate on the phone.
"""authfwd.py: a local HTTP proxy that adds SparkProxy credentials upstream.
Android's Wi-Fi proxy has no username/password field. Point it at this
forwarder instead; the forwarder adds Proxy-Authorization and relays to the
SparkProxy gateway. Standard library only, Python 3.8+.
"""
import asyncio
import base64
import os
LISTEN_HOST = os.environ.get("LISTEN_HOST", "127.0.0.1")
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8888"))
UPSTREAM_HOST = os.environ.get("UPSTREAM_HOST", "gateway.sparkproxy.io")
UPSTREAM_PORT = int(os.environ.get("UPSTREAM_PORT", "11000"))
TOKEN = base64.b64encode(
f"{os.environ['SPARK_USER']}:{os.environ['SPARK_PASS']}".encode()
).decode()
DROP = (b"proxy-authorization:", b"proxy-connection:", b"connection:")
async def pipe(reader, writer):
try:
while data := await reader.read(65536):
writer.write(data)
await writer.drain()
except (ConnectionError, asyncio.CancelledError):
pass
finally:
writer.close()
async def handle(client_r, client_w):
try:
head = await asyncio.wait_for(client_r.readuntil(b"\r\n\r\n"), 30)
except (asyncio.IncompleteReadError, asyncio.LimitOverrunError,
asyncio.TimeoutError, ConnectionError):
client_w.close()
return
lines = head.split(b"\r\n")
request_line = lines[0]
is_connect = request_line.upper().startswith(b"CONNECT ")
kept = [ln for ln in lines[1:] if ln and not ln.lower().startswith(DROP)]
extra = [b"Proxy-Authorization: Basic " + TOKEN.encode()]
if not is_connect:
# One request per upstream connection, so every request carries auth.
extra.append(b"Connection: close")
new_head = b"\r\n".join([request_line] + kept + extra) + b"\r\n\r\n"
try:
up_r, up_w = await asyncio.open_connection(UPSTREAM_HOST, UPSTREAM_PORT)
except OSError as exc:
client_w.write(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
await client_w.drain()
client_w.close()
print("upstream unreachable:", exc)
return
up_w.write(new_head)
await up_w.drain()
print(request_line.decode(errors="replace"))
await asyncio.gather(pipe(client_r, up_w), pipe(up_r, client_w))
async def main():
server = await asyncio.start_server(handle, LISTEN_HOST, LISTEN_PORT)
print(f"listening on {LISTEN_HOST}:{LISTEN_PORT} -> "
f"{UPSTREAM_HOST}:{UPSTREAM_PORT}")
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
We tested it on 2026-10-30 with Python 3.14.2 against a stand-in upstream that demands Basic auth. Through the forwarder, curl -x http://127.0.0.1:8888 https://api.ipify.org completed its CONNECT tunnel and a plain-HTTP GET returned 200, with the upstream logging a valid Proxy-Authorization header both times. The same request sent straight to the upstream without the forwarder got 407. We didn't run it against the live SparkProxy gateway with a customer account, so treat the gateway leg as the documented configuration rather than a measurement.
To serve a phone on the same Wi-Fi from a laptop, bind to the LAN interface:
export SPARK_USER='your-username' SPARK_PASS='your-password'
LISTEN_HOST=0.0.0.0 LISTEN_PORT=8888 UPSTREAM_PORT=11002 python3 authfwd.py
# Phone: Proxy hostname = the laptop's LAN IP (e.g. 192.168.1.50), Proxy port = 8888
Binding to 0.0.0.0 makes the forwarder an open, pre-authenticated proxy for everyone on that network. On a shared Wi-Fi, firewall port 8888 to the phone's address only, or use the USB setup in the adb section, which never exposes the port to the LAN.
One more property of this design is handy: the forwarder prints every request line. If you open an app and its hostnames never appear in that log, the app isn't using the proxy. That's the fastest bypass test there is, and we come back to it below.
Running the Forwarder on the Phone With Termux
A laptop-side forwarder only helps while the phone is on the laptop's network. To take the setup anywhere, run the same script on the phone and point the Wi-Fi proxy at 127.0.0.1.
Termux is the usual way to get Python on an unrooted Android device. Its GitHub repository showed the latest stable release as v0.118.3, published 2025-05-22, with the most recent commit on 2026-09-26 when we checked on 2026-10-30. The project README recommends installing from F-Droid or GitHub Releases and describes the Google Play build as an experimental branch with "missing functionality and bugs".
# Inside Termux
pkg update && pkg install python
mkdir -p ~/proxy && cd ~/proxy
# copy authfwd.py here (termux-setup-storage gives access to Downloads)
export SPARK_USER='your-username' SPARK_PASS='your-password'
UPSTREAM_PORT=11002 python authfwd.py
# Wi-Fi proxy on the same phone: hostname 127.0.0.1, port 8888
The default LISTEN_HOST=127.0.0.1 matters here. It keeps the forwarder reachable only from the phone itself, so nobody else on the caf茅 Wi-Fi can ride on your credentials.
You'll still have to set the Wi-Fi proxy to 127.0.0.1:8888 on each network you join, because Android stores it per network. And Android will kill a background process it thinks is idle. The Termux README tells users to make sure battery optimisations are disabled for the app. If pages stop loading after the screen has been off for a while, the forwarder is usually the thing that died.
For Testers: the adb Global Proxy
QA benches don't want per-network taps. Android has a network-independent global proxy, and adb can set it without root:
adb shell settings put global http_proxy 127.0.0.1:8888
adb shell settings get global http_proxy
The value is the Settings.Global.HTTP_PROXY constant, documented as "Host name and port for global http proxy. Uses ':' seperator for between host and port." The DevicePolicyManager.setRecommendedGlobalProxy() reference describes the same global proxy as "network-independent," and adds a line every tester should remember: "This proxy is only a recommendation and it is possible that some apps will ignore it."
In the current AOSP Connectivity source (ConnectivityService.getProxyForNetwork(), read on 2026-10-30), a global proxy is returned before any per-network proxy is consulted. So once it's set, it beats whatever the Wi-Fi screen says and also applies on mobile data.
Pair it with adb reverse and the credentials never leave your laptop. adb reverse tcp:8888 tcp:8888 makes port 8888 on the device connect to port 8888 on the host over USB, so the device-side 127.0.0.1:8888 reaches the forwarder running on the laptop:
# Laptop: forwarder on loopback only
export SPARK_USER='your-username' SPARK_PASS='your-password'
UPSTREAM_PORT=11002 python3 authfwd.py &
# Device: tunnel its 127.0.0.1:8888 to the laptop, then set the global proxy
adb reverse tcp:8888 tcp:8888
adb shell settings put global http_proxy 127.0.0.1:8888
Watch the format. AOSP's ProxyTracker.loadDeprecatedGlobalHttpProxy() splits the string on : and parses the second piece as the port. The behaviour that follows from that code:
| Value you put | What the platform does |
|---|---|
| `127.0.0.1:8888` | Global proxy set to 127.0.0.1, port 8888 |
| `gateway.sparkproxy.io` | Port defaults to 8080, which is wrong for SparkProxy |
| `http://127.0.0.1:8888` | Port parse fails, setting silently ignored |
| `user:pass@gateway.sparkproxy.io:11000` | Port parse fails, silently ignored |
| `:0` | Empty host, global proxy cleared |
No error is printed in the ignored cases. settings put writes the string happily, and the device carries on without a proxy. The android_proxy.py helper further down refuses those formats before they reach the device.
If you'd rather not run a forwarder on a test bench at all, whitelist the bench's public IP and set http_proxy straight to gateway.sparkproxy.io:11002.
Clearing the adb Proxy Without Leaving It Stuck
This is the step that catches people. The obvious undo, adb shell settings delete global http_proxy, removes the key you set, but in the AOSP code a missing http_proxy makes loadDeprecatedGlobalHttpProxy() return early without touching the active proxy. The values it copied into the hidden global_http_proxy_host and global_http_proxy_port keys stay put, and loadGlobalProxy() reads those keys back at boot. A device can stay proxied after you "deleted" the setting and rebooted, which shows up as a test phone that has no internet the day after the bench laptop goes home.
Putting :0 works because it parses to an empty host, and ProxyTracker.setGlobalProxy() treats an empty host as "clear": it writes an empty host and port 0 into those keys. Do that first, then delete:
adb shell settings put global http_proxy :0
adb shell settings delete global http_proxy
adb shell settings get global global_http_proxy_host # expect an empty line
adb reverse --remove tcp:8888
For a bench with several devices, wrap the set, show and clear steps so nobody hand-types them:
"""android_proxy.py: set, inspect and clear the Android global proxy over adb.
Usage:
python android_proxy.py set 127.0.0.1:8888 # host:port only, no scheme
python android_proxy.py show
python android_proxy.py clear
python android_proxy.py check # opens an IP echo page on the device
"""
import re
import subprocess
import sys
KEYS = ("http_proxy", "global_http_proxy_host", "global_http_proxy_port",
"global_http_proxy_exclusion_list")
def adb(*args):
out = subprocess.run(["adb", *args], capture_output=True, text=True, check=True)
return out.stdout.strip()
def show():
for key in KEYS:
print(f"{key:34} {adb('shell', 'settings', 'get', 'global', key)}")
def set_proxy(value):
# The platform splits on ':' and silently ignores anything it can't parse,
# so reject schemes and credentials here instead of failing quietly.
if not re.fullmatch(r"[A-Za-z0-9.\-]+:\d{1,5}", value):
sys.exit("expected host:port, e.g. 127.0.0.1:8888 (no http://, no user:pass)")
adb("shell", "settings", "put", "global", "http_proxy", value)
show()
def clear():
# ':0' parses to an empty host, which makes the platform wipe the
# global_http_proxy_* keys. Deleting http_proxy alone leaves them set.
adb("shell", "settings", "put", "global", "http_proxy", ":0")
adb("shell", "settings", "delete", "global", "http_proxy")
show()
def check():
adb("shell", "am", "start", "-a", "android.intent.action.VIEW",
"-d", "https://api.ipify.org")
print("Read the IP on the device screen and compare it with your direct IP.")
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "show"
if cmd == "set" and len(sys.argv) == 3:
set_proxy(sys.argv[2])
elif cmd in ("show", "clear", "check"):
{"show": show, "clear": clear, "check": check}[cmd]()
else:
sys.exit(__doc__)
Set ANDROID_SERIAL to target one device when several are attached. We compiled this helper but had no device on our build machine, so the clearing behaviour above comes from reading AOSP source on its main branch, not from a device run. Behaviour on older Android releases or heavily modified vendor builds may differ, which is why the script prints all four keys after every change.
Apps That Ignore the Android Proxy
Setting the proxy correctly is half the job. Android's proxy is advice that each app's network stack chooses to follow or not. The ProxyInfo reference spells out the split: proxy settings "are already integrated within the java.net and Apache HTTP stack," and "Other HTTP stacks will need to obtain the proxy info by watching for the Proxy.PROXY_CHANGE_ACTION broadcast." A stack that never does that connects directly.
The categories that usually bypass it:
- Flutter apps on
dart:io. Dart'sHttpClient.findProxydocumentation says: "If this function is not set, direct connections will always be used." The default client inpackage:http(current version 1.6.0) isIOClienton Android, so unless the developer wired up a proxy lookup or switched tocronet_http, a Flutter app ignores the Wi-Fi and global proxy alike. - Apps with their own native networking, such as games and video apps that open raw sockets from C++ code.
- UDP traffic of any kind. An HTTP proxy carries TCP connections only. Voice and video calls, and anything else on UDP, go direct.
- Background work after a network switch, when the Wi-Fi-only proxy no longer applies because the phone moved to mobile data.
The browser is the case Android itself vouches for, per that Settings warning. For WebView, Chromium's proxy documentation says it "uses the Android system's PAC resolver," so it reads the system configuration too. The same document adds a detail relevant here: Chrome "will not use any credentials embedded in the proxy settings," so even on platforms that store them, authentication goes through the normal challenge flow.
A proxy cannot fix a bypassing app. Whitelisting, forwarders and the global proxy all depend on the app asking the system which proxy to use. To force an app that doesn't ask, you need a VPN-style tunnel that captures all traffic on the device, or you test it on the emulator, whose proxy works below the app at the TCP layer. If your test plan includes Flutter builds or games, budget for that from the start. Our mobile proxies guide for social media automation and app testing covers when a mobile carrier IP matters for those apps at all.
Confirm the Exit IP, Not Just the Setting
A saved setting only proves the setting was saved. Prove the path in three layers.
1. Check the gateway from a laptop first. This separates "my credentials or whitelist are wrong" from "the phone isn't using the proxy":
"""exit_ip.py: confirm credentials and rotation before touching the phone."""
import os
import urllib.request
ECHO = "https://api.ipify.org"
USER, PASS = os.environ["SPARK_USER"], os.environ["SPARK_PASS"]
def fetch(proxy=None):
handlers = []
if proxy:
handlers.append(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
opener = urllib.request.build_opener(*handlers)
with opener.open(ECHO, timeout=20) as resp:
return resp.read().decode().strip()
print("direct ", fetch())
for port, label in ((11000, "random"), (11002, "sticky")):
url = f"http://{USER}:{PASS}@gateway.sparkproxy.io:{port}"
print(f"{label:6} {port}", [fetch(url) for _ in range(3)])
On port 11000 the three addresses should normally differ. On 11002 they should match for up to 5 minutes. If both lines raise a 407 error, fix the credentials before going near the phone.
2. Check from the device's browser. Open https://api.ipify.org on the phone (or run python android_proxy.py check). It should show a proxy IP, not the one you wrote down in Route 1. With the forwarder running, a CONNECT api.ipify.org:443 line appears in its log at the same moment.
3. Check the app you actually care about. Open it, use the feature you're testing, and watch the forwarder log. Its API hostnames should scroll past. If the log stays quiet while the app clearly loads data, that app is in the bypass list from the previous section, and a browser check passing tells you nothing about it.
For a quick on-device read of the active value, adb shell settings get global http_proxy shows the global proxy. The per-network Wi-Fi proxy lives in that network's saved configuration rather than in Settings.Global, so check it in the Wi-Fi screen.
Symptoms and Fixes
Work down this list from the top. The cheapest checks come first.
| What you see | Most likely cause | Fix | Can the proxy fix it? |
|---|---|---|---|
| "The hostname you typed isn't valid." in the Wi-Fi dialog | Scheme or `user:pass@` typed into the hostname | Hostname only: `gateway.sparkproxy.io` or `127.0.0.1` | Yes, config |
| Chrome shows `ERR_PROXY_CONNECTION_FAILED` | Nothing listening at the host and port: forwarder stopped, wrong LAN IP, or `adb reverse` dropped | Restart the forwarder, re-run `adb reverse`, check the port | Yes, config |
| Every page fails with a 407 | Public IP changed or was never whitelisted, or wrong credentials in the forwarder | Re-check with `exit_ip.py`, update the whitelist | Yes, account |
| Browser shows a proxy IP, app still shows home region | App bypasses the system proxy | Emulator, VPN-style capture, or test on the backend | No |
| Works on Wi-Fi, leaks on mobile data | Wi-Fi proxy is per network | Use the adb global proxy, or switch mobile data off | Partly |
| Device has no internet after testing | Global proxy still set after `settings delete` | `settings put global http_proxy :0` | Yes, config |
| App logs out repeatedly behind the proxy | Exit IP rotating between parallel connections | Use port 11002 (Sticky Sessions) | Yes, config |
| TLS errors in one app only | Certificate pinning, or a decrypting proxy upstream | The forwarder here doesn't decrypt; look for something else intercepting TLS | No |
That last row comes up when people swap in a debugging proxy. Android's network security configuration documentation says apps trust the pre-installed system CAs by default, and only "apps targeting Android 6.0 (API level 23) and lower" also trust user-added certificates. A pass-through forwarder like authfwd.py never presents its own certificate, so it can't cause this error. An intercepting tool can.
On cost: SparkProxy plans are packs starting from $75/month (USD, as listed on sparkproxy.io on 2026-10-09) with unlimited bandwidth, so the billing unit is the plan and its concurrency, not gigabytes. A phone left on the proxy overnight pulling app updates doesn't run up a traffic bill. The 24-hour free trial needs no credit card, which is enough to run all three verification layers above on a real device before you commit.
Frequently asked questions
FAQ
Android stores the proxy as a ProxyInfo object, which has a host, port, exclusion list and optional PAC URL, and nothing for credentials. Every vendor skin writes that same object, so no menu will reveal a hidden password field. Use IP whitelisting or a local forwarder instead.
Run a forwarder that adds the Proxy-Authorization header, either on a laptop on the same Wi-Fi or on the phone in Termux, and point the Wi-Fi proxy at it. Neither needs root, and the credentials stay out of Android's settings.
No. Android's own Settings warns that the HTTP proxy "may not be used by the other apps." Apps on the java.net stack, Chrome and WebView follow it, while Flutter apps on dart:io, native socket code and all UDP traffic usually go direct.
Run adb shell settings put global http_proxy host:port with no scheme and no credentials. Pair it with adb reverse tcp:8888 tcp:8888 and a laptop-side forwarder if the proxy needs a password.
Run adb shell settings put global http_proxy :0 first, then delete the key. In AOSP's code, deleting http_proxy alone leaves the hidden global_http_proxy_host and global_http_proxy_port values in place, so the device can stay proxied after a reboot.
Open https://api.ipify.org in the phone's browser and compare it with the address you saw before enabling the proxy. For a specific app, watch the forwarder log while you use it, because a browser check can't tell you whether that app follows the proxy.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
SSL WRONG_VERSION_NUMBER With a Proxy: The Fix
Getting SSL WRONG_VERSION_NUMBER through a proxy in Python? It's almost always https:// in the proxy URL. The one-character fix, plus every SSLEOFError cause.
pip install Proxy Setup: --proxy, pip.conf and Env Vars
pip install proxy setup that works: the --proxy flag, pip.conf, PIP_PROXY vs HTTPS_PROXY precedence, encoded passwords, and the SSL error a wrong scheme causes.
Proxy Not Working on Certain Websites? How to Fix It
Proxy not working on certain websites but fine everywhere else? Tell a block from a port, TLS/SNI or DNS fault with one command each, then apply the right fix.
