corsblobgoogle-photos

Download image blob from Google Photos using JavaScript and REST API


I cannot download an image blob in my JavaScript client using the Google Photos REST API. My XMLHttpRequest is getting a 404. I have a valid OAuth token and can list the mediaItems. The token was generated using an offline access code. The same XMLHttpRequest download method works with both GDrive and Dropbox, allowing me to show download progress.

I've tried using the "=d" and "=w123h345-c" baseUrl suffixes (where 123 and 345 are the respective width and height of the image). I've tried using both fetch and XMLHttpRequest with various combinations of credentialed access (e.g. using access_token as a URL param or in the Authorization header.

Note that the same URL works fine in the Chrome URL bar, either downloading the file or showing the full-res version. I can also download the file from the command line using curl with the URL passing the access_token as a parameter. If I send the curl command using the OPTIONS setup, I do NOT get Access-Control-Allow-Origin in the response:

curl --verbose --output foo -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: X-Requested-With" \
-X OPTIONS \
"https://lh3.googleusercontent.com/lr/<baseUrl>=d?access_token=<access_token>"

Returns the following response headers:

< HTTP/2 200 
< access-control-expose-headers: Content-Length
< etag: "v3d"
< expires: Fri, 01 Jan 1990 00:00:00 GMT
< cache-control: private, max-age=86400, no-transform
< content-disposition: attachment;filename="MA-Distancing.png"
< content-type: image/png
< vary: Origin
< x-content-type-options: nosniff
< date: Thu, 06 Aug 2020 20:06:22 GMT
< server: fife
< content-length: 772787
< x-xss-protection: 0
< alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
< 

I've found a similar SO question with no answers.

All of my testing has been on localhost:3000 since I do not yet have OAuth validation for the photoslibrary.readonly scope. I get the "Advanced" auth popup during testing, as expected, and the access_token is valid and can be used to list and get mediaItems, just not with the media downloads.

export const xhrDownloadURL = (url, accessToken, body, onProgress) => new Promise((resolve, reject) => {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', url)
  if (accessToken) xhr.setRequestHeader('Authorization', `Bearer ${accessToken}`)
  xhr.setRequestHeader('Access-Control-Allow-Credentials', true)
  xhr.responseType = 'blob'
  const updateProgress = e => {
    if (onProgress) {
      const { loaded, total } = e
      const cancel = onProgress({ loaded, size: total })
      if (cancel.type === CACHE_CANCELED) {
        reject(new Error(`onProgress canceled download at range ${loaded} of ${total} in ${url}`))
      }
    }
  }
  xhr.onloadstart = updateProgress
  xhr.onprogress = updateProgress
  xhr.onabort = event => {
    console.warn(`xhr ${url}: download aborted at ${event.loaded} of ${event.total}`)
    reject(new Error('Download aborted'))
  }
  xhr.onerror = event => {
    console.error(`xhr ${url}: download error at ${event.loaded} of ${event.total}`)
    reject(new Error('Error downloading file'))
  }
  xhr.onload = event => {
    const { loaded, total } = event
    if (onProgress) onProgress({ loaded, size: total })
    const data = process.env.NODE_ENV === 'test' && Array.isArray(xhr.response) && xhr.response.length === 1 ? xhr.response[0] : xhr.response
    resolve(data)
  }
  xhr.onloadend = (/* event */) => {
    // console.log(`xhr ${url}: download of ${event.total} completed`)
  }
  xhr.ontimeout = event => {
    console.warn(`xhr ${url}: download timeout after ${event.loaded} of ${event.total}`)
    reject(new Error('Timout downloading file'))
  }
  xhr.send(body)
})

Solution

  • It looks like the Google Photos endpoints do not support CORS access to pixel data.

    https://issuetracker.google.com/issues/118662029

    Again, it is fine to use curl or <img> tags to display the pixels. You just cannot get the data in the client JS until CORS access is added.