Solved

FME Cloud API return Zip file

  • 5 February 2018
  • 4 replies
  • 4 views

Badge +10

Hi,

I have some JSON GET requests working AWS API Gateway and an FME backend thanks to @GerhardAtSafe and @stewartharper for the youtube videos and github repo. The next step is to offer up zip files. The workbench streams a small zip file and I am now trying to get this working through the AWS API Gateway. I have added application/zip for the content type and got the gateway to support application/zip binary. When I try to make the call though I only get an empty zip file - although the FME job does run successfully and a file is created.

I suspect it is because the body mapping is incorrect. There is some information for images here:

https://aws.amazon.com/blogs/compute/binary-support-for-api-integrations-with-amazon-api-gateway/

Does anyone know what I should put in the body mapping template for the integration response?

Many Thanks,

Oliver

icon

Best answer by oliver.morris 21 February 2018, 22:09

View original

4 replies

Badge +10

Looks now like the lambda function is the blocker, it currently is only configured to send JSON content and not binary. We have made some amendments to the function but currently it isn't working at it is a very tricky beast to debug directly in lambda. 

The code is below but ideally if there is anyway to test this directly in lamba to see where it is faulty but we haven't worked out what we need to pass to test.

Any suggestions are welcome, thanks

console.log('Loading function');
var https = require('https');
var querystring = require('querystring');
var host = '[xx].fmecloud.com';
var path = '/fmedatastreaming/APIrepository/';
var dataZIPObj = false;  
/*
* Handles the HTTP requests to FME Cloud.
*/
var makeHttpRequest = function (httpMethod, event, callback) {
    var dataJsonObj = '';
    var qs = querystring.stringify(event.params.querystring)
    var paths = querystring.stringify(event.params.path);
var statusCode;
var parsed;
    
    var options = {
      hostname: host,
      port: 443,
      path: path + event.workspace + '?' + qs + '&' + paths,
      method: httpMethod
    };

    var req = https.request(options, function(response){

        //Trigger an error response if the ajax request fails
        response.on('error', function(d) {
            console.log("error");
            if (callback) {
                callback({
                    statusCode: response.statusCode
                });
            }
        });
        
        //Populate the JSON object as data comes in.
        response.on('data', function(d) {
            if (response.headers['content-type'] === 'application/json') {
                dataJsonObj += d;
            }
            if (response.headers['content-type'] === 'application/zip') {
                dataZIPObj = true;
            }
        });
        
        //Populate the callback object with the json response and status code.
        response.on('end', function(d) {

            //Check to see if there is any data returned from the request
            if(dataJsonObj === "" && dataZIPObj === false){
                if (callback) {
                    callback({
                        body: { "status": response.statusCode, "message": response.statusMessage},
                        statusCode: response.statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }else if(dataZIPObj){

                parsed = d;
if(parsed.status){
                    //Use status code defined in JSON
                    statusCode = parsed.status;
}
}
else {
                
                //JSON was returned in the request, process.
                parsed = JSON.parse(dataJsonObj);
                
                //FME can return a 200 HTTP status but send a warning error back, this
                //overrides the status code on the request object with the status code
                //in the JSON from FME.
              
                if(parsed.status){
                    //Use status code defined in JSON
                    statusCode = parsed.status;
                }else{
                    //Default to HTTP status code returned by request    
                    statusCode = response.statusCode;
                }
                
                if (callback) {
                    callback({
                        body: parsed,
                        statusCode: statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }
        });
      return response;
    });
    req.end();
};


/*
* Handler that gets called by the API gateway.
*/
exports.handler = function(event, context) {
    try {
        var httpMethod = event.context["http-method"];
        makeHttpRequest(httpMethod, event, function (response) {
if (!dataZIPObj)
            return context.fail(JSON.stringify(response.body));
else 
return response.body;
        });
    } catch (e) {
        context.fail(e);
    }
};
Badge

Looks now like the lambda function is the blocker, it currently is only configured to send JSON content and not binary. We have made some amendments to the function but currently it isn't working at it is a very tricky beast to debug directly in lambda. 

The code is below but ideally if there is anyway to test this directly in lamba to see where it is faulty but we haven't worked out what we need to pass to test.

Any suggestions are welcome, thanks

console.log('Loading function');
var https = require('https');
var querystring = require('querystring');
var host = '[xx].fmecloud.com';
var path = '/fmedatastreaming/APIrepository/';
var dataZIPObj = false;  
/*
* Handles the HTTP requests to FME Cloud.
*/
var makeHttpRequest = function (httpMethod, event, callback) {
    var dataJsonObj = '';
    var qs = querystring.stringify(event.params.querystring)
    var paths = querystring.stringify(event.params.path);
var statusCode;
var parsed;
    
    var options = {
      hostname: host,
      port: 443,
      path: path + event.workspace + '?' + qs + '&' + paths,
      method: httpMethod
    };

    var req = https.request(options, function(response){

        //Trigger an error response if the ajax request fails
        response.on('error', function(d) {
            console.log("error");
            if (callback) {
                callback({
                    statusCode: response.statusCode
                });
            }
        });
        
        //Populate the JSON object as data comes in.
        response.on('data', function(d) {
            if (response.headers['content-type'] === 'application/json') {
                dataJsonObj += d;
            }
            if (response.headers['content-type'] === 'application/zip') {
                dataZIPObj = true;
            }
        });
        
        //Populate the callback object with the json response and status code.
        response.on('end', function(d) {

            //Check to see if there is any data returned from the request
            if(dataJsonObj === "" && dataZIPObj === false){
                if (callback) {
                    callback({
                        body: { "status": response.statusCode, "message": response.statusMessage},
                        statusCode: response.statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }else if(dataZIPObj){

                parsed = d;
if(parsed.status){
                    //Use status code defined in JSON
                    statusCode = parsed.status;
}
}
else {
                
                //JSON was returned in the request, process.
                parsed = JSON.parse(dataJsonObj);
                
                //FME can return a 200 HTTP status but send a warning error back, this
                //overrides the status code on the request object with the status code
                //in the JSON from FME.
              
                if(parsed.status){
                    //Use status code defined in JSON
                    statusCode = parsed.status;
                }else{
                    //Default to HTTP status code returned by request    
                    statusCode = response.statusCode;
                }
                
                if (callback) {
                    callback({
                        body: parsed,
                        statusCode: statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }
        });
      return response;
    });
    req.end();
};


/*
* Handler that gets called by the API gateway.
*/
exports.handler = function(event, context) {
    try {
        var httpMethod = event.context["http-method"];
        makeHttpRequest(httpMethod, event, function (response) {
if (!dataZIPObj)
            return context.fail(JSON.stringify(response.body));
else 
return response.body;
        });
    } catch (e) {
        context.fail(e);
    }
};
Maybe start with an image rather than a zip and get that working. As defined here. Content-type is determined in FME by the first writer added to a workspace. For example, if a Google KML writer is added to a workspace, followed by an Adobe 3D PDF writer, the Data Streaming service sends content-type application/vnd.google-earth.kmz. If a PDF writer is added first, followed by an OGCKML writer, the content-type is application/pdf. 

 

 

So maybe you just create a really simple workspace that streams a png then you know the content type will be image/png?

 

Badge +10

For others in a similar situation - you would like to return a zip file / image anything other than JSON really here is what you need to do.

Firstly lambda wont let you stream directly from fme because it tries to chunk the data and then the API gateway can't put it back properly such then when it encodes to binary it fails (most of the time). So the work around I found was basically to save whatever file it is to S3, then pick it up in lambda and sent it to API Gateway. 

The lambda code needs to change to something like:

console.log("Lambda Function working");
var AWS = require("aws-sdk");
var https = require('https');
var querystring = require('querystring');
var host = 'xxx';
var path = '/fmedatastreaming/APIrepository/';
var S3Key = 'xx';
var S3SecretKey = 'xx';


var isJSON = false;
var isZIP = false;




var makeRequestToFME = function (httpMethod, event, callback) {
    var qs = querystring.stringify(event.params.querystring);
    var token = querystring.stringify(event["stage-variables"]);
    var statusCode;
    var parsed;
    var fmeResponse = '';
    var parsedResponse;
    var options = {
        hostname: host,
        port: 443,
        path: path + event.workspace + '?' + qs + '&' + token ,
        timeout: 20000, 
        method: httpMethod
    };
    console.log("Options", options);
    var req = https.request(options, function (response) {
        response.on('error', function (d) {
            console.log("FME Error", d);
            if (callback) {
                callback({
                    statusCode: response.statusCode
                });
            }
        });
        response.on('data', function (returnData) {
            console.log("Headers", response.headers);
            if (response.headers['content-type'] === 'application/json') {
                isJSON = true;
                fmeResponse += returnData;               
            }
            if (response.headers['content-type'] === 'application/zip') {
                isZIP = true;
                fmeResponse += returnData;               
            }
        });
        response.on('end', function (d) {
            console.log('FME Response end', fmeResponse);  
            parsed = JSON.parse(fmeResponse);
            
            if ( parsed  != null && (parsed.status !== undefined && parsed.filename !== undefined)){
                isZIP = true;
            }
            
            if (isZIP){
                if (fmeResponse==='') {
                    if (callback) {
                        callback({
                            body: { "status": response.statusCode, "message": response.statusMessage },
                            statusCode: response.statusCode,
                            statusMessage: response.statusMessage
                        });
                    }
                }
                else {
                    console.log('FME Response end', fmeResponse);
                    parsed = JSON.parse(fmeResponse);
                    
                    if (callback) {
                        callback({
                            body: parsed.filename,
                            statusCode: parsed.status,
                            statusMessage: 'Reply from FME '+parsed.status
                        });
                    }
                }
            }
            else if (isJSON){
                parsed = JSON.parse(fmeResponse);
                if (parsed.status) {
                    //Use status code defined in JSON
                    statusCode = parsed.status;
                } else {
                    //Default to HTTP status code returned by request    
                    statusCode = response.statusCode;
                }
                if (callback) {
                    callback({
                        body: parsed,
                        statusCode: statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }
        });
        console.log("End makeFMERequest", response);
        return response;
    });     
    console.log("Request gone");
    req.end();
};
exports.handler = (event, context, callback) => {           
    try {       
        var httpMethod = event.context["http-method"];
        makeRequestToFME(httpMethod, event, function (response) {             
            console.log('makeFMERequest done', response);


            if (response.body != null && response.statusCode === 'ok') {
                var s3 = new AWS.S3({ region: "us-east-1", accessKeyId: S3Key, secretAccessKey: S3SecretKey });
                s3.getObject({
                    Bucket: 'xxxxx',
                    Key: 'DOWNLOAD/' + response.body
                }, function (err, data) {
                    if (err) {
                        console.log("S3", err);
                        callback(err);
                    }
                    else {
                        console.log("ZIP", data.Body);
                        callback(null, data.Body.toString('base64'));
                    }
                });
            }
            else {
                console.log('context failed', response);
                return context.fail(JSON.stringify(response.body));
            }
        }); 
    }
    catch (e) {
        console.log('try catch', e);
        context.fail(e);
    }   
};


 And in the API Gateway, under settings and Binary Media Types add the MIME type e.g. application/zip

Then in the method response add content-type

0684Q00000ArKyxQAF.png

and in the integration response pull it into binary and update the header mappings as below

0684Q00000ArKrkQAF.png

Then when making the curl request - in say Postman be sure to request the data in the header as content-type / application-zip.

 

I hope this helps someone, AWS Gateway and lambda integration is quite a tricky piece, FME is the easiest bit.

Cheers

Oliver

Badge

For others in a similar situation - you would like to return a zip file / image anything other than JSON really here is what you need to do.

Firstly lambda wont let you stream directly from fme because it tries to chunk the data and then the API gateway can't put it back properly such then when it encodes to binary it fails (most of the time). So the work around I found was basically to save whatever file it is to S3, then pick it up in lambda and sent it to API Gateway. 

The lambda code needs to change to something like:

console.log("Lambda Function working");
var AWS = require("aws-sdk");
var https = require('https');
var querystring = require('querystring');
var host = 'xxx';
var path = '/fmedatastreaming/APIrepository/';
var S3Key = 'xx';
var S3SecretKey = 'xx';


var isJSON = false;
var isZIP = false;




var makeRequestToFME = function (httpMethod, event, callback) {
    var qs = querystring.stringify(event.params.querystring);
    var token = querystring.stringify(event["stage-variables"]);
    var statusCode;
    var parsed;
    var fmeResponse = '';
    var parsedResponse;
    var options = {
        hostname: host,
        port: 443,
        path: path + event.workspace + '?' + qs + '&' + token ,
        timeout: 20000, 
        method: httpMethod
    };
    console.log("Options", options);
    var req = https.request(options, function (response) {
        response.on('error', function (d) {
            console.log("FME Error", d);
            if (callback) {
                callback({
                    statusCode: response.statusCode
                });
            }
        });
        response.on('data', function (returnData) {
            console.log("Headers", response.headers);
            if (response.headers['content-type'] === 'application/json') {
                isJSON = true;
                fmeResponse += returnData;               
            }
            if (response.headers['content-type'] === 'application/zip') {
                isZIP = true;
                fmeResponse += returnData;               
            }
        });
        response.on('end', function (d) {
            console.log('FME Response end', fmeResponse);  
            parsed = JSON.parse(fmeResponse);
            
            if ( parsed  != null && (parsed.status !== undefined && parsed.filename !== undefined)){
                isZIP = true;
            }
            
            if (isZIP){
                if (fmeResponse==='') {
                    if (callback) {
                        callback({
                            body: { "status": response.statusCode, "message": response.statusMessage },
                            statusCode: response.statusCode,
                            statusMessage: response.statusMessage
                        });
                    }
                }
                else {
                    console.log('FME Response end', fmeResponse);
                    parsed = JSON.parse(fmeResponse);
                    
                    if (callback) {
                        callback({
                            body: parsed.filename,
                            statusCode: parsed.status,
                            statusMessage: 'Reply from FME '+parsed.status
                        });
                    }
                }
            }
            else if (isJSON){
                parsed = JSON.parse(fmeResponse);
                if (parsed.status) {
                    //Use status code defined in JSON
                    statusCode = parsed.status;
                } else {
                    //Default to HTTP status code returned by request    
                    statusCode = response.statusCode;
                }
                if (callback) {
                    callback({
                        body: parsed,
                        statusCode: statusCode,
                        statusMessage: response.statusMessage
                    });
                }
            }
        });
        console.log("End makeFMERequest", response);
        return response;
    });     
    console.log("Request gone");
    req.end();
};
exports.handler = (event, context, callback) => {           
    try {       
        var httpMethod = event.context["http-method"];
        makeRequestToFME(httpMethod, event, function (response) {             
            console.log('makeFMERequest done', response);


            if (response.body != null && response.statusCode === 'ok') {
                var s3 = new AWS.S3({ region: "us-east-1", accessKeyId: S3Key, secretAccessKey: S3SecretKey });
                s3.getObject({
                    Bucket: 'xxxxx',
                    Key: 'DOWNLOAD/' + response.body
                }, function (err, data) {
                    if (err) {
                        console.log("S3", err);
                        callback(err);
                    }
                    else {
                        console.log("ZIP", data.Body);
                        callback(null, data.Body.toString('base64'));
                    }
                });
            }
            else {
                console.log('context failed', response);
                return context.fail(JSON.stringify(response.body));
            }
        }); 
    }
    catch (e) {
        console.log('try catch', e);
        context.fail(e);
    }   
};


 And in the API Gateway, under settings and Binary Media Types add the MIME type e.g. application/zip

Then in the method response add content-type

0684Q00000ArKyxQAF.png

and in the integration response pull it into binary and update the header mappings as below

0684Q00000ArKrkQAF.png

Then when making the curl request - in say Postman be sure to request the data in the header as content-type / application-zip.

 

I hope this helps someone, AWS Gateway and lambda integration is quite a tricky piece, FME is the easiest bit.

Cheers

Oliver

Godo job figuring that out! Thanks for posting the answer.

 

 

Reply