There are three different methods to call an AWS Lambda over HTTP externally: Function URLs, Application Load Balancers (ALBs) and API Gateway
As I investigated each I found they had slightly different behaviours, quirks and setup instructions. I ended up with 20+ tabs open and even wrote bunch of test cases to learn more. I thought I would collect all that together in one place to share with you all (and my future self).
Methodology
While researching, the questions tried to answer were:
- How do I set this up?
- What are the input and output formats?
- What happens if I don’t use those formats?
- What happens if an exception is thrown?
- What other quirks do I notice?
Mostly I can link you to the relevant section of the docs, but for some questions I needed to run experiments. The tests I ran were:
Test | Command |
---|---|
Valid JSON request | curl -v -XPOST --data '{}' <url under test> |
Invalid JSON request | curl -v -XPOST --data '{' <url under test> |
Valid JSON response | Covered by that first test |
Invalid JSON response | curl -v -XPOST --data '{}' --header 'Test: invalid' <url under test> |
Exception thrown | curl -v -XPOST --data '{}' --header 'Test: error' <url under test> |
And the Clojure code I used to setup the lambda was (no Clojure knowledge is needed for the rest of this post):
(ns lambda
(:gen-class
:implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler]))
(defn -handleRequest [_ is os context]
(let [input (slurp is)]
(println "input:" input)
; If the word "error" is found anywhere in the input
(when (re-find #"error" input)
(throw (Exception. "test")))
; If the word "invalid" is found anywhere in the input
(let [output (if (re-find #"invalid" input)
"{" ; output invalid JSON
"{}") ; output valid JSON
(spit os output))))
(See my last post for how I set the project up)
Please note that the input is not parsed, so any errors that say something
about “parsing input” come from AWS not this code. Also note that the output
(valid or invalid) does not conform to any of the formats required below (e.g.
it does not contain a statusCode
field), this tests how each option behaves
when this is the case.
Most services have multiple ways of configuring them, the methods I’m covering are:
- Plain Function URLs
- Function URLs + CloudFront
- ALB
- API Gateway w/ HTTP API
- API Gateway w/ REST API
- API Gateway w/ REST API & Lambda Proxy Integration
Which should you choose?
Before we start, this article doesn’t aim to judge which option of these fits your requirements. But if you want to know which to check out first I would suggest:
- If you want just a URL with one lambda on the other end:
- If you don’t want a custom domain: Plain Function URLs
- If you do want a custom domain: Function URLs + CloudFront
- Otherwise try: ALBs or API Gateway w/ REST & Lambda Proxy Integration
Plain Function URLs
This is by far the easiest option to setup: just enable it, set two settings and you’re done!
The (small) downside is that you get a randomly generated url of the form:
https://<url-id>.lambda-url.<region>.on.aws
, but for many use cases this is
just fine.
As with most of these options, the request and response formats are in JSON.
Here are the results of my tests:
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON response | 200 | { | The JSON formatted request |
Exception thrown | 502 | Internal Server Error | The JSON formatted request |
Surprisingly whenever the response does not match this format (including
being invalid JSON) a 200
response is assumed! That said it’s unlikely to run
into this in practice, how often do you hand craft your response after all?
Just be wary if you are.
Function URLs + CloudFront
This option allows you to create a TLD that points at your Lambda Function URL. The behaviour mostly identical to Function URLs but I have included it because of one gotcha in the setup that can change the behaviour.
The setup is:
- Create a Function URL (same as above)
- Create your CloudFront Distribution
- For origin domain choose your Function URL
- For Allowed HTTP methods choose all the methods you plan to use (this is the gotcha I mentioned)
- If you want a custom domain, add an A record to Route53
- Use an alias to point to your Distribution’s URL
- Make sure to setup Alternative domain names in the distribution
- Wait until the CloudFront Distribution has finished deploying
As mentioned, this option uses Function URLs under the hood so the request and response formats are the same as above.
Because we have our additional gotcha, I’ve added an additional test for when
the POST
method is disabled in CloudFront. For this test I used the same
command as in “Valid JSON request”:
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON response | 200 | { | The JSON formatted request |
Exception thrown | 502 | Internal Server Error | The JSON formatted request |
POST Method disabled | 403 | HTML explaining the error | Nothing |
This last test result was very confusing to me initially because I missed the Allowed HTTP methods option initially. Once setup correctly though I had no issues, so this is only a problem during initial setup.
ALB
Creating an Internet-facing ALB with a Target Group pointing at a Lambda is simple enough. As ever when configuring ALBs be cautious when configuring Security Groups, during testing my requests were timing out until I resolved some things I had misconfigured.
The request and response formats are similar but slightly different to the Function URL formats. The major difference is in behaviour, ALBs are more strict with the response format.
Because of this strictness I wanted to test the case where the response format is valid too. I modified the lambda code like so:
(let [output (if (re-find #"invalid" input)
"{" ; output invalid JSON
- "{}") ; output valid JSON
+ (if (re-find #"valid" input)
+ ; 👇 Added an option to return JSON in the format that the ALB wants
+ "{\"isBase64Encoded\":false,\"statusCode\":200}"
+ "{}")) ; output valid JSON
(spit os output))))
I added an additional test using the command: curl -v -XPOST --data '{}' --header 'Test: valid' <url under test>
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 502 | HTML for 502 Bad Gateway | The JSON formatted request |
Invalid JSON request | 502 | HTML for 502 Bad Gateway | The JSON formatted request |
Invalid JSON response | 502 | HTML for 502 Bad Gateway | The JSON formatted request |
Exception thrown | 502 | HTML for 502 Bad Gateway | The JSON formatted request |
Valid response format | 200 | (an empty body) | The JSON formatted request |
As mentioned the response format is very strict, failing to use it means an
obscure 502 Bad Gateway
error. Make sure to catch exceptions and always
return the response in the correct format.
API Gateway w/ HTTP API
Using API Gateway presents a few options, but using HTTP APIs is the simplest option:
- Create your HTTP API
- For Integrations select lambda
- For Lambda function choose your lambda
- See here for setting up a custom domain
The request
and response
formats seem to be identical to Function URLs. Also similarly to Function URLs
it’s worth noting that responses that don’t match this format are assumed to be
a 200
response.
Here are my test results:
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON request | 200 | {} | The JSON formatted request |
Invalid JSON response | 500 | {"message":"Internal Server Error"} | The JSON formatted request |
Exception thrown | 500 | {"message":"Internal Server Error"} | The JSON formatted request |
As mentioned these results are similar to Function URLs, to save you scrolling
up the two main differences are: “Invalid JSON response” now returns a 500
instead of a 200
; and the body of 500
responses are now JSON.
API Gateway w/ REST API
Setting up a REST API is a tiny bit more involved:
- Create your API REST API
- Choose New API instead of Example API
- Create your Lambda Integration
- Keep Lambda Proxy Integration turned off (that’s the next section)
- See here for setting up a custom domain
The request and response formats are a bit more fuzzily documented due to requests being configured using Integration Requests and responses being configured using Integration Responses. Meaning you can transform the request and response in a variety of ways.
These transformations can affect the results of the tests but I still found it useful to understand the default behaviours and to know what to configure. By default the body is passed through to the lambda directly and I’ve left things as the default for the tests. I would suggest re-running these tests (and others!) after making configuration changes to see how these behaviours have really changed:
To test this setup I had to tweak two of the commands (the lambda code was kept the same):
Test | Command |
---|---|
Invalid JSON response | curl -v -XPOST --data '"invalid"' <url under test> |
Exception thrown | curl -v -XPOST --data '"error"' <url under test> |
And now for the tests:
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 200 | {} | {} (the input body) |
Invalid JSON request | 400 | A JSON object with a message field explaining parse error | Nothing |
Invalid JSON response | 200 | { | "invalid" (the input body) |
Exception thrown | 200 | A JSON object in this format showing the error thrown | "error" (the input) |
A couple things to note here:
- If the request is not valid JSON, your function won’t be called. I was not able to find a way around this in my testing, please let me know if I missed something.
- Throwing an error gets you a
200
by default. The official way to handle this is to configure the Lambda error regex which is matched against the errorMessage property in the response (as noted here in the final Note box, search for errorMessage). Luckily exceptions are already formatted this way, but note that you can also return JSON in this format if you like.
API Gateway w/ REST API & Lambda Proxy Integration
The setup is nearly identical to the above:
- Create your API REST API
- Choose New API instead of Example API
- Create your Lambda Integration
- This time turn Lambda Proxy Integration on
- See here for setting up a custom domain
The request and response formats are similar to the Function URL formats but with enough small differences that it’s worth you taking a look at them.
And here are my test results (using the original commands):
Test | Status Code | Response Body | input in lambda |
---|---|---|---|
Valid JSON request | 200 | (an empty body) | The JSON formatted request |
Invalid JSON request | 200 | (an empty body) | The JSON formatted request |
Invalid JSON response | 502 | {"message": "Internal server error"} | The JSON formatted request |
Exception thrown | 502 | {"message": "Internal server error"} | The JSON formatted request |
The results here are very similar to using API Gateway w/ HTTP API, the main difference being that if you don’t conform to the output format (i.e. you have valid JSON but an invalid format) it doesn’t fill the body of the request with your original response.
Conclusion
There are many different options for calling an AWS Lambda via HTTP, and while it’s fairly obvious that each of them would have different setup instructions I found that the variance in their behaviours was enough to trip me up many times. Hopefully the above information will be useful to avoid the gotchas in your chosen option the next time either of us wants to call a Lambda over HTTP.