Archive for the ‘GData’ Category
Google API – A Moving Target
The latest issue I’ve run into is with creating documents. Because Google Docs does not support creating new documents offline, GDNote creates a folder full of blank documents that can be moved and renamed while offline. Yesterday this worked, but today the call gets the error 400 Bad Request – Document content required.
We have very good error logging set up, so I should be able to fix it quickly and not too many users will be affected, but these unexpected issues are still pretty frustrating.
Dealing with Google API Bugs
The most recent case involves a user getting errors doing anything that creates a folder. Everything works fine with my own heavy usage and all of the test accounts, but for one particular user it doesn’t work. Fortunately GDNote logs all errors in as much detail as possible without causing security issues, so I can see the request that failed. It’s exactly the same api call that works for everyone else, but for this particular user Google returns 404 not found.
A bit of searching reveals that this is a known Google bug that has shown up in the last week or so, with a fix due later this month. Best I can do is wait for the fix and tell the affected user to create the folders manually. For similar issues in the past I’ve been able to create workarounds in the code, for example checking if a call really failed or just returned an error code for no apparent reason. What I can’t do is find a way to prevent these problems from occurring for real users in the future. Normally when an issue comes up and you fix it you would add a test that reproduces the original error, preferably before a real user has problems. But with these api problems the issue only occurs for some accounts, and setting up a test account with the same configuration doesn’t trigger the error.
Is it actually possible to do anything better than “Works on My Machine” when working with cloud APIs?
Uploading HTML with Images to Google Docs
However, if you upload an image file to Google docs, it will be converted into a document – the image gets a file id and a document is created containing a link to that image. This feature can be used to upload any necessary images before sending the html with the full content.
- Upload an image, naming it “Temporary Image Upload Doc” or something similarly obviously temporary
- Download the resulting document and parse the content to find the img tag with src starting with File?id=
- Delete the temporary document
- Update the main html content to use the file url obtained from the temporary document
- Upload the complete html document.
Image upload code is below. Note that I had to create the request manually rather than using service.UploadDocument – even though the server will accept images, the .NET api does not list them as valid content types.
///
/// Upload an image for embedding in a document
///
///
///
/// The id of the uploaded image
public static string UploadImage(string notesFolderId, Stream imageStream, string contentType, string token)
{
var service = GetService(token);
// upload
// standard .net api won't accept png
var respstr = service.StreamSend(new Uri("http://docs.google.com/feeds/documents/private/full"), imageStream, GDataRequestType.Insert, "image/png", "Temporary Image Upload File");
var rssr = new StreamReader(respstr);
var se = rssr.ReadToEnd();
// get ids from response
var tempDocId = GetDocIdFromResponseString(se);
// Download document
// TODO: this can be made a bit more efficient if we get the media uri from
// the original response
var e = service.Get(string.Format("http://docs.google.com/feeds/documents/private/full/document%3A{0}", tempDocId));
var resp = DownloadDocumentFromAltUri(e.AlternateUri.Content, token);
// delete temp doc
var tempDocEditUri = e.EditUri.Content;
var tempDocMediaUri =
string.Format("http://docs.google.com/feeds/media/private/full/document%3A{0}/{1}",
tempDocId,
tempDocEditUri.Substring(tempDocEditUri.LastIndexOf("/") + 1));
service.Delete(new Uri(tempDocMediaUri));
//
var mc = Regex.Matches(resp, "File\\?id=(.*?)'");
if (mc.Count < 1 || mc[0].Groups.Count < 2) {
throw new Exception("Unable to get image id from response");
}
return mc[0].Groups[1].Value;
}
Downloading Documents with the Google Docs API
- Searching for error messages doesn’t help much because the platform and its bugs are quite new
- The tools have pieces missing and don’t quite work as they should
- Documentation is inadequate at best
One of the most obviously missing functions in the API is the ability to download a document. I found a workaround on google groups easily enough, creating the http request manually and setting the Authorization header, but unfortunately that doesn’t seem to work for hosted accounts.
With a few wrong turns trying to get a new auth token manually, it took me a full day of trial and error to find something that worked – setting a cookie and adding the hosted domain name to the start of the auth token.
To save anyone else from going through the same process, here’s the code:
public static string DownloadDocumentFromAltUri(string altUri, string token)
{
var service = GetService(token);
string docurl;
string domain = null;
var docId = altUri.ToDocumentId();
if (altUri.Contains("docs.google.com/a/"))
{
// google apps
var s1 = altUri.Substring(altUri.IndexOf("docs.google.com/a/") + 18);
domain = s1.Substring(0, s1.IndexOf("/"));
docurl = string.Format("http://docs.google.com/a/{0}/RawDocContents?revision=_latest&docID={1}&justBody=true&browserok=true", domain, docId);
}
else
{
// google docs
docurl = string.Format("http://docs.google.com/RawDocContents?revision=_latest&docID={0}&justBody=true&browserok=true", docId);
}
Debug.WriteLine(string.Format("Downloading. DocId={0}, AltUri={1}, DocUri={2}", docId, altUri, docurl));
HttpWebRequest wreq = (HttpWebRequest)HttpWebRequest.Create(docurl);
if (UseAuthSub)
{
wreq.Headers.Add("Authorization", string.Format("AuthSub token={0}", token));
}
else
{
var authToken = ((GDataGAuthRequestFactory)service.RequestFactory).QueryAuthToken(service.Credentials);
if (string.IsNullOrEmpty(domain))
{
// unhosted accounts should accept this header
wreq.Headers.Add("Authorization", string.Format("GoogleLogin auth={0}", authToken));
}
else
{
wreq.Headers.Add("Cookie",
string.Format("WRITELYH={0}={1}",
domain,
authToken
)
);
}
}
wreq.KeepAlive = true;
var wresp = wreq.GetResponse();
var stream1 = wresp.GetResponseStream();
var cl = wresp.ContentLength;
StreamReader sr = new StreamReader(stream1);
var resp = sr.ReadToEnd();
return resp;
}