Tracing the scenic route
Or: how following a silly little meme might involve digging through lots of source code, building out a new Linux server, reviewing several IETF/W3C standards, and filing multiple bug reports.
Such a simple idea
Within an hour or so of joining the Room to Think co-working group here in town, I was introduced to a strange Internet meme. I still don't know the first thing about the trend itself, but there are a bunch of animal-themed sites that show you some ads and tell you your public IP address: ipchicken.com, iprooster.com, ipgoat.com, ipcow.com…
Given that my own company has an animal in its name — and likes Internet Protocol! — why not see if IP Calf was still available for adoption? Yes. It was! What would I put there?
Back in its Mac/iPhone shareware days, Calf Trail Software, LLC had a kind of nostalgic brand — NOT showing ads for one, and building software that maybe wasn't shiny but designed to serve its purpose in a straightforward, user-friendly way. I don't always check my IP address, but when I do, I prefer this kind of experience. (Looks like the guy behind http://iphorse.com invented the same idea before I did. But Horse Trail Software doesn't have the same ring to it now does it??!) Anyway, just a simple What Is My Public IP Address? site, mostly "just because".
So it began…
Getting http://ipcalf.com/ into production
The basic code took barely more than a minute to write. I added a new show function to whatever CouchDB project I was working on at the time, did one quick test to check that the client IP address is available, and then implemented it in the minimal way I had envisioned:
function (doc, req) { return "Your IP address is: <h1>" + req.peer + "</h1> Have a nice day."; }
My simple markup appeared as intended in my current web browsers, but I needed to take a quick look at the HTML5 spec to make sure I only omitted optional tags. My markup did appear to follow the rules. So let's get this thing into production! (Note below what I missed here, though...)
If this thing's gonna be real, it needs to be a standalone project with its own code repository. That means setting up some quick boilerplate. For the old Python couchapp utility, this just means a .couchapprc file containing
{}
and an _id file with the design document's name. Now I can upload my JavaScript code from the new app's shows folder.I'll be using git for source control so I set that up too. Just a simple
git init
plus a local .gitignore so no one has to see those pesky .DS_Store files. Oh, and I'll be sharing the code on GitHub, so I should make a README explaining the project.As I type the documentation, I realize it would be cool to make the service a little more useful. Why not use content negotiation to serve a plaintext and a JSON version of the client's IP address? Then I could use this from shell scripts and node.js experiments! No worries, another quick few lines of code and now I can put a few interesting examples in the README.
I should test the code examples, eh? Oh, I missed some braces when I typed up the JavaScript one. Oops. Still not working! Oh, right: I'm testing this in a browser, and it's a cross-origin request. No problem, I think there's just a little header I need to set.
Skim through the W3C's Cross-Origin Resource Sharing spec and add an
Access-Control-Allow-Origin: *
header to my JSON response. Done!Hmm, still not working. Review the CORS and XHR2 specs more thoroughly. Aha! My Fermata REST client library is setting a superfluous Content-Type header on its GET requests. This header isn't whitelisted as a safe cross-origin header, which is why the browser is trying to obtain more extensive pre-flight permission before the actual fetch. After I publish a patch for Fermata, my README example now works great. Success! Ship it!
I need a basic up-to-date, reliable public CouchDB server and that's exactly the kind of hosting IrisCouch provides. I've already set up a free account there, Hosting a CouchApp there is configuring a Virtual Host entry I register the domain and point its DNS records at my free account there.
Oh no! IPCalf.com is live, but it's telling me my public address is 10 dot something something. The 10/8 prefix is reserved for private internets so this must be a load balancer or proxy inside of IrisCouch's hosting setup. I spend some time in CouchDB's source code to see if it handles the X-Forwarded-For header that's typically used in these kind of situations. It looks like it does, so I file a support ticket with Jason. He's on the case. But I'm on a roll here; I want to get this project wrapped up!
I guess I'll use my own server for now. Lately I've been having trouble rebuilding CouchDB on my current barebones EC2 instance. It's running the default Amazon Linux, and I built it out before AWS opened new datacenters right down the Columbia from where I live. My devops ninja friend is embarrassed I'm messing with such a bad distro and I've been meaning to move my server closer anyway. So blah blah blah, I'll just spin up a new server. This time in the Oregon region with an Ubuntu-provided image. [gets it all set up, spares you the details]
So now I have my own server, running the latest release version of CouchDB, and ipcalf.com is pointing to it. I try the examples in my documentation one more time, just to be sure. Hmmm, the JSON example is failing and in an odd sort of way. It works on my local machine, but not on the server. Am I doing something wrong? What's going on? It's getting late but I have to know!
Trying this, trying that, finally trying to find how all the relevant underlying pieces in CouchDB are wired together. Getting closer, still not finding exactly what I'm looking for. Making mistakes, testing code changes without deploying them first, still looking for the answer.
Finally, yes there's the code and that's the bug! Not my fault, but something I can work around. It's now past 3AM, over twelve hours after I decided I'd toss this quick little site idea online. Even if I can get it working correctly now, not many friends will be online to see it, so I guess I'll finish it up tomorrow.
The next day I'm a little tired but ready for a fresh start. There's one other issue that's been on my mind, but I haven't finished dealing with yet. CouchDB has a nice built-in infrastructure that enables efficient validation of cached responses. The problem is, it's designed for more typical (stored data–based) use cases and can cause a user's browser to report a stale IP Address if they've visited the site earlier. As I worked on everything above, I'd started learning the ins and outs of caching in HTTP and experimenting with various header fields and redirect strategies that might help me avoid the situation. I continued to study how different browsers handle multiple ETag headers, noticing how only some browsers seem to cache redirect responses, and finally reached the conclusion that CouchDB needs modification before I can resolve this issue.
So now I seem to have the site working as well as I can. I still need to follow up with some of the support tickets I've opened, bugs reports I've found or filed, and conversations I happened to spark while logging my progress on Twitter. By the end of the next day, I've finally got all relevant browser tabs closed and I have the project to a point where I can blog about it.
That was yesterday. Writing up everything I did the last two days has been today's main project. I've been finding links and notes back, trying to describe what I did in a way that makes sense (or is at least moderately entertaining) to everyone who is interested in this experience but may have been learning about something completely different the last three days/decades.
While boasting in step 2 above how much care I took to ensure my output was correct, I realized I should check to see what an HTML5 validator thinks of my markup. Whoops! I guess I still need to add something like "<!doctype html><title>IP Calf</title>" to specify the parsing mode and provide the required page title.
Now I wonder: does the doctype really "specify the parsing mode" like I just claimed? Curious, I start looking into what this processing instruction meant in SGML, XML and what its practical implications are in HTML…
I step away from writing for a bit. Realize this whole story would have been even more fun if it were presented as a branch's history log on github itself. Research the feasibility of an accurate retelling — can I override the commit timestamps to match what I would reconstruct from my browser history log? This idea grows on me. I dream of what wealth and fame the SHEER ARTISTRY of this idea could bring me. Wrestle with this new idea for much longer than it would take to just jump in and commit, weighing how much time it might take to do well against how fun it would be to pull off.
Finally decide to just share this story as a regular old blog post, the same tired way everyone else shares their regular old stories. Spend a while brooding over how boring a person I must be and no wonder nothing I do ever stands out. Begin to feel guilty for wasting my time and yours with this silly little site that hardly made the world a better place and is not even much to look at.
The end of the matter
You might be thinking "wow, is programming always that hard?" or "that would have been SOOOO much easier if he'd just used _____".
No, and no.
Shipping product-grade code is never easy. You can spend minutes to make something work on your machine, hours making sure it should work in situations you may never have opportunity to test on, and days, sometimes, to figure out why it doesn't work in a situation you did test. But every time you do it, you beat a path behind you. It's a lot easier to build, analyze or debug something the second time.
And yes, in this case the tools I used were not the best fit for what I was trying to accomplish. Or were they?
Could something do everything this does in less (or better) code on a different (or better) platform? Sure! But I didn't choose CouchDB, I didn't first go to IrisCouch and I didn't end up using Ubuntu on an EC2 instance, because I was sure they were better than every other option for this silly little task. It might have been less time or less typing to accomplish the same thing in PHP or node.js. It might have been more. I might have had less to learn deploying on Heroku or Savvy Sally's "IP Address Website" Amazing Hosting Platform. I might have had more.
Every now and then, deploying three minutes of high-level code means I spend three days learning what's underneath holding things up. But more often than not, it takes me only three months to deliver something that might take three years in a typical enterprise. I'm okay with the former if it makes the latter possible.
In this case, the road wasn't as well travelled as I imagined at the start. I still don't regret taking it. Not only did I gain a lot of new experience for myself, but by discovering some pitfalls along the way I've made it easier for someone else to build better along that frontier. That "someone else" might even end up being me!