Zelf
Zelf

Reputation: 2250

Lambda function s3.getObject returns "Internal server error"

This code works just fine locally using nodejs. Images download from s3, write to file.

However, in Lambda (using nodejs 8.10) I'm getting "Internal Server Error" when testing the function with this in the Logs:

"Execution failed due to configuration error: Malformed Lambda proxy response"

I am using the lambda proxy response in the callback, but clearly some AWS SDK error with S3 is not getting caught.

I do have a role setup with S3 full access that the Lambda has access to.

What am I missing with my first Lambda function? Docs and tutorials I've followed correctly and it is not working.

const async = require('async')
const aws = require('aws-sdk')
const fs = require('fs')
const exec = require('child_process').exec

const bucket = 'mybucket'
const s3Src = 'bucket_prefix'
const s3Dst = 'new_prefix'
const local = `${__dirname}/local/`
aws.config.region = 'us-west-2'
const s3 = new aws.S3()

exports.handler = async (event, context, callback) => {
    const outputImage = 'hello_world.png'
    const rack = JSON.parse(event.body)
    const images = my.images

    async.waterfall([
            function download(next) {
                let downloaded = 0
                let errors = false
                let errorMessages = []

                for (let i = 0; i < images.length; i++) {
                    let key = `${s3Src}/${images[i].prefix}/${images[i].image}`,
                        localImage = `${local}${images[i].image}`

                    getBucketObject(bucket, key, localImage).then(() => {
                        ++downloaded
                    if (downloaded === images.length) { // js is non blocking, need to check if all images have been downloaded. If so, then go to next function
                        if (errors) {
                            next(errorMessages.join(' '))
                        } else {
                            next(null)
                        }
                    }
                }).catch(error => {
                    errorMessages.push(`${error} - ${localImage}`)
                    ++downloaded
                    errors = true
                })
            }

            function getBucketObject(bucket, key, dest) {
                return new Promise((resolve, reject) => {
                    let ws = fs.createWriteStream(dest)

                    ws.once('error', (err) => {
                        return reject(err)
                    })

                    ws.once('finish', () => {
                        return resolve(dest)
                    })

                    let s3Stream = s3.getObject({
                        Bucket: bucket,
                        Key: key
                    }).createReadStream()

                    s3Stream.pause() // Under load this will prevent first few bytes from being lost

                    s3Stream.on('error', (err) => {
                        return reject(err)
                    })

                    s3Stream.pipe(ws)
                    s3Stream.resume()
                })
            }
        }
    ], err => {
        if (err) {
            let response = {
                "statusCode": 400,
                "headers": {
                    "my_header": "my_value"
                },
                "body": JSON.stringify(err),
                "isBase64Encoded": false
            }
            callback(null, response)
        } else {
            let response = {
                "statusCode": 200,
                "headers": {
                    "my_header": "my_value"
                },
                "body": JSON.stringify(`<img src="${local}${outputImage}" />`),
                "isBase64Encoded": false
            }
            callback(null, response)
        }
    }
)

}

Upvotes: 0

Views: 617

Answers (2)

Zelf
Zelf

Reputation: 2250

Locally, I've been running nodejs 10.10 and lambda currently is at 8.10. That is a big part I'm sure. In the end I had to remove the async. I had to move the getBucketObject function out of the waterfall. Once I made those adjustments it started working. And another issue was the downloaded images needed to go into "/tmp" directory.

const aws = require('aws-sdk')
const async = require('async')
const fs = require('fs')

const bucket = 'mybucket'
const s3Src = 'mys3src'
const local = '/tmp/'
aws.config.region = 'us-west-2'
const s3 = new aws.S3()

exports.handler = (event, context, callback) => {
    const outputImage = 'hello_world.png'

    async.waterfall([
            function download(next) {
                let downloaded = 0,
                    errorMessages = []

                for (let i = 0; i < event['images'].length; i++) {
                    let key = `${s3Src}/${event['images'][i]['prefix']}/${event['images'][i]['image']}`,
                        localImage = `${local}${event['images'][i]['image']}`

                    getBucketObject(bucket, key, localImage).then(() => {
                        downloaded++

                        if (downloaded === event['images'].length) {
                            if (errorMessages.length > 0) {
                                next(errorMessages.join(' '))
                            } else {
                                console.log('All downloaded')
                                next(null)
                            }
                        }
                    }).catch(error => {
                        downloaded++
                        errorMessages.push(`${error} - ${localImage}`)

                        if (downloaded === event['images'].length) {
                            next(errorMessages.join(' '))
                        }
                    })
                }
            }
        ], err => {
            if (err) {
                console.error(err)
                callback(null, {
                    "statusCode": 400,
                    "body": JSON.stringify(err),
                    "isBase64Encoded": false
                })
            } else {
                console.log('event image created!')
                callback(null, {
                    "statusCode": 200,
                    "body": JSON.stringify(`<img src="${local}${outputImage}" />`),
                    "isBase64Encoded": false
                })
            }
        }
    )
}

function getBucketObject(bucket, key, dest) {
    return new Promise((resolve, reject) => {
        let ws = fs.createWriteStream(dest)

        ws.once('error', (err) => {
            return reject(err)
        })

        ws.once('finish', () => {
            return resolve(dest)
        })

        let s3Stream = s3.getObject({
            Bucket: bucket,
            Key: key
        }).createReadStream()

        s3Stream.pause() // Under load this will prevent first few bytes from being lost

        s3Stream.on('error', (err) => {
            return reject(err)
        })

        s3Stream.pipe(ws)
        s3Stream.resume()
    })
}

Upvotes: 0

michail_w
michail_w

Reputation: 4471

Response should be always sent to callback function. Your code sends response only on error. That's why Lambda executor thinks your code fails.

BTW - should your functions in async.waterfall be separated with coma, as two tasks?

Upvotes: 1

Related Questions