NSDateFormatter, 24-Hour Time, and Region Formats

Working with a REST API is a common task for the intrepid iOS developer. A decent API will implement various mechanisms to secure itself against unauthorized access. One such common mechanism involves generating a cryptographic signature and timestamp that is attached to each client request in the HTTP headers. Part of the server's request validation involves checking the age of the request via the timestamp; if the time is outside some small window of variance, such as plus or minus 15 minutes, the request will be rejected. This helps mitigate the potential for replay attacks against the API.

This is all well and good, but for such a system to work, your server and client must agree on the format of the timestamp that is serialized. The client must eventually generate a NSString representation of the NSDate timestamp that can be added to your HTTP header. A typical way to perform this serialization is with the ever so helpfully named NSDateFormatter class.

Given a NSDate and a NSString date format, you can use NSDateFormatter.stringFromDate() to get your timestamp. Simple enough, right? Typically, you want to generate a UTC timestamp. So, you might do something like this (in Swift):

let now = NSDate()  
let formatter = NSDateFormatter()

formatter.timeZone = NSTimeZone.init(abbreviation: "UTC")  
formatter.dateFormat = "yyyyMMddHHmmss" 

let timestamp = formatter.stringFromDate(now)  
// 20160112025000

Great! And this will work most of the time. But, an interesting "bug" crops up when you start messing around with the Language & Region settings of iOS. If you change your region to one where the date format defaults to 24-hour time, such as Japanese, but you force time to be displayed as 12-hour (i.e. AM/PM) by turning off the switch in Date & Time in Settings, you'll find that the above code may not work as expected and your beautiful timestamp strings do not match the provided date format string.

If you are relying on code like the above to generate your REST API timestamps, you may suddenly find that some of your users get authentication errors for apparently no reason! Even better, if you try to debug the issue in the Simulator, you may be left scratching your head because everything seems to work fine, no matter what language and region you use. That's because the 24-Hour switch doesn't exist in the Simulator. You'll have to debug on hardware, like I did, to experience your own Eureka moment.

It seems that even if the date format string is explicit, NSDateFormatter will take into account the system locale in stringFromDate(). This is not a new issue. If you search Stack Overflow or Open Radar, you'll find multiple examples of developers encountering this issue in different contexts. I don't know if it counts as a bug or a feature, but it was definitely a frustrating gotcha for me until I figured out what was going on.

Maybe there is a better fix than what I'm doing, but if you explicitly set the locale on NSDateFormatter, your strings will be as expected. Although I looked around a bit, I didn't find anywhere in the documentation where locale strings are listed, but updating the above code snippet to the following should work around the issue:

let now = NSDate()  
let formatter = NSDateFormatter()

formatter.timeZone = NSTimeZone.init(abbreviation: "UTC")  
formatter.dateFormat = "yyyyMMddHHmmss"  
formatter.locale = NSLocale.init(localeIdentifier: "en_US")

let timestamp = formatter.stringFromDate(now)  
// 20160112025000

Maybe there is a better way, but this seems to work and now I'll hopefully never forget as I can just DuckDuckGo for this post the next time I'm messing around withNSDateFormatter and timestamps.