How to cors-proxy an image using a Netlify Function

site is https://unruffled-jang-4961e9.netlify.app/

This aggregates a bunch of feeds including a bunch of images hosted on various other sites. Some of those images come through fine using a naive fetch from the frontend, but others run into CORS issues along the way, so trying to set up a proxy.

Carefully followed through this article Solve CORS once and for all with Netlify Dev

I got that to work as far as how it’s proxying some JSON.

But in this case I just have binary data coming back. I got slightly different results when setting the isBase64Encoded property on the function handler’s result.

Seems like I can base64 encode the original response data and return that instead of setting isBase64Encoded true.

I’m not sure it makes much difference, though.

On the frontend, I found one example showing how to set an img by response data, and in that case the author used this data,

  const testByteArray = new Uint8Array([
    137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,8,0,0,
    0,8,8,2,0,0,0,75,109,41,220,0,0,0,34,73,68,65,84,8,215,99,120,
    173,168,135,21,49,0,241,255,15,90,104,8,33,129,83,7,97,163,136,
    214,129,93,2,43,2,0,181,31,90,179,225,252,176,37,0,0,0,0,73,69,
    78,68,174,66,96,130])

That works fine when substituted for the live function response. Something about the encoding I’m not tracking correctly while processing it on the frontend.

For the netlify function I have:

    const response = await fetch(foreignUrl)

    if (!response.ok) {
      return {
        statusCode: 502,
        body: 'Bad Gateway'
      }
    }
    else {
      const buffer = await response.buffer()
      return {
        body: buffer.toString('base64'),
        isBase64Encoded: true,
        statusCode: 200,
      }
    }

Then in the frontend I have:

  return axios.get(proxyUrl).then(
    response => {
      const data = response.data
      console.log('data', data)

      // const testByteArray = new Uint8Array([
      //   137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,8,0,0,
      //   0,8,8,2,0,0,0,75,109,41,220,0,0,0,34,73,68,65,84,8,215,99,120,
      //   173,168,135,21,49,0,241,255,15,90,104,8,33,129,83,7,97,163,136,
      //   214,129,93,2,43,2,0,181,31,90,179,225,252,176,37,0,0,0,0,73,69,
      //   78,68,174,66,96,130])

      const byteArray = (new TextEncoder()).encode(data)
      console.log(byteArray)

      return new Blob([byteArray], {type: 'image/png'})
    }

The dump of the response.data looks like e.g. �PNGIHDR���)�tEXtSoftware Adobe Image Readyq�e< ... (lots of characters don’t copy into here but looks like this)

The dump of the byteArray looks like e.g. Uint8Array(428297) [239, 191, 189, 80, 78, 71, 13, 10, ...

I get a broken <img/> when setting the img with this src={URL.createObjectURL(blob)}.

However like I said if I switch out the testByteArray I get the test image showing up no problem.

So something in all this encoding/decoding utf-8, etc, has gone wrong, although it seems like it’s almost there.

Hey @Nick_Vantzos,
If you end up with a Uint8Array at this step:

...
const byteArray = (new TextEncoder()).encode(data)
console.log(byteArray) <---- this is Uint8Array
...

then you should be able to get to an image from there. So I would think that the issue is coming in at the next line:

return new Blob([byteArray], {type: 'image/png'})

You said “if I switch out the testByteArray I get the test image showing up no problem.”- where would you switch that out that would get it to work?

In the meantime, sharing this site I made a few years ago to help me sort through some similar questions, maybe it’ll be useful to you too :slight_smile: Glitch :・゚✧ / live site: https://base64-is-yr-friend.glitch.me/

You said “if I switch out the testByteArray I get the test image showing up no problem.”- where would you switch that out that would get it to work?

At the final line of the “frontend” code block copied above, like,

return new Blob(
    // [byteArray],
    // {
    //   type: response.headers['content-type'],
    // }
    [testByteArray], { type: 'image/png' }
  )

That works every time.

In the meantime, sharing this site …

Thanks, will take a look at that!

That site is pretty cool. Yeah, those ��� are hard to see as my friend atm.

I did find at the wikipedia on PNG it says the magic number at the beginning should be

hex  89   50  4e  47  0d  0a  1a  0a
dec  137  80  78  71  13  10  26  10

One of my live test images is actually PNG I think, and when I dump the Uint8Array it begins with

0: 239
1: 191
2: 189
3: 80
4: 78
5: 71
6: 13
7: 10
8: 26
9: 10

That’s mysterious, the 1st byte is not there and instead the first few bytes are something else. That dump is produced on the frontend.

I’m not sure how to get any logs from the Netlify function, not the main problem but an obstacle.

The function code is currently

      const contentType = response.headers.get('content-type')

      const buffer = await response.buffer()
      const bufferFileType = JSON.stringify(await FileType.fromBuffer(buffer))
      return {
        body: buffer.toString('base64'),
        headers: {
          'content-type': contentType,
          'x-file-type': bufferFileType,
        },
        isBase64Encoded: true,
        statusCode: 200,
      }

In the case of this same example PNG (with the odd 3 bytes at the beginning), I get back in the response headers

content-type: image/png
x-file-type: {"ext":"png","mime":"image/png"}

Perhaps there’s another kind of PNG magic number and this is not the problem, since it’s detected as PNG by the file-type lib as seen in the x-file-type header I put in. I’m using that x-file-type instead of console.log, just to check this aspect.

Or perhaps something has messed with the body content after FileType.fromBuffer function has run. Could even be in axios on the frontend.

1 Like

The 3 weird bytes are ef bf bd, and at this stackoverflow post, where the problem was a buffer messed up by ef bf bd, they said

The original byte array is not encoded as UTF-8. The StreamReader therefore replaces each invalid byte with the replacement character U+FFFD. When that character gets encoded back to UTF-8, this results in the byte sequence EF BF BD. You cannot construct the original byte value from the string because the information is completely lost.

I can see the PNG image ok in the network tab preview in Chrome, so perhaps this is axios reading the buffer in a misconfigured way… Something concrete to look for at least. :sweat_smile:

Edit I found with npx netlify dev I can get the console.log from the function in the shell foreground :man_facepalming:

With (in the function)

      const buffer = await response.buffer()
      const bufferFileType = JSON.stringify(
            await FileType.fromBuffer(buffer))
      const base64String = buffer.toString('base64')
      console.log('base64String', base64String.substring(0, 11))
      return {
        body: base64String,
        headers: {
          'content-type': contentType,
          'x-file-type': bufferFileType,
        },
        isBase64Encoded: true,
        statusCode: 200,
      }

I see it print base64String iVBORw0KGgo, that looks like the correct magic number, using an online base64-to-hex app.

Back on the frontend, I have

  axios.get(proxyUrl).then(
    response => {
      const data = response.data

      const charCodes = new Array(data.length);
      for (let i = 0; i < data.length; i++) {
        charCodes[i] = data.charCodeAt(i);
      }
      console.log('char codes', charCodes.slice(0, 8))

and I see in the console char codes (8) [65533, 80, 78, 71, 13, 10, 26, 10]

1 Like

Solved, I hope, seems to work right with axios option responseType: blob, then the frontend code is simplified reassuringly to

    axios.get(proxyUrl, { responseType: 'blob' }).then(
        response => response.data)

that returns a blob I can set on the <img/> given the above code in the netlify/lambda function :tada:

1 Like

What an awesome write up, thanks so much for reporting your fix!