Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lambda Function URL HTTP request and response types #436

Merged
merged 4 commits into from Apr 13, 2022

Conversation

bmoffatt
Copy link
Collaborator

@bmoffatt bmoffatt commented Apr 6, 2022

Description of changes:

https://aws.amazon.com/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/

lifted sample data from https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html

structs are based on the APIGatewayV2 events, but for the subset of features that Lambda Function URLs do not support, where sample data has null, I removed the fields - in particular this affected the Authorizer struct tree removing Lambda and JWT types, and eliminated Authentication from the request context struct.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov-commenter
Copy link

codecov-commenter commented Apr 6, 2022

Codecov Report

Merging #436 (950c2ba) into main (bc1ec47) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##             main     #436   +/-   ##
=======================================
  Coverage   71.42%   71.42%           
=======================================
  Files          19       19           
  Lines        1050     1050           
=======================================
  Hits          750      750           
  Misses        232      232           
  Partials       68       68           

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bc1ec47...950c2ba. Read the comment docs.

package events

// LambdaHTTPRequest contains data coming from the new HTTP API Gateway
type LambdaHTTPRequest struct {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm gonna change this to LambdaFunctionURLRequest/Response to align with the feature name in the documentation

type LambdaHTTPRequestContext struct {
RouteKey string `json:"routeKey"` // RouteKey is expected to always be `"$default"`
AccountID string `json:"accountId"`
Stage string `json:"stage"` // Stage is expected to always be `"$default"`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to delete this too, docs are pretty clear that it's a vestigial field.


// LambdaHTTPRequestContext contains the information to identify the AWS account and resources invoking the Lambda function.
type LambdaHTTPRequestContext struct {
RouteKey string `json:"routeKey"` // RouteKey is expected to always be `"$default"`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, thinking to delete this

@carlzogh
Copy link
Contributor

carlzogh commented Apr 7, 2022

For ref. here's the equivalent change in the Java events lib: aws/aws-lambda-java-libs#320

Comment on lines +11 to +12
Headers map[string]string `json:"headers"`
QueryStringParameters map[string]string `json:"queryStringParameters,omitempty"`
Copy link

@jasdel jasdel Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be map[string][]string or similar to events.APIGatewayProxyRequest. (Same comment for LambdaFunctionURLResponse)

Bringing this up mainly because both HTTP header and URL query strings can have multiple keys of the same value.

Header:

x-foo-bar: abc123
x-foo-bar: efg456
x-foo-bar: hij789, lkm000

query:

?foo=abc123&foo=efg456&foo=hij789&foo=lkm000

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambda Function URLs ended up copying the API Gateway payload 2.0 shape, which I believe got rid of the MultiValue*** option - events.APIGatewayV2HTTPRequest

I'll double check what the Lambda Function URLs behavior is when a client sends multi value headers/query, maybe there is the option for a custom MarshalJSON/UnmarshalJSON so that the struct consumer can still get a map[string][]string - is that what you had in mind @jasdel ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verified that the function urls respects multi-value query string and multi-value headers, but concatenates with , rather than representing as an array in the json

my echo function:

def lambda_handler(event, _):
    return event

my test case:

~/test
$ cat furl.go
package main

import (
	"bytes"
	"io"
	"net/http"
	"os"
)

func main() {
	url := "https://sbehoygxywlg7dihj4baeak4im0sbkyf.lambda-url.us-west-2.on.aws/yolo?hello=world&hello=lambda&foo=bar"
	body := bytes.NewBuffer([]byte("hello"))
	req, _ := http.NewRequest("POST", url, body)
	req.Header.Add("whats", "up")
	req.Header.Add("hello", "world")
	req.Header.Add("hello", "lambda")
	res, _ := http.DefaultClient.Do(req)
	io.Copy(os.Stdout, res.Body)
}
~/test
$ go run furl.go | jq
{
  "headers": {
    "whats": "up",
    "x-amzn-trace-id": "Root=1-62512b3e-7cc00b5e15c31ddd5201e2b6",
    "x-forwarded-proto": "https",
    "host": "sbehoygxywlg7dihj4baeak4im0sbkyf.lambda-url.us-west-2.on.aws",
    "x-forwarded-port": "443",
    "hello": "world,lambda",
    "x-forwarded-for": "205.251.233.105",
    "accept-encoding": "gzip",
    "user-agent": "Go-http-client/1.1"
  },
  "isBase64Encoded": true,
  "rawPath": "/yolo",
  "routeKey": "$default",
  "requestContext": {
    "accountId": "anonymous",
    "timeEpoch": 1649486654761,
    "routeKey": "$default",
    "stage": "$default",
    "domainPrefix": "sbehoygxywlg7dihj4baeak4im0sbkyf",
    "requestId": "274319d8-362b-488d-af20-1350781093f9",
    "domainName": "sbehoygxywlg7dihj4baeak4im0sbkyf.lambda-url.us-west-2.on.aws",
    "http": {
      "path": "/yolo",
      "protocol": "HTTP/1.1",
      "method": "POST",
      "sourceIp": "205.251.233.105",
      "userAgent": "Go-http-client/1.1"
    },
    "time": "09/Apr/2022:06:44:14 +0000",
    "apiId": "sbehoygxywlg7dihj4baeak4im0sbkyf"
  },
  "queryStringParameters": {
    "foo": "bar",
    "hello": "world,lambda"
  },
  "body": "aGVsbG8=",
  "version": "2.0",
  "rawQueryString": "hello=world&hello=lambda&foo=bar"
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came up with this approach to make the .Headers field more easily assignable to the stdlib http.Headers type. The same could be done for url.Values for the .QueryStringParameters field

type LambdaFunctionURLResponse struct {
// ...
	Headers         functionURLHeaders `json:"headers"`
// ...
}

type functionURLHeaders http.Header

func (headers *functionURLHeaders) UnmarshalJSON(b []byte) error {
	var intermediate map[string]commaSeperatedValues
	if err := json.Unmarshal(b, &intermediate); err != nil {
		return err
	}
	*headers = make(functionURLHeaders, len(intermediate))
	for k, v := range intermediate {
		(*headers)[k] = v
	}
	return nil
}

type commaSeperatedValues []string

func (values *commaSeperatedValues) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	*values = strings.Split(s, ",")
	return nil
}

func (values commaSeperatedValues) MarshalJSON() ([]byte, error) {
	return json.Marshal(strings.Join(values, ","))
}

Which was neat, but I'm not 100% convinced this is the right approach for this struct. I'd hoped instead to be able to legally cast like var values map[string]commaSeperatedValue; headers := http.Header(values), but go's type system seemed to demand the make(...) and re-assignment loop, which felt marginally wasteful.

I poked @carlzogh again about this, and right now I'm leaning to keep this PR as-is rather than extending the serialization.

  1. For the most part, structs in this library, and types in the Java and .NET libraries, mirror the implicit JSON schema
  2. Documentation claims that Lambda function URLs are compatible with API Gateway HTTP Payload Version 2.0 - the struct for this type does not customize the .Header or .QueryStringParameters fields.

longer term, I'm open to smarter serialization for the http proxy event sources, as either some v2 types with a richer set of built-in types and appropriate documentation for construction, or with a higher abstraction layer that hides the json goo behind the stdlib http.Request/http.ResponseWriter types

Copy link

@jasdel jasdel Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current implementation that joins multiple headers makes sense, given the API gateway 2.0 style. I'd suggestion not added automatic splitting on commas though, because comma being a delimiter in a header is dependent on the header, and requires domain knowledge.

For example a common date format, HTTP-Date, Wed, 21 Oct 2015 07:28:00 GMT. Contains a comma in its value. Logic that splits on commas without domain knowledge of the header would produce the wrong result. Leaving header value splitting to the consumer probably is the best path forward.

@bmoffatt bmoffatt merged commit 138d021 into aws:main Apr 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants