Chuyện gì đã xảy ra
Tôi tạm thời chuyển một API staging từ HTTPS sang HTTP thuần, và ứng dụng bắt đầu bị crash trên Android. Logcat hiển thị:
java.io.IOException: Cleartext HTTP traffic to api.staging.example.com not permitted
Android 9 (API 28) chặn cứng hoàn toàn — không cảnh báo, chỉ crash thẳng. Trước phiên bản Pie, HTTP vẫn hoạt động bình thường. Bắt đầu từ API 28, mọi request http:// đến domain không được khai báo rõ ràng sẽ bị hệ điều hành chặn. Không có cách nào vượt qua trong lúc chạy.
Ba tình huống thường gặp nhất: một backend tạm thời chỉ dùng HTTP trong quá trình migration, một máy chủ dev nội bộ tại địa chỉ 192.168.x.x, hoặc một SDK bên thứ ba vẫn đang gọi về server qua HTTP. Cái cuối cùng mới nguy hiểm — vì bạn không phải người viết code đó.
Quá trình debug
Trước tiên hãy xác định chính xác domain nào đang bị chặn. Lấy toàn bộ logcat và lọc theo từ khóa CLEARTEXT:
adb logcat | grep -i cleartext
Bạn sẽ thấy thông báo kiểu như:
W/NetworkSecurityConfig: Cleartext HTTP traffic to api.staging.example.com not permitted
Đó chính là domain cần xử lý. Đôi khi lỗi không phải do code của bạn — các SDK analytics nhúng sẵn như Firebase Crashlytics phiên bản cũ hay các SDK quảng cáo là thủ phạm thường gặp.
Tiếp theo, kiểm tra xem file network_security_config.xml đã tồn tại chưa:
find app/src/main/res -name "network_security_config.xml"
Nếu file đã có sẵn, nghĩa là ai đó đã thiết lập quy tắc bảo mật mạng và domain của bạn chỉ đơn giản là chưa được thêm vào danh sách.
Giải pháp
Cách 1: Cho phép từng domain cụ thể (khuyên dùng)
Tạo file app/src/main/res/xml/network_security_config.xml nếu chưa có:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">api.staging.example.com</domain>
<domain includeSubdomains="true">192.168.1.100</domain>
</domain-config>
</network-security-config>
Đăng ký file này trong AndroidManifest.xml bên trong thẻ <application>:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Cách này rõ ràng và dễ kiểm tra. Traffic HTTPS lên production vẫn được bảo vệ hoàn toàn — chỉ những domain bạn khai báo mới được phép dùng HTTP.
Cách 2: Chỉ cho phép HTTP trên bản debug
Chỉ cần HTTP trong lúc phát triển? Hãy giới hạn cấu hình lỏng này cho bản debug. Tạo file app/src/debug/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
Chỉ thêm thuộc tính android:networkSecurityConfig vào file manifest dành cho debug (app/src/debug/AndroidManifest.xml). Bản release sẽ không bao giờ thấy cấu hình này.
Cách 3: Mở toàn bộ (không dùng cho production)
Thêm thẳng vào AndroidManifest.xml:
<application
android:usesCleartextTraffic="true"
...>
Cho phép HTTP ở khắp nơi. Đừng bao giờ đưa cái này lên production — Google Play sẽ gắn cờ cảnh báo, và bạn đang bỏ đi bảo mật truyền tải cho toàn bộ người dùng. Chỉ dùng để xác nhận rằng HTTP thực sự là nguyên nhân gây lỗi, rồi xóa đi ngay.
Cách 4: Chuyển sang HTTPS (giải pháp thực sự)
Với các server do bạn quản lý, hãy bắt buộc dùng HTTPS. Với môi trường dev nội bộ, hãy dùng chứng chỉ tự ký hoặc chạy ngrok để có HTTPS tunnel trong vòng chưa đầy một phút. Với các SDK bên thứ ba vẫn còn dùng HTTP, hãy báo lỗi lên nhà cung cấp — đó là trách nhiệm của họ.
Kiểm tra lại
Build lại và cài đặt:
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
Kích hoạt network call trong ứng dụng. Cảnh báo Cleartext HTTP traffic sẽ không còn xuất hiện trong logcat nữa. Nếu bạn đang dùng OkHttp, hãy thêm logging interceptor để xem toàn bộ request hoàn thành:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor().setLevel(Level.BASIC))
.build();
Nhận được response 200 là đã sửa thành công. Nếu vẫn còn IOException, nghĩa là domain đó vẫn chưa được thêm vào whitelist.
Mẹo thêm
Không xác định được thư viện nào đang gọi HTTP? Bật StrictMode trong Application class khi debug:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectNetwork()
.penaltyLog()
.build());
Cách này sẽ ghi lại toàn bộ stack trace — tận đến class khởi tạo request. Nhanh hơn nhiều so với việc dò thủ công qua 15 SDK.
Đang làm việc với dải IP cho máy chủ dev nội bộ hoặc proxy công ty? Subnet Calculator trên ToolCraft giúp bạn kiểm tra dải CIDR nhanh chóng. Rất tiện khi bạn chưa chắc 192.168.x.x và 10.x.x.x có cùng subnet hay không trước khi viết các quy tắc domain-config.
Bài học rút ra
- Đừng để
android:usesCleartextTraffic="true"trong file manifest chính. Nếu thực sự cần, hãy giới hạn trong phạm vi debug. - Cấu hình theo từng domain trong
network_security_config.xmlmới là cách làm đúng — tường minh, dễ review trong code, và không ảnh hưởng đến traffic production. - Chạy
adb logcat | grep -i cleartexttrước mỗi lần release. Các SDK bên thứ ba là nguồn gây ra lỗi này một cách âm thầm và thường bị bỏ sót cho đến khi ai đó xem báo cáo bảo mật trên Play Store. - Tài liệu Network Security Config của Android khá đầy đủ và dễ đọc. Đọc hết một lần sẽ nắm được certificate pinning, trust anchors và debug overrides — tất cả những tùy chọn bạn sẽ cần đến sau này.

