The code to create the upload URL looked like this:
So that seemed to be correct, but as I later found out it wasn't. To track down the problem, I had to enable debug output for the dev server itself. This is done by passing --dev_appserver_log_level=debug when running the dev_appserver.py script. For me the whole command line looked like this:
Now I was getting more detailed error messages, including a stack trace, like so:
The final message in the output was key here...
After some experimenting and re-reading the documentation I realised that the default bucket name '#default#'can't be used in this case. Even though much of the PHP runtime does support this shorthand expression, when working with the CloudStorageTools class, you have to provide the actual bucket name.
I still wanted to use the default bucket, so used the CloudStorageTools::getDefaultGoogleStorageBucketName() function to retrieve it. My $options array now looked like this:
Once I made that change, all of my uploads started working!