Mobile


30
Aug 08

ORM comes to the iPhone: SQLite Persistent Objects

Although I cut my “programming teeth” on functional database APIs, I happily gave them up years ago in favor of ORM—and I have no desire to go back.

Naturally, the iPhone a certain NDA-bound platform of indeterminate nature has no ORM layer for the bundled SQLite library.

So I was pretty thrilled to stumble onto Jeff LaMarche‘s SQLite Persistent Objects. Think Active Record, except instead of the database telling your model what it’s supposed to look like, the model tells the database what it should look like. Jeff calls the pattern “reverse Active Record”, but I think it looks a lot like Data Mapper.

The best part is that it works exactly like you’d expect:

Person *person1 = [[Person alloc] init];
person1.givenName  = @"John";
person1.familyName = @"Doe";
person1.birthdate  = [NSDate dateWithString: @"1908-11-24 04:12:00 -0800"];
[person1 save]; // Inserts a new row
[person1 release];

Person *person2 = [[Person alloc] init];
person2.givenName  = @"John";
person2.familyName = @"Smith";
person2.birthdate  = [NSDate dateWithString: @"1945-09-13 03:54:00 -0800"];
[person2 save]; // Inserts a new row
[person2 release];

// Oh wait, they both go by Jack

NSArray *people = [Person findByGivenName: @"John"];
for (Person *person in people) {
    person.givenName = @"Jack";
    [person save];
}

It’s still in the very, very early phases of development (it’s at revision 5), so there are some issues to work out, like what happens when the schema changes.

There are also currently a few bugs preventing SQLite Persistent Objects from, uh, compiling at all. I’ve submitted a patch (removed) that addresses these issues. I’ll update this entry when it’s been applied to trunk. (Update: It appears to have been patched.)

Regardless, it’s worth keeping an eye on, even if you don’t use it right away. Seriously, download the source and check it out.

Like this post? You might also like Coalmine, my centralized error tracking service for your apps. Coalmine captures errors and all kinds of helpful debugging information, notifies you, and makes it all searchable. Check it out!

27
Aug 08

Major bug with BlackBerry browser and multiple cookies

This is a long post.

Having investigated this issue for the last few days, I believe that there is a significant issue with the cookie implementation in BlackBerry browsers using the default Internet Browser. I haven’t been able to test the recently-released BlackBerry Bold (9000), which is bundled with the new BlackBerry Browser, but as far as I can determine, devices like the 8800 and 8820 are affected.

The problem occurs when a site attempts to set multiple cookies. Although they are stored on the device, the cookies are returned to the server haphazardly. Sometimes all cookies are returned; more often it’s only the first or last cookie that was set that is passed back.

The following is a TCP dump of traffic to a particular server. I’ve bolded the relevant parts [update: well, I did at one time] and changed a few names. The server is not load-balanced, and the headers are not altered on my end in any way.

GET / HTTP/1.0
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
x-wap-profile: "http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf"
Accept: application/vnd.rim.html,text/html,application/xhtml+xml,application/vnd.wap.xhtml+xml,text/vnd.sun.j2me.app-descriptor,image/vnd.rim.png,image/jpeg,application/x-vnd.rim.pme.b,image/gif;anim=1,application/vnd.rim.jscriptc;v=0-8-8,application/x-javascript,application/vnd.rim.css;v=1,text/css;media=handheld,application/vnd.wap.wmlc;q=0.9,application/vnd.wap.wmlscriptc;q=0.7,text/vnd.wap.wml;q=0.7,*/*;q=0.5
Accept-Charset: ISO-8859-1,UTF-8,US-ASCII,UTF-16BE,windows-1252,UTF-16LE,windows-1254,KOI8-R,windows-1251,windows-1255,windows-1256,windows-1250
Accept-Language: en-US,en;q=0.5
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
Host: cookie.example.com
Via: BISB_3.3.0.45, 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:19 GMT
Set-Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:19 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=2d14a216211bc7f1a1b03fa747d1087e; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Length: 7975
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

Here we set three cookies: cookie1, cookie2, and cookie3.

GET /subsequent/request HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:21 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:21 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=a082e222cde7b4aedb6a8b42f2723849; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Length: 3701
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/css

The BlackBerry (an 8820) returns only one cookie back (cookie1), so the server tries to resend the cookies it did not receive. Keep in mind the expiration times here were set in the future for this request.

GET / HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie1=o4s9o298mbqjf5qnjmgad76rs0
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:56:21 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:56:22 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=270773eec992a03fd63a7b74b2f5ef9a; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=utf-8

This is the same as above. The next GET is really interesting:

GET / HTTP/1.0
User-Agent: BlackBerry8820/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100
profile: http://www.blackberry.net/go/mobile/profiles/uaprof/8820/4.2.2.rdf
Referer: http://cookie.example.com/
Host: cookie.example.com
Cookie: cookie2=en_us
Cookie: expires=Thu, 27-Aug-2009 15:57:03 GMT
Via: 1.1 pmds76.bisb1.blackberry:3128 (squid/2.5.STABLE12)
X-Forwarded-For: unknown
Cache-Control: no-cache, max-age=259200
Connection: keep-alive

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 15:57:05 GMT
Set-Cookie: cookie1=32mrdio18lvpg6cduhnt1prmj5; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 15:57:05 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=2d0b160a97e20469aad34e01890adbfb; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=utf-8

The BlackBerry sends back cookie2 and “expires” in two headers, as if they are two separate cookies!

For comparison, here’s a similar request from the same device accessing the same URL, but using Opera Mini instead. It performs as expected.

GET / HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Connection: Keep-Alive
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.82, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:14:47 GMT
Set-Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; path=/; domain=cookie.example.com
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: cookie2=en_us; expires=Thu, 27-Aug-2009 17:14:47 GMT; path=/; domain=cookie.example.com
Content-Language: en_us
Set-Cookie: cookie3=1ce651a62bfe62a25faff44e5e5d500b; expires=Mon, 01-Sep-2008 06:59:59 GMT; path=/; domain=cookie.example.com
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 2176
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

GET /image/example.gif HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.82, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:14:49 GMT
Last-Modified: Wed, 20 Aug 2008 19:36:32 GMT
ETag: "2c"
Accept-Ranges: bytes
Content-Length: 44
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: image/gif

GET / HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Cache-Control: no-cache
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:16:08 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Language: en_us
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 2283
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

GET /subsequent/request HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 200 OK
Date: Wed, 27 Aug 2008 17:16:09 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Language: en_us
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 958
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/css

GET /image/example.gif HTTP/1.1
User-Agent: Opera/9.50 (J2ME/MIDP; Opera Mini/4.1.11355/546; U; en)
Host: cookie.example.com
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en
Accept-Charset: iso-8859-1, utf-8, utf-16, *;q=0.1
Accept-Encoding: deflate, gzip, x-gzip, identity, *;q=0
Referer: http://cookie.example.com/
If-Modified-Since: Wed, 20 Aug 2008 19:36:32 GMT
If-None-Match: "2c"
Cookie: cookie1=dpr07co4i5dchtgmk8g3gdt0b6; cookie3=1ce651a62bfe62a25faff44e5e5d500b; cookie2=en_us
Cookie2: $Version=1
Connection: Keep-Alive, TE
TE: deflate, gzip, chunked, identity, trailers
X-OperaMini-Features: advanced, file_system, folding
X-OperaMini-Phone-UA: BlackBerry
X-OperaMini-Phone: RIM #
x-forwarded-for: 206.53.144.186, unknown

HTTP/1.1 304 Not Modified
Date: Wed, 27 Aug 2008 17:16:10 GMT
Connection: Keep-Alive
Keep-Alive: timeout=5, max=98
ETag: "2c"

So what’s the solution in this situation? It looks like your best option is to set just one cookie—but shove all your values into it and delimit them in some way (and make sure it’s less than 1024 bytes). If you have multiple expire times, though, you will also have to handle expiration manually. Google does this:

Set-Cookie: PREF=ID=xxxxxxxxxxxxxxxx:TM=1219863220:LM=1219863220:S=yyyyyyyyyyyyyyyy; expires=Fri, 27-Aug-2010 18:53:40 GMT; path=/; domain=.google.com

Can everyone just buy an iPhone already?

Like this post? You might also like Coalmine, my centralized error tracking service for your apps. Coalmine captures errors and all kinds of helpful debugging information, notifies you, and makes it all searchable. Check it out!