architecture
Jul 29, 2024

Calling AWS Lambda over HTTP

Learning too much about calling a Lambda over HTTP

author picture
Oliver Marshall
Software Engineer
image

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:

  1. How do I set this up?
  2. What are the input and output formats?
  3. What happens if I don’t use those formats?
  4. What happens if an exception is thrown?
  5. 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:

TestCommand
Valid JSON requestcurl -v -XPOST --data '{}' <url under test>
Invalid JSON requestcurl -v -XPOST --data '{' <url under test>
Valid JSON responseCovered by that first test
Invalid JSON responsecurl -v -XPOST --data '{}' --header 'Test: invalid' <url under test>
Exception throwncurl -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:

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:

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:

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request200{}The JSON formatted request
Invalid JSON request200{}The JSON formatted request
Invalid JSON response200{The JSON formatted request
Exception thrown502Internal Server ErrorThe 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:

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”:

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request200{}The JSON formatted request
Invalid JSON request200{}The JSON formatted request
Invalid JSON response200{The JSON formatted request
Exception thrown502Internal Server ErrorThe JSON formatted request
POST Method disabled403HTML explaining the errorNothing

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>

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request502HTML for 502 Bad GatewayThe JSON formatted request
Invalid JSON request502HTML for 502 Bad GatewayThe JSON formatted request
Invalid JSON response502HTML for 502 Bad GatewayThe JSON formatted request
Exception thrown502HTML for 502 Bad GatewayThe JSON formatted request
Valid response format200(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:

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request200{}The JSON formatted request
Invalid JSON request200{}The JSON formatted request
Invalid JSON response500{"message":"Internal Server Error"}The JSON formatted request
Exception thrown500{"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:

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):

TestCommand
Invalid JSON responsecurl -v -XPOST --data '"invalid"' <url under test>
Exception throwncurl -v -XPOST --data '"error"' <url under test>

And now for the tests:

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request200{}{} (the input body)
Invalid JSON request400A JSON object with a message field explaining parse errorNothing
Invalid JSON response200{"invalid" (the input body)
Exception thrown200A JSON object in this format showing the error thrown"error" (the input)

A couple things to note here:

  1. 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.
  2. 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:

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):

TestStatus CodeResponse Bodyinput in lambda
Valid JSON request200(an empty body)The JSON formatted request
Invalid JSON request200(an empty body)The JSON formatted request
Invalid JSON response502{"message": "Internal server error"}The JSON formatted request
Exception thrown502{"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.

Recommended Resources
Head Office
Norfolk House, Silbury Blvd.
Milton Keynes, MK9 2AH
United Kingdom
Company registration: 08457399
Copyright © JUXT LTD. 2012-2024
Privacy Policy Terms of Use Contact Us
Get industry news, insights, research, updates and events directly to your inbox

Sign up for our newsletter