6

There are a couple of questions that are similar, but none of the answers have so far worked for me.

I have an AWS Lambda function behind an AWS API Gateway powered by Serverless, the Lambda should be returning a PDF document via:

let responseObj = {
      statusCode: 200,
      isBase64Encoded: true,
      headers: {
        'Content-type': 'application/pdf',
        // 'accept-ranges': 'bytes',
        'Content-Disposition': 'attachment; filename=' + pdfName + '.pdf'
      },
      body: pdfBuffer && pdfBuffer.toString('base64')
    }
    return responseObj;

when I do an console.log() to AWS CloudWatch of pdfBuffer (before base64, it indeed looks like PDF data:

%PDF-1.4
%����
1 0 obj
<</Creator (Chromium)
/Producer (Skia/PDF m90)
...

Yet when I look in postman, I see in my body:

JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9DcmVhdG9yIChDaHJvbWl1bSkKL1Byb2R1Y2VyIChTa2lhL1...

So it's obviously not returning a binary file (my pdf).

Looking at API Gateway, it's been suggested you set Binary Media Types to contain */*.

Now, my API gateway has two end points, when I set it to */*, the PDF serving endpoint does indeed correctly serve my PDF, however I have another endpoint that takes in a body of JSON, and when */* is set under Binary Media Types, it malforms/base64 encodes the JSON input making my CSV endpoint useless.

Setting Binary Media Types to contain application/pdf allows my CSV serving endpoint to work, but my PDF endpoint reverts back to serving up junk data, even when manually setting the Accepts header in postman to application/pdf.

So leaving Binary Media Types as application/pdf, I turn to Resources within the API Gateway UI settings:

Screenshot of API Gateway UI showing the Resources tab selected.

Here i'm a little unsure which to edit. It seems i have two options in the sidebar, one for GET and one for OPTIONS:

Screenshot of API Gateway UI showing "GET - Method Execution" options

Screenshot of API Gateway UI showing "OPTIONS - Method Execution" options

The OPTIONS - Method Execution allows me to edit the Integration Response whereas the GET - Method Execution does not.

When I edit the Integration Response option, and I set Content Handling to Convert to binary (if needed), there appears to be no change in what is returned to me via Postman.

Screenshot of API Gateway UI showing Integration Response selected from OPTIONS - Method Execution

There must be a step or something I am missing. Setting Binary Media Types to contain */* seems like a broken answer. There must be a way to allow certain endpoints to return binary data (like a pdf file) whilst allowing other endpoints to return or accept non binary data.

4
  • Did you have any luck with this? I am running into the same issues trying to serve a PDF file from Lambda/API Gateway Commented May 25, 2021 at 10:10
  • 1
    @GerardvandenBosch i've had to go off the trail to get it to work, and even then it doesn't do exactly what i want it to do. You need to remove proxy lambda integration and then you can edit the integration response. but you can only pass back the base64encoded binary file (no headers), otherwise if you try and include headers and such, it seems to malform the binary file. Commented May 25, 2021 at 14:44
  • 1
    @GerardvandenBosch also changing to non-proxied lambda integration will affect the way you pass through data to the lambda. Commented May 25, 2021 at 15:19
  • @GerardvandenBosch see my answer. please add to it (as it's a wiki) if you find better solutions. Commented May 26, 2021 at 10:26

3 Answers 3

4

I'm going to add an answer as a wiki. I think this is something that will change, certainly screenshots will change over time and should be updated as such rather than new answers added.

To enable Binary output from a lambda you have to disable "Use Lambda Proxy Integration" on the Integration Request page:

Screenshot showing the Integration Request settings page of a API Gateway endpoint

By changing this, it will knock out what you'd normally expect in the event object of your Lambda (easily accessible path parameters, body parameters etc), so you'll need to alter the Mapping Templates on the same page of the Integration Request.

For my own needs, I set the Request body passthrough to be When there are no templates defined (recommended) with a Content-Type of: application/pdf and a template of:

##  See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
##  This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload
#set($allParams = $input.params())
{
"body-json" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
"$type" : {
    #foreach($paramName in $params.keySet())
    "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
        #if($foreach.hasNext),#end
    #end
}
    #if($foreach.hasNext),#end
#end
},
"stage-variables" : {
#foreach($key in $stageVariables.keySet())
"$key" : "$util.escapeJavaScript($stageVariables.get($key))"
    #if($foreach.hasNext),#end
#end
},
"requestContext" : {
    "authorizer" : {
#foreach($key in $context.authorizer.keySet())
"$key" : "$util.escapeJavaScript($context.authorizer.get($key))"
    #if($foreach.hasNext),#end
#end
    },
    "account-id" : "$context.identity.accountId",
    "api-id" : "$context.apiId",
    "api-key" : "$context.identity.apiKey",
    "authorizer-principal-id" : "$context.authorizer.principalId",
    "caller" : "$context.identity.caller",
    "cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
    "cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
    "cognito-identity-id" : "$context.identity.cognitoIdentityId",
    "cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
    "http-method" : "$context.httpMethod",
    "stage" : "$context.stage",
    "source-ip" : "$context.identity.sourceIp",
    "user" : "$context.identity.user",
    "user-agent" : "$context.identity.userAgent",
    "user-arn" : "$context.identity.userArn",
    "request-id" : "$context.requestId",
    "resource-id" : "$context.resourceId",
    "resource-path" : "$context.resourcePath"
    }
}

Screenshot of the Mapping Templates on the Integration Request settings page of the API Gateway endpoint

This should now pass through the things you would expect in the event parameter of your Lambda. You might need to change the names within the Mapping Template to match your needs, or within your code in your Lambda.

Your endpoint will still fail to output a PDF correctly though.

We should add a response to the Method Response, for my own purposes, I am adding a 200 response. I add a Response Header of "Content-Type" and a Response Body of "application/pdf" with an Model of "Empty".

Screenshot from the Method Response settings page of the API Gateway endpoint

At this point, if your Lambda is outputting: return pdfBuffer.toString('base64') you should start seeing the correct headers and base64 body within your curl/postman request:

curl -i --output - --location --request GET 'https://example.com/document-generation/soa/pdf/unitresult/1cc1eece-cf4e-e811-80e5-00155d210d92/2a174f81-a055-e511-80c2-00155d220a0c' \
--header 'Content-Type: application/pdf' \
--header 'Accept: application/pdf' \
--header 'Authorization: abc123
HTTP/2 200
content-type: application/pdf
content-length: 32502
date: Wed, 26 May 2021 09:58:15 GMT
x-amzn-requestid: blah blah
x-amz-apigw-id: anonymous
x-amzn-trace-id: all kinds of things
via: other stuff
x-amz-cf-pop: letters
x-cache: Miss from cloudfront
x-amz-cf-pop: letters
x-amz-cf-id: stuff

"JVBERi0xLjQKJdPr6..."

We now need to look at the Integration Response settings page, which is now enabled due to disabling Use Lambda Proxy Integration.

A 200 Method response status should already be setup, expand that and set the "Content handling" to Convert to binary (if needed). Expand Header Mappings and add a Response header of "Content-Type" with a Mapping value of "'application/pdf'".

Screenshot of the Integration Response settings page of an API Gateway endpoint

If you rerun your curl or Postman request, you should now receive your PDF correctly.

If you need to access the PDF from a website on a different domain, you'll need to allow for CORS. To do this, you'll need to add an OPTIONS method to your PDF serving resource.

In the Integration Request for your new OPTIONS method, you'll need to set it to Mock.

In the Method Response for your OPTIONS method, you'll need to add:

  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin

to the Response Headers for 200.

In the Integration Response for your OPTIONS method you'll need to add the following Header Mappings:

  • Response Header: Access-Control-Allow-Headers

  • Mapping value: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'

  • Response Header: Access-Control-Allow-Methods

  • Mapping value: 'OPTIONS, GET'

  • Response Header: Access-Control-Allow-Origin

  • Mapping value: '*'

If your PDF endpoint is using something other than GET. you will need to change the Mapping Value for Access-Control-Allow-Methods to match the method type e.g. 'OPTIONS, POST' if your pdf is served from a POST method.

Sign up to request clarification or add additional context in comments.

4 Comments

It would be helpful to share what the Lambda function is returning and the mapping template in the Integration Response.
This defeats the purpose and makes everything more complicated!
@atefth feel free to add your own answer? this is the answer that worked for me at the time.
Ugh, even if this works, this is horrific. This should really be something API gateway supports with Lambda integration.
0

You can use serverless-apigw-binary plugin in your serverless.yaml file. The gateway binary plugin helps to generate binary types from api gate way.
Try:

plugins: 
   - serverless-apigw-binary

custom:
    apigwBinary:
     types:
      - "application/pdf"

Hope that helps.

2 Comments

That package hasn't seen an update in 6 years =(
The package is not required, you can set Binary Media Types in Serverless core serverless.com/framework/docs/providers/aws/events/…
0

My use case was a little different but I had a similar conflict between wanting the api to accept json on post requests and respond with binary images on certain get requests.

Instead of adding */* to the binary media types which caused the conflict with json I just included text/html and image/* in my binary media types. The text/html allowed the lambda to respond with binary using the proxy integration since it attempts to match on the first accept header which by default in a browser req is text/html. Still needed to make sure the lambda had isBase64Encoded set to true and was responding with the proper content-type header (image/png in my case).

The response mentioned above about removing the proxy integration and setting your own headers is definitely more robust but this one was a lot easier to implement and seems to work fine for my needs but obviously this won't work if you need text/html for something else on your API.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.