My new project: Tact, a simple chat app.

Debugging HTTP on an Android phone or tablet with Charles proxy for fun and profit

February 12, 2012

Although I am an UI designer and UI engineer by trade, I enjoy practicing other crafts that go into making apps. At this day and age, to build a client app, you basically need to know about two things: UI and networking/cloud, and the latter usually involves HTTP. It’s one of the most important technology protocols of our time, and I try to keep my edge in working with it.

Recently, my team had a problem. When our app was talking to our test servers, we had a seemingly identical request that worked from iPhone and failed from Android. The failure was that instead of returning the expected 200 OK with a response, the server responds with 302 Found and redirects the caller to an authentication page where they must authenticate themselves when approaching from outside a corporate network. This authentication is fine, but it was unclear why the same request returned 200 OK on iPhone and 302 Found on Android.

I saw this as a fun opportunity to debug a bit with Charlesproxy. It’s an indispensable tool for all sorts of HTTP proxying/debugging kinds of things. Apparently people have tried to hack Facebook games with it, because on several pages, Charlesproxy now says “Please note: Charles is not intended or marketed as a tool for hacking Facebook games. Please do not contact me for support in this case.”

There are many other tools like this available. Maybe some of them are cheaper or better. I just know Charlesproxy the best and it works well on my Mac so I’m using it.

The text below assumes you have basic knowledge about HTTP and curl.

Preparing your Android device

There are basically two steps you need to do to prepare your device:

  1. Turn on proxying in your device.
  2. Install the Charlesproxy CA certificate if you intend to debug SSL calls (which I did need to do, as our app was using SSL)

Charlesproxy FAQ contains instructions for the iPhone on how to do these things, but not Android.

Installing the certificate is easy. Just browse to http://charlesproxy.com/charles.crt and a dialog pops up, prompting you to install it. Go ahead. You’re now configured to trust Charlesproxy as “man in the middle” for your SSL connections, and for all SSL connections during the proxying, your device will see a certificate issued by Charles instead of the real server.

If you don’t install the certificate, you’ll see strange things in Charles where the phone seemingly opens a request, but then it disappears from the list as if it never happened. That’s because the phone received a response that was signed with Charles’ untrusted certificate, and closes the session.

Configuring the proxy was more challenging for me. Assuming you’re on Wifi, turns out you have to long-press your wifi network in wifi settings, select “Modify network”, and you can set up the proxy.

Use your computer’s IP address for the host name. Find it out with Charles Help menu, “Local IP Address…” option

Charles’ default proxy port is 8888, you can change it in Charles proxy settings.

Be sure to turn off the proxy when done debugging, otherwise the phone’s HTTP stack will simply not work, trying to talk to a proxy that isn’t there any more.

Preparing Charles

Just launch the Charles app and there’s not much to prepare. By default, it will change Mac network settings to proxy all your Mac use for you. If that’s not needed, turn off “Mac OS X Proxy” in the “Proxy” menu.

By default, the “Record” button is already on, and you’re set to go.

One other thing to do is to turn SSL proxying for each host that you need. In Charles, that’s “opt in”: you need to explicitly turn on SSL proxying for each host that you need SSL for. In Proxy Settings, navigate to the SSL tab, and you’ll see the locations. Click “Add” to enter the target SSL hosts and ports. I just entered them as I went: Charles will still show the requests in the list even if you haven’t opted in for the host, but the request and response contents are not available until you opt in for that host and Charles then starts playing man-in-the-middle.

Debugging

The actual debugging depends on what exactly you’re after. To start, make sure that proxying is working and just do a basic request from your browser. In my case, I ran a request to cnn.com from the Android browser and here’s what showed up in my Charlesproxy for that.

Tons of stuff: the actual content, supporting CSS, Javascript, images, and external hosts that have nothing to do with CNN, that I guess are some sort of tracking/analytics. Pretty informative, actually. Just try using some apps on your phone and see what kinds of data they send. (Or turn on Mac OS X proxying and do the same on your computer.)

In my case, I wasn’t really sure how to tackle the problem, so I started by just using the relevant part of our apps on both iPhone and Android to run the requests (having, of course, set up proxying on both devices.) Sure enough, I could see 200 OK for iPhone and 302 Found for Android. The requests had different HTTP headers, and they were different enough that I didn’t want to compare them just yet.

My next plan was to replay the failing request with commandline curl. I always like getting “close to the metal/wire” this way, eliminating the possible extra problems added by the client stack.

Replaying with curl

curl is another amazing commandline tool for HTTP. You can perform any HTTP request/response and just watch things happen. Charlesproxy had handily already captured all the parameters, so all I needed to do was to navigate to the right tab to grab the POST parameters (the request in question was a POST)

My thinking was like this: let’s do exactly the same request with curl (meaning the same host and same POST parameters), and it should fail the same way. I grabbed the POST parameters and host from the Android-generated request, copied them to curl commandline, and pressed Enter, expecting to see a 302 Found…

… but I saw a 200 OK, and it worked. Wow. This was one of those “holy crap, I don’t understand what exactly this is but I feel it’s a huge step closer to the solution” moments. I have exactly the same request that fails when ran from Android and works when run with curl. It is coming (from the server’s point of view) from exactly the same network endpoint (my computer). But clearly, something is different.

How do we find out? Well, we have this handy Charlesproxy thing running here. Let’s just proxy curl’s request to it as well and compare the breakdown of the curl and Android requests.

How do we proxy curl through Charlesproxy? Sadly, it isn’t as easy as just turning on “Mac OS X Proxy” in Charles’ settings, as curl isn’t built to consider the OS X proxy settings. But it’s not much harder either, you just need to know an extra commandline setting. My final curl command was:

curl -k -x 10.0.1.4:8888 -v "https://my.server.example.com/endpoint"
-d "post-data"

A few handy switches here.

-k means “don’t validate the server certificate, just blindly trust it.” Remember how we added Charlesproxy certificate for the device? I coud do the same for curl if I wanted to, it has a similar certificate store. But I can also tell it with -k to just not bother about validating.

-x is the proxy IP and port, which should be your computer’s IP and the port that Charles is running on.

-v just emits extra debugging info for extra coolness factor. You may leave it out and it works the same.

Comparing the curl and Android requests, and the answer

So if I ran the above, indeed, the request showed up in my Charlesproxy next to the failing request from Android. Now it was really time for deep detective work and comparing each bit and piece of the requests.

The answer wasn’t very far. The curl request had an User-Agent header, and the Android one didn’t. A bit strange: turns out that our Android app was using the DefaultHttpClient library, and the library does not insert any User-Agent if you don’t specify any. That’s a bit strange: on iOS, if you don’t specify a header, the system automatically inserts it for you.

Well, but it nevertheless shouldn’t matter, should it? User-Agent is just cosmetics and shouldn’t affect this kind of requests? Let’s test it. You can tell curl to remove the user-agent by inserting an empty value, or you can set it to any value. Here’s a line that removes it:

curl -k -x 10.0.1.4:8888 -v "https://my.server.example.com/endpoint"
-d "post-data" -H "User-Agent:"

I typed this in, and pressed Enter, and… hurrah. There’s my 302 as expected. I had a reproducible test case now and had found the root cause (from the client’s POV) of why the requests were behaving this way.

If this was the case, the opposite should also be true, right? I should specify the User-Agent header in Android code and it should work? That’s exactly how it was. I added a User-Agent and things started flowing smoothly: the app worked and nice 200 OK-s showed up in Charlesproxy.

So apparently in our server request validation, the server was verifying User-Agent header and redirecting the caller if the header wasn’t there. Which is a bit strange, but I’ve seen stranger things. I could report this finding back to my team and we could move on.