diff --git a/README.md b/README.md
index 207351b..f5b521a 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
- "github.com/awslabs/aws-lambda-go-api-proxy/gin"
+ ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
)
@@ -84,6 +84,8 @@ func main() {
}
```
+If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead.
+
### Fiber
To use with the Fiber framework, following the instructions from the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html), declare a `Handler` method for the main package.
@@ -132,6 +134,7 @@ func main() {
lambda.Start(Handler)
}
```
+If you're using a Function URL, you can use the `ProxyFunctionURLWithContext` instead.
## Other frameworks
This package also supports [Negroni](https://github.com/urfave/negroni), [GorillaMux](https://github.com/gorilla/mux), and plain old `HandlerFunc` - take a look at the code in their respective sub-directories. All packages implement the `Proxy` method exactly like our Gin sample above.
diff --git a/core/requestFnURL.go b/core/requestFnURL.go
new file mode 100644
index 0000000..00f3dbe
--- /dev/null
+++ b/core/requestFnURL.go
@@ -0,0 +1,205 @@
+// Package core provides utility methods that help convert proxy events
+// into an http.Request and http.ResponseWriter
+package core
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/aws/aws-lambda-go/events"
+ "github.com/aws/aws-lambda-go/lambdacontext"
+)
+
+const (
+ // FnURLContextHeader is the custom header key used to store the
+ // Function Url context. To access the Context properties use the
+ // GetFunctionURLContext method of the RequestAccessorFnURL object.
+ FnURLContextHeader = "X-GoLambdaProxy-Fu-Context"
+)
+
+// RequestAccessorFnURL objects give access to custom API Gateway properties
+// in the request.
+type RequestAccessorFnURL struct {
+ stripBasePath string
+}
+
+// GetFunctionURLContext extracts the API Gateway context object from a
+// request's custom header.
+// Returns a populated events.LambdaFunctionURLRequestContext object from
+// the request.
+func (r *RequestAccessorFnURL) GetFunctionURLContext(req *http.Request) (events.LambdaFunctionURLRequestContext, error) {
+ if req.Header.Get(APIGwContextHeader) == "" {
+ return events.LambdaFunctionURLRequestContext{}, errors.New("No context header in request")
+ }
+ context := events.LambdaFunctionURLRequestContext{}
+ err := json.Unmarshal([]byte(req.Header.Get(FnURLContextHeader)), &context)
+ if err != nil {
+ log.Println("Erorr while unmarshalling context")
+ log.Println(err)
+ return events.LambdaFunctionURLRequestContext{}, err
+ }
+ return context, nil
+}
+
+// StripBasePath instructs the RequestAccessor object that the given base
+// path should be removed from the request path before sending it to the
+// framework for routing. This is used when the Lambda is configured with
+// base path mappings in custom domain names.
+func (r *RequestAccessorFnURL) StripBasePath(basePath string) string {
+ if strings.Trim(basePath, " ") == "" {
+ r.stripBasePath = ""
+ return ""
+ }
+
+ newBasePath := basePath
+ if !strings.HasPrefix(newBasePath, "/") {
+ newBasePath = "/" + newBasePath
+ }
+
+ if strings.HasSuffix(newBasePath, "/") {
+ newBasePath = newBasePath[:len(newBasePath)-1]
+ }
+
+ r.stripBasePath = newBasePath
+
+ return newBasePath
+}
+
+// ProxyEventToHTTPRequest converts an Function URL proxy event into a http.Request object.
+// Returns the populated http request with additional two custom headers for the stage variables and Function Url context.
+// To access these properties use GetFunctionURLContext method of the RequestAccessor object.
+func (r *RequestAccessorFnURL) ProxyEventToHTTPRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
+ httpRequest, err := r.EventToRequest(req)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+ return addToHeaderFunctionURL(httpRequest, req)
+}
+
+// EventToRequestWithContext converts an Function URL proxy event and context into an http.Request object.
+// Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context.
+// Access those using GetFunctionURLContextFromContext and GetRuntimeContextFromContext functions in this package.
+func (r *RequestAccessorFnURL) EventToRequestWithContext(ctx context.Context, req events.LambdaFunctionURLRequest) (*http.Request, error) {
+ httpRequest, err := r.EventToRequest(req)
+ if err != nil {
+ log.Println(err)
+ return nil, err
+ }
+ return addToContextFunctionURL(ctx, httpRequest, req), nil
+}
+
+// EventToRequest converts an Function URL proxy event into an http.Request object.
+// Returns the populated request maintaining headers
+func (r *RequestAccessorFnURL) EventToRequest(req events.LambdaFunctionURLRequest) (*http.Request, error) {
+ decodedBody := []byte(req.Body)
+ if req.IsBase64Encoded {
+ base64Body, err := base64.StdEncoding.DecodeString(req.Body)
+ if err != nil {
+ return nil, err
+ }
+ decodedBody = base64Body
+ }
+
+ path := req.RawPath
+ // if RawPath empty is, populate from request context
+ if len(path) == 0 {
+ path = req.RequestContext.HTTP.Path
+ }
+
+ if r.stripBasePath != "" && len(r.stripBasePath) > 1 {
+ if strings.HasPrefix(path, r.stripBasePath) {
+ path = strings.Replace(path, r.stripBasePath, "", 1)
+ }
+ fmt.Printf("%v", path)
+ }
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ serverAddress := "https://" + req.RequestContext.DomainName
+ if customAddress, ok := os.LookupEnv(CustomHostVariable); ok {
+ serverAddress = customAddress
+ }
+ path = serverAddress + path
+
+ if len(req.RawQueryString) > 0 {
+ path += "?" + req.RawQueryString
+ } else if len(req.QueryStringParameters) > 0 {
+ values := url.Values{}
+ for key, value := range req.QueryStringParameters {
+ values.Add(key, value)
+ }
+ path += "?" + values.Encode()
+ }
+
+ httpRequest, err := http.NewRequest(
+ strings.ToUpper(req.RequestContext.HTTP.Method),
+ path,
+ bytes.NewReader(decodedBody),
+ )
+
+ if err != nil {
+ fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path)
+ log.Println(err)
+ return nil, err
+ }
+
+ httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP
+
+ for _, cookie := range req.Cookies {
+ httpRequest.Header.Add("Cookie", cookie)
+ }
+
+ for headerKey, headerValue := range req.Headers {
+ for _, val := range strings.Split(headerValue, ",") {
+ httpRequest.Header.Add(headerKey, strings.Trim(val, " "))
+ }
+ }
+
+ httpRequest.RequestURI = httpRequest.URL.RequestURI()
+
+ return httpRequest, nil
+}
+
+func addToHeaderFunctionURL(req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) (*http.Request, error) {
+ apiGwContext, err := json.Marshal(FunctionURLRequest.RequestContext)
+ if err != nil {
+ log.Println("Could not Marshal API GW context for custom header")
+ return req, err
+ }
+ req.Header.Add(APIGwContextHeader, string(apiGwContext))
+ return req, nil
+}
+
+func addToContextFunctionURL(ctx context.Context, req *http.Request, FunctionURLRequest events.LambdaFunctionURLRequest) *http.Request {
+ lc, _ := lambdacontext.FromContext(ctx)
+ rc := requestContextFnURL{lambdaContext: lc, FunctionURLProxyContext: FunctionURLRequest.RequestContext}
+ ctx = context.WithValue(ctx, ctxKey{}, rc)
+ return req.WithContext(ctx)
+}
+
+// GetFunctionURLContextFromContext retrieve APIGatewayProxyRequestContext from context.Context
+func GetFunctionURLContextFromContext(ctx context.Context) (events.LambdaFunctionURLRequestContext, bool) {
+ v, ok := ctx.Value(ctxKey{}).(requestContextFnURL)
+ return v.FunctionURLProxyContext, ok
+}
+
+// GetRuntimeContextFromContextFnURL retrieve Lambda Runtime Context from context.Context
+func GetRuntimeContextFromContextFnURL(ctx context.Context) (*lambdacontext.LambdaContext, bool) {
+ v, ok := ctx.Value(ctxKey{}).(requestContextFnURL)
+ return v.lambdaContext, ok
+}
+
+type requestContextFnURL struct {
+ lambdaContext *lambdacontext.LambdaContext
+ FunctionURLProxyContext events.LambdaFunctionURLRequestContext
+}
diff --git a/core/requestFnURL_test.go b/core/requestFnURL_test.go
new file mode 100644
index 0000000..6bd5eb2
--- /dev/null
+++ b/core/requestFnURL_test.go
@@ -0,0 +1,133 @@
+package core_test
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+
+ "github.com/aws/aws-lambda-go/events"
+ "github.com/awslabs/aws-lambda-go-api-proxy/core"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("RequestAccessorFnURL tests", func() {
+ Context("Function URL event conversion", func() {
+ accessor := core.RequestAccessorFnURL{}
+ qs := make(map[string]string)
+ mvqs := make(map[string][]string)
+ hdr := make(map[string]string)
+ qs["UniqueId"] = "12345"
+ hdr["header1"] = "Testhdr1"
+ hdr["header2"] = "Testhdr2"
+ // Multivalue query strings
+ mvqs["k1"] = []string{"t1"}
+ mvqs["k2"] = []string{"t2"}
+ bdy := "Test BODY"
+ basePathRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs)
+
+ It("Correctly converts a basic event", func() {
+ httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest)
+ Expect(err).To(BeNil())
+ Expect("/hello").To(Equal(httpReq.URL.Path))
+ Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI))
+ Expect("GET").To(Equal(httpReq.Method))
+ headers := basePathRequest.Headers
+ Expect(2).To(Equal(len(headers)))
+ })
+
+ binaryBody := make([]byte, 256)
+ _, err := rand.Read(binaryBody)
+ if err != nil {
+ Fail("Could not generate random binary body")
+ }
+
+ encodedBody := base64.StdEncoding.EncodeToString(binaryBody)
+
+ binaryRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "POST"), true, hdr, bdy, qs, mvqs)
+ binaryRequest.Body = encodedBody
+ binaryRequest.IsBase64Encoded = true
+
+ It("Decodes a base64 encoded body", func() {
+ httpReq, err := accessor.EventToRequestWithContext(context.Background(), binaryRequest)
+ Expect(err).To(BeNil())
+ Expect("/hello").To(Equal(httpReq.URL.Path))
+ Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI))
+ Expect("POST").To(Equal(httpReq.Method))
+ })
+
+ mqsRequest := getFunctionURLProxyRequest("/hello", getFunctionURLRequestContext("/hello", "GET"), false, hdr, bdy, qs, mvqs)
+ mqsRequest.RawQueryString = "hello=1&world=2&world=3"
+ mqsRequest.QueryStringParameters = map[string]string{
+ "hello": "1",
+ "world": "2",
+ }
+
+ It("Populates query string correctly", func() {
+ httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest)
+ Expect(err).To(BeNil())
+ Expect("/hello").To(Equal(httpReq.URL.Path))
+ fmt.Println("SDYFSDKFJDL")
+ fmt.Printf("%v", httpReq.RequestURI)
+ Expect(httpReq.RequestURI).To(ContainSubstring("hello=1"))
+ Expect(httpReq.RequestURI).To(ContainSubstring("world=2"))
+ Expect("GET").To(Equal(httpReq.Method))
+ query := httpReq.URL.Query()
+ Expect(2).To(Equal(len(query)))
+ Expect(query["hello"]).ToNot(BeNil())
+ Expect(query["world"]).ToNot(BeNil())
+ })
+ })
+
+ Context("StripBasePath tests", func() {
+ accessor := core.RequestAccessorFnURL{}
+ It("Adds prefix slash", func() {
+ basePath := accessor.StripBasePath("app1")
+ Expect("/app1").To(Equal(basePath))
+ })
+
+ It("Removes trailing slash", func() {
+ basePath := accessor.StripBasePath("/app1/")
+ Expect("/app1").To(Equal(basePath))
+ })
+
+ It("Ignores blank strings", func() {
+ basePath := accessor.StripBasePath(" ")
+ Expect("").To(Equal(basePath))
+ })
+ })
+})
+
+func getFunctionURLProxyRequest(path string, requestCtx events.LambdaFunctionURLRequestContext,
+ is64 bool, header map[string]string, body string, qs map[string]string, mvqs map[string][]string) events.LambdaFunctionURLRequest {
+ return events.LambdaFunctionURLRequest{
+ RequestContext: requestCtx,
+ RawPath: path,
+ RawQueryString: generateQueryString(qs),
+ Headers: header,
+ Body: body,
+ IsBase64Encoded: is64,
+ }
+}
+
+func getFunctionURLRequestContext(path, method string) events.LambdaFunctionURLRequestContext {
+ return events.LambdaFunctionURLRequestContext{
+ DomainName: "example.com",
+ HTTP: events.LambdaFunctionURLRequestContextHTTPDescription{
+ Method: method,
+ Path: path,
+ },
+ }
+}
+
+func generateQueryString(queryParameters map[string]string) string {
+ var queryString string
+ for key, value := range queryParameters {
+ if queryString != "" {
+ queryString += "&"
+ }
+ queryString += fmt.Sprintf("%s=%s", key, value)
+ }
+ return queryString
+}
diff --git a/core/requestv2_test.go b/core/requestv2_test.go
index 180e22b..8a45e78 100644
--- a/core/requestv2_test.go
+++ b/core/requestv2_test.go
@@ -3,12 +3,14 @@ package core_test
import (
"context"
"encoding/base64"
- "github.com/onsi/gomega/gstruct"
+ "fmt"
"io/ioutil"
"math/rand"
"os"
"strings"
+ "github.com/onsi/gomega/gstruct"
+
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambdacontext"
"github.com/awslabs/aws-lambda-go-api-proxy/core"
@@ -74,6 +76,8 @@ var _ = Describe("RequestAccessorV2 tests", func() {
It("Populates multiple value query string correctly", func() {
httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest)
Expect(err).To(BeNil())
+ fmt.Println("SDY!@$#!@FSDKFJDL")
+ fmt.Printf("%v", httpReq.RequestURI)
Expect("/hello").To(Equal(httpReq.URL.Path))
Expect(httpReq.RequestURI).To(ContainSubstring("hello=1"))
Expect(httpReq.RequestURI).To(ContainSubstring("world=2"))
diff --git a/core/responseFnURL.go b/core/responseFnURL.go
new file mode 100644
index 0000000..31cd892
--- /dev/null
+++ b/core/responseFnURL.go
@@ -0,0 +1,121 @@
+// Package core provides utility methods that help convert proxy events
+// into an http.Request and http.ResponseWriter
+package core
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "net/http"
+ "strings"
+ "unicode/utf8"
+
+ "github.com/aws/aws-lambda-go/events"
+)
+
+// FunctionURLResponseWriter implements http.ResponseWriter and adds the method
+// necessary to return an events.LambdaFunctionURLResponse object
+type FunctionURLResponseWriter struct {
+ headers http.Header
+ body bytes.Buffer
+ status int
+ observers []chan<- bool
+}
+
+// NewFunctionURLResponseWriter returns a new FunctionURLResponseWriter object.
+// The object is initialized with an empty map of headers and a
+// status code of -1
+func NewFunctionURLResponseWriter() *FunctionURLResponseWriter {
+ return &FunctionURLResponseWriter{
+ headers: make(http.Header),
+ status: defaultStatusCode,
+ observers: make([]chan<- bool, 0),
+ }
+}
+
+func (r *FunctionURLResponseWriter) CloseNotify() <-chan bool {
+ ch := make(chan bool, 1)
+
+ r.observers = append(r.observers, ch)
+
+ return ch
+}
+
+func (r *FunctionURLResponseWriter) notifyClosed() {
+ for _, v := range r.observers {
+ v <- true
+ }
+}
+
+// Header implementation from the http.ResponseWriter interface.
+func (r *FunctionURLResponseWriter) Header() http.Header {
+ return r.headers
+}
+
+// Write sets the response body in the object. If no status code
+// was set before with the WriteHeader method it sets the status
+// for the response to 200 OK.
+func (r *FunctionURLResponseWriter) Write(body []byte) (int, error) {
+ if r.status == defaultStatusCode {
+ r.status = http.StatusOK
+ }
+
+ // if the content type header is not set when we write the body we try to
+ // detect one and set it by default. If the content type cannot be detected
+ // it is automatically set to "application/octet-stream" by the
+ // DetectContentType method
+ if r.Header().Get(contentTypeHeaderKey) == "" {
+ r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body))
+ }
+
+ return (&r.body).Write(body)
+}
+
+// WriteHeader sets a status code for the response. This method is used
+// for error responses.
+func (r *FunctionURLResponseWriter) WriteHeader(status int) {
+ r.status = status
+}
+
+// GetProxyResponse converts the data passed to the response writer into
+// an events.APIGatewayProxyResponse object.
+// Returns a populated proxy response object. If the response is invalid, for example
+// has no headers or an invalid status code returns an error.
+func (r *FunctionURLResponseWriter) GetProxyResponse() (events.LambdaFunctionURLResponse, error) {
+ r.notifyClosed()
+
+ if r.status == defaultStatusCode {
+ return events.LambdaFunctionURLResponse{}, errors.New("Status code not set on response")
+ }
+
+ var output string
+ isBase64 := false
+
+ bb := (&r.body).Bytes()
+
+ if utf8.Valid(bb) {
+ output = string(bb)
+ } else {
+ output = base64.StdEncoding.EncodeToString(bb)
+ isBase64 = true
+ }
+
+ headers := make(map[string]string)
+ cookies := make([]string, 0)
+
+ for headerKey, headerValue := range http.Header(r.headers) {
+ if strings.EqualFold("set-cookie", headerKey) {
+ cookies = append(cookies, headerValue...)
+ continue
+ }
+ headers[headerKey] = strings.Join(headerValue, ",")
+ }
+
+ return events.LambdaFunctionURLResponse{
+ StatusCode: r.status,
+ Headers: headers,
+ Body: output,
+ IsBase64Encoded: isBase64,
+ Cookies: cookies,
+ }, nil
+}
diff --git a/core/responseFnURL_test.go b/core/responseFnURL_test.go
new file mode 100644
index 0000000..3607297
--- /dev/null
+++ b/core/responseFnURL_test.go
@@ -0,0 +1,118 @@
+package core
+
+import (
+ "math/rand"
+ "net/http"
+ "strings"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("FunctionURLResponseWriter tests", func() {
+ Context("writing to response object", func() {
+ response := NewFunctionURLResponseWriter()
+
+ It("Sets the correct default status", func() {
+ Expect(defaultStatusCode).To(Equal(response.status))
+ })
+
+ It("Initializes the headers map", func() {
+ Expect(response.headers).ToNot(BeNil())
+ Expect(0).To(Equal(len(response.headers)))
+ })
+
+ It("Writes headers correctly", func() {
+ response.Header().Add("Content-Type", "application/json")
+
+ Expect(1).To(Equal(len(response.headers)))
+ Expect("application/json").To(Equal(response.headers["Content-Type"][0]))
+ })
+
+ It("Writes body content correctly", func() {
+ binaryBody := make([]byte, 256)
+ _, err := rand.Read(binaryBody)
+ Expect(err).To(BeNil())
+
+ written, err := response.Write(binaryBody)
+ Expect(err).To(BeNil())
+ Expect(len(binaryBody)).To(Equal(written))
+ })
+
+ It("Automatically set the status code to 200", func() {
+ Expect(http.StatusOK).To(Equal(response.status))
+ })
+
+ It("Forces the status to a new code", func() {
+ response.WriteHeader(http.StatusAccepted)
+ Expect(http.StatusAccepted).To(Equal(response.status))
+ })
+ })
+
+ Context("Automatically set response content type", func() {
+ xmlBodyContent := "