5

So I've created this nice little lambda, which runs great locally, however not so much when actually out in the wild.

The lambda takes an event, with html in the event source, converts that html to a PDF (using the html-pdf node module), passes that pdf to an s3 bucket, and then hands back a signed url that expires in 60 seconds.

Or at least that is what ought to happen (again, works locally). When testing on Lambda, I get the following error:

{
   "errorMessage": "spawn EACCES",
   "errorType": "Error",
   "stackTrace": [
       "exports._errnoException (util.js:870:11)",
       "ChildProcess.spawn (internal/child_process.js:298:11)",
       "Object.exports.spawn (child_process.js:362:9)",
       "PDF.PdfExec [as exec] (/var/task/node_modules/html-pdf/lib/pdf.js:87:28)",
       "PDF.PdfToFile [as toFile] (/var/task/node_modules/html-pdf/lib/pdf.js:83:8)",
       "/var/task/index.js:72:43",
       "Promise._execute (/var/task/node_modules/bluebird/js/release/debuggability.js:272:9)",
       "Promise._resolveFromExecutor (/var/task/node_modules/bluebird/js/release/promise.js:473:18)",
       "new Promise   (/var/task/node_modules/bluebird/js/release/promise.js:77:14)",
       "createPDF (/var/task/index.js:71:19)",
       "main (/var/task/index.js:50:5)"
  ]
}

Here's the code itself (not compiled, there's a handy gulp task for that)

if(typeof regeneratorRuntime === 'undefined') {
    require("babel/polyfill")
}

import fs from 'fs'
import pdf from 'html-pdf'
import md5 from 'md5'
import AWS from 'aws-sdk'
import Promise from 'bluebird'
import moment from 'moment'

const tempDir = '/tmp'
const config = require('./config')
const s3 = new AWS.S3()

export const main = (event, context) => {
    console.log("Got event: ", event)

    AWS.config.update({
        accessKeyId: config.awsKey,
        secretAccessKey: config.awsSecret,
        region: 'us-east-1'
    })

    const filename = md5(event.html) + ".pdf"

    createPDF(event.html, filename).then(function(result) {
        uploadToS3(filename, result.filename).then(function(result) {
            getOneTimeUrl(filename).then(function(result) {
                return context.succeed(result)
            }, function(err) {
                console.log(err)
                return context.fail(err)
            })
        }, function(err) {
            console.log(err)
            return context.fail(err)
        })
    }, function(err) {
        console.log(err)
        return context.fail(err)
    })
}

const createPDF = (html, filename) => {
    console.log("Creating PDF")
    var promise = new Promise(function(resolve, reject) {
        pdf.create(html).toFile(filename, function(err, res) {
            if (err) {
                reject(err)
            } else {
                resolve(res)
            }
        })
    })
    return promise
}

const uploadToS3 = (filename, filePath) => {
    console.log("Pushing to S3")
    var promise = new Promise(function(resolve, reject) {

        var fileToUpload = fs.createReadStream(filePath)
        var expiryDate = moment().add(1, 'm').toDate()

        var uploadParams = {
            Bucket: config.pdfBucket,
            Key: filename,
            Body: fileToUpload
        }

        s3.upload(uploadParams, function(err, data) {
            if(err) {
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
    return promise
}

const getOneTimeUrl = (filename) => {
    var promise = new Promise(function(resolve, reject) {
        var params = {
            Bucket: config.pdfBucket,
            Key: filename,
            Expires: 60
        }

        s3.getSignedUrl('getObject', params, function(err, url) {
            if (err) {
                reject(err)
            } else {
                resolve(url)
            }
        })
    })
    return promise
}

Seems like a problem within html-pdf. I thought it might be a problem with PhantomJS (which html-pdf depends on) due to some reading I did here: https://engineering.fundingcircle.com/blog/2015/04/09/aws-lambda-for-great-victory/ , however, since Lambda has bumped the max zip size to 50mb, I don't have a problem uploading the binary.

Any thoughts?

wvm2008
  • 2,864
  • 4
  • 21
  • 25

1 Answers1

4

html-pdf uses phantomjs under the hood, which needs to compile some binaries when being installed. I guess your problem is that you are deploying those locally compiled binaries but Lambda needs the binaries compiled on Amazon Linux.

You can solve this problem by building your deploy package on an EC2 instance that is running Amazon Linux and then e.g. directly deploy it from there like it is explained in this tutorial.

Also check out this answer on a similar problem.

Community
  • 1
  • 1
birnbaum
  • 4,718
  • 28
  • 37
  • Trying this out, will accept once I'm in the clear, thanks for the clarity friend – wvm2008 May 08 '16 at 04:13
  • well, in the end that didn't work...I was able to build my code on ec2 and deploy ok, but I end up with the exact same error...looking in the node_modules of html-pdf, it's using phantomjs_prebuilt, think that could have something to do with it? – wvm2008 May 09 '16 at 16:23
  • Is the code working on EC2? You might have to call `npm rebuild` when using phantomjs-prebuilt. – birnbaum May 09 '16 at 16:26
  • Yes, it runs as expected on ec2 (when I run the dist/ code using lambda-local) – wvm2008 May 09 '16 at 18:07
  • Has the binary it's executable flag set? This is the main reason for `EACCES` errors. If your code works on Amazon Linux and you are deploying the exact same code, it should really behave the same on Lambda. – birnbaum May 09 '16 at 18:23
  • Yes...executable flag is set as well...*head against wall* – wvm2008 May 09 '16 at 19:14
  • Then the last thing I could think of is that PhantomJS is producing some output and is trying to write it somewhere else than in the /tmp folder. You only have write access to this folder on Lambda – birnbaum May 10 '16 at 07:50
  • 2
    I was able to run it down, finally. Phantomjs-prebuilt dynamically pulls in whichever phantomjs library is considered to be 'ideal' at runtime, rather than statically compiling the binary when I run npm install or npm rebuild. So even though I had the executable flag set, the binary that eventually gets loaded in, I didn't have access to (read: permission). Workaround was statically compiling my own phantomjs library on aws linux, then uploading that to an s3 bucket, then telling html-pdf to use that statically compiled version of phantom rather than finding its own...thanks for all your help! – wvm2008 May 10 '16 at 14:56
  • I am in the same boat - can you describe in more detail how you statically compile phantomjs and tell html-pdf to use that version instead? – DenverCoder9 Jul 20 '17 at 06:36