A lot of my work involves 3rd party API integration. We have plenty of tools at our disposal: protocols like SOAP, XML-RPC, JSON-RPC, and conventions like RESTful URI schemes. You would think it’d be a no-brainer to hook services together. Not always.
I was recently writing some code that called an XML-RPC method to retrieve logging data for an external service. The method signature itself was simple (names changed to protect the, all right, for the heck of it):
log(id, start, end)
That should be straightforward. The XML-RPC spec says that dateTime values should conform to ISO 8601, a well-defined format. The id field here is a simple string, start and end mark the time window for which to pull the data.
When Simple Isn’t
The server side of this project is mostly PHP, so the natural thing to do was pull in the PEAR XML_RPC2 class. Just set up the RPC endpoint and any options you need; then call functions as though they were defined locally (incidentally, if the API requires authentication, you supply it in the URI; this wasn’t documented anywhere that I could find, so I commented on the PEAR site).
At its core, our code boiled down to this:
$s = date("c", strtotime($start)); // iso8601 date/time $e = date("c", strtotime($end)); $log = $xmlrpc_api->log($acct_id, array( "start" => XML_RPC2_Value::createFromNative($s, 'datetime'), "end" => XML_RPC2_Value::createFromNative($e, 'datetime'), ));
However, the data we got back was for the wrong time window. If we asked for logging from 12:00-13:00, for example, the API gave us 16:00-17:00. Clearly we had a time zone shift thing happening.
Pick Your Poison
As it turns out, correct usage of the PEAR class created faulty XML:
It is technically valid XML-RPC, but it was completely stripping the time zone offset from the data we provided. The API we were calling defaults to
America/New_York in the absence of an explicit time zone, so as far as the API was concerned, we were getting exactly what we asked for; on our end, the request was just being generated incorrectly (frustratingly, this is utterly compliant with the spec, which drops time zone concerns into the programmer’s lap rather than setting a standard, which would make it easy to handle in library code). Our project runs in UTC, so we needed that offset.
We saw a couple of options. We could:
Munge our times by offsetting them from UTC to EST/EDT, depending on daylight savings. We would get correct data, but the system would still be incorrect in that we would still be providing dates to the API sans time zone, effectively relying on their default behavior (which is not specified in their documentation, making it a poor candidate for a dependency in a production system).
Hack the XML_RPC2 library to tack a TZ onto the end of the dateTime fields. Again, we would get correct data, but we would be incorrect in that we are modifying our library code, introducing the potential for future issues should we ever need to upgrade the library and forget to re-apply our hack.
Format the dates as ISO 8601 ourselves (including the time zone offset), and encode the value as a regular string, by skipping the call to
XML_RPC2_Value::createFromNative(). This gave us XML like this:
Surprisingly, the API gave us correct data without complaining. However, nothing in the spec says this is allowed to work, and besides, it is semantically incorrect as well. You could make the argument that this is just a different facet of the first option’s downside, incorrectly relying on the API default, unspecified behavior.
None of these options seemed very appealing, but in the end, we went with the second one. The hardware in question is dedicated to this project, so we were not worried that another application might need to run in something other than UTC.
The bottom line is: watch your date/time values carefully. Keep your libraries close, and your sense of pragmatism closer.