Skip to content

Commit 66b6370

Browse files
committed
Implement echov4 and use new APIGatewayV2HTTPRequest and APIGatewayV2HTTPResponse structs
1 parent 8564c1c commit 66b6370

File tree

8 files changed

+341
-16
lines changed

8 files changed

+341
-16
lines changed

core/request.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ func (r *RequestAccessor) ProxyEventToHTTPRequest(req events.APIGatewayProxyRequ
116116
return addToHeader(httpRequest, req)
117117
}
118118

119+
func (r *RequestAccessor) ProxyEventToHTTPRequestV2(req events.APIGatewayV2HTTPRequest) (*http.Request, error) {
120+
httpRequest, err := r.EventToRequestV2(req)
121+
if err != nil {
122+
log.Println(err)
123+
return nil, err
124+
}
125+
return addToHeaderV2(httpRequest, req)
126+
}
127+
119128
// EventToRequestWithContext converts an API Gateway proxy event and context into an http.Request object.
120129
// Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context.
121130
// Access those using GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package.
@@ -128,6 +137,15 @@ func (r *RequestAccessor) EventToRequestWithContext(ctx context.Context, req eve
128137
return addToContext(ctx, httpRequest, req), nil
129138
}
130139

140+
func (r *RequestAccessor) EventToRequestWithContextV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (*http.Request, error) {
141+
httpRequest, err := r.EventToRequestV2(req)
142+
if err != nil {
143+
log.Println(err)
144+
return nil, err
145+
}
146+
return addToContextV2(ctx, httpRequest, req), nil
147+
}
148+
131149
// EventToRequest converts an API Gateway proxy event into an http.Request object.
132150
// Returns the populated request maintaining headers
133151
func (r *RequestAccessor) EventToRequest(req events.APIGatewayProxyRequest) (*http.Request, error) {
@@ -196,6 +214,73 @@ func (r *RequestAccessor) EventToRequest(req events.APIGatewayProxyRequest) (*ht
196214
return httpRequest, nil
197215
}
198216

217+
func (r *RequestAccessor) EventToRequestV2(req events.APIGatewayV2HTTPRequest) (*http.Request, error) {
218+
decodedBody := []byte(req.Body)
219+
if req.IsBase64Encoded {
220+
base64Body, err := base64.StdEncoding.DecodeString(req.Body)
221+
if err != nil {
222+
return nil, err
223+
}
224+
decodedBody = base64Body
225+
}
226+
227+
path := req.RequestContext.HTTP.Path
228+
if r.stripBasePath != "" && len(r.stripBasePath) > 1 {
229+
if strings.HasPrefix(path, r.stripBasePath) {
230+
path = strings.Replace(path, r.stripBasePath, "", 1)
231+
}
232+
}
233+
if !strings.HasPrefix(path, "/") {
234+
path = "/" + path
235+
}
236+
serverAddress := DefaultServerAddress
237+
if customAddress, ok := os.LookupEnv(CustomHostVariable); ok {
238+
serverAddress = customAddress
239+
}
240+
path = serverAddress + path
241+
242+
if len(req.QueryStringParameters) > 0 {
243+
queryString := ""
244+
for q, l := range req.QueryStringParameters {
245+
values := strings.Split(l, ",")
246+
for _, v := range values {
247+
if queryString != "" {
248+
queryString += "&"
249+
}
250+
queryString += url.QueryEscape(q) + "=" + url.QueryEscape(v)
251+
}
252+
}
253+
path += "?" + queryString
254+
} else if len(req.QueryStringParameters) > 0 {
255+
// Support `QueryStringParameters` for backward compatibility.
256+
// https://github.com/awslabs/aws-lambda-go-api-proxy/issues/37
257+
queryString := ""
258+
for q := range req.QueryStringParameters {
259+
if queryString != "" {
260+
queryString += "&"
261+
}
262+
queryString += url.QueryEscape(q) + "=" + url.QueryEscape(req.QueryStringParameters[q])
263+
}
264+
path += "?" + queryString
265+
}
266+
267+
httpRequest, err := http.NewRequest(
268+
strings.ToUpper(req.RequestContext.HTTP.Method),
269+
path,
270+
bytes.NewReader(decodedBody),
271+
)
272+
273+
if err != nil {
274+
fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path)
275+
log.Println(err)
276+
return nil, err
277+
}
278+
for h := range req.Headers {
279+
httpRequest.Header.Add(h, req.Headers[h])
280+
}
281+
return httpRequest, nil
282+
}
283+
199284
func addToHeader(req *http.Request, apiGwRequest events.APIGatewayProxyRequest) (*http.Request, error) {
200285
stageVars, err := json.Marshal(apiGwRequest.StageVariables)
201286
if err != nil {
@@ -212,13 +297,36 @@ func addToHeader(req *http.Request, apiGwRequest events.APIGatewayProxyRequest)
212297
return req, nil
213298
}
214299

300+
func addToHeaderV2(req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) (*http.Request, error) {
301+
stageVars, err := json.Marshal(apiGwRequest.StageVariables)
302+
if err != nil {
303+
log.Println("Could not marshal stage variables for custom header")
304+
return nil, err
305+
}
306+
req.Header.Add(APIGwStageVarsHeader, string(stageVars))
307+
apiGwContext, err := json.Marshal(apiGwRequest.RequestContext)
308+
if err != nil {
309+
log.Println("Could not Marshal API GW context for custom header")
310+
return req, err
311+
}
312+
req.Header.Add(APIGwContextHeader, string(apiGwContext))
313+
return req, nil
314+
}
315+
215316
func addToContext(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayProxyRequest) *http.Request {
216317
lc, _ := lambdacontext.FromContext(ctx)
217318
rc := requestContext{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables}
218319
ctx = context.WithValue(ctx, ctxKey{}, rc)
219320
return req.WithContext(ctx)
220321
}
221322

323+
func addToContextV2(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) *http.Request {
324+
lc, _ := lambdacontext.FromContext(ctx)
325+
rc := requestContextV2{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables}
326+
ctx = context.WithValue(ctx, ctxKey{}, rc)
327+
return req.WithContext(ctx)
328+
}
329+
222330
// GetAPIGatewayContextFromContext retrieve APIGatewayProxyRequestContext from context.Context
223331
func GetAPIGatewayContextFromContext(ctx context.Context) (events.APIGatewayProxyRequestContext, bool) {
224332
v, ok := ctx.Value(ctxKey{}).(requestContext)
@@ -244,3 +352,9 @@ type requestContext struct {
244352
gatewayProxyContext events.APIGatewayProxyRequestContext
245353
stageVars map[string]string
246354
}
355+
356+
type requestContextV2 struct {
357+
lambdaContext *lambdacontext.LambdaContext
358+
gatewayProxyContext events.APIGatewayV2HTTPRequestContext
359+
stageVars map[string]string
360+
}

core/response.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@ const contentTypeHeaderKey = "Content-Type"
1818
// ProxyResponseWriter implements http.ResponseWriter and adds the method
1919
// necessary to return an events.APIGatewayProxyResponse object
2020
type ProxyResponseWriter struct {
21-
headers http.Header
22-
body bytes.Buffer
23-
status int
24-
observers []chan <-bool
21+
headers http.Header
22+
body bytes.Buffer
23+
status int
24+
observers []chan<- bool
2525
}
2626

2727
// NewProxyResponseWriter returns a new ProxyResponseWriter object.
2828
// The object is initialized with an empty map of headers and a
2929
// status code of -1
3030
func NewProxyResponseWriter() *ProxyResponseWriter {
3131
return &ProxyResponseWriter{
32-
headers: make(http.Header),
33-
status: defaultStatusCode,
32+
headers: make(http.Header),
33+
status: defaultStatusCode,
3434
observers: make([]chan<- bool, 0),
3535
}
3636

@@ -111,7 +111,41 @@ func (r *ProxyResponseWriter) GetProxyResponse() (events.APIGatewayProxyResponse
111111

112112
return events.APIGatewayProxyResponse{
113113
StatusCode: r.status,
114-
Headers: proxyHeaders,
114+
Headers: proxyHeaders,
115+
MultiValueHeaders: http.Header(r.headers),
116+
Body: output,
117+
IsBase64Encoded: isBase64,
118+
}, nil
119+
}
120+
121+
func (r *ProxyResponseWriter) GetProxyResponseV2() (events.APIGatewayV2HTTPResponse, error) {
122+
r.notifyClosed()
123+
124+
if r.status == defaultStatusCode {
125+
return events.APIGatewayV2HTTPResponse{}, errors.New("Status code not set on response")
126+
}
127+
128+
var output string
129+
isBase64 := false
130+
131+
bb := (&r.body).Bytes()
132+
133+
if utf8.Valid(bb) {
134+
output = string(bb)
135+
} else {
136+
output = base64.StdEncoding.EncodeToString(bb)
137+
isBase64 = true
138+
}
139+
140+
proxyHeaders := make(map[string]string)
141+
142+
for h := range r.headers {
143+
proxyHeaders[h] = r.headers.Get(h)
144+
}
145+
146+
return events.APIGatewayV2HTTPResponse{
147+
StatusCode: r.status,
148+
Headers: proxyHeaders,
115149
MultiValueHeaders: http.Header(r.headers),
116150
Body: output,
117151
IsBase64Encoded: isBase64,

core/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ func GatewayTimeout() events.APIGatewayProxyResponse {
1212
return events.APIGatewayProxyResponse{StatusCode: http.StatusGatewayTimeout}
1313
}
1414

15+
func GatewayTimeoutV2() events.APIGatewayV2HTTPResponse {
16+
return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusGatewayTimeout}
17+
}
18+
1519
// NewLoggedError generates a new error and logs it to stdout
1620
func NewLoggedError(format string, a ...interface{}) error {
1721
err := fmt.Errorf(format, a...)

echov4/adapter.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Packge echolambda add Echo support for the aws-severless-go-api library.
2+
// Uses the core package behind the scenes and exposes the New method to
3+
// get a new instance and Proxy method to send request to the echo.Echo
4+
package echov4adapter
5+
6+
import (
7+
"context"
8+
"net/http"
9+
10+
"github.com/aws/aws-lambda-go/events"
11+
"github.com/awslabs/aws-lambda-go-api-proxy/core"
12+
"github.com/labstack/echo/v4"
13+
)
14+
15+
// EchoLambda makes it easy to send API Gateway proxy events to a echo.Echo.
16+
// The library transforms the proxy event into an HTTP request and then
17+
// creates a proxy response object from the http.ResponseWriter
18+
type EchoLambda struct {
19+
core.RequestAccessor
20+
21+
Echo *echo.Echo
22+
}
23+
24+
// New creates a new instance of the EchoLambda object.
25+
// Receives an initialized *echo.Echo object - normally created with echo.New().
26+
// It returns the initialized instance of the EchoLambda object.
27+
func New(e *echo.Echo) *EchoLambda {
28+
return &EchoLambda{Echo: e}
29+
}
30+
31+
// Proxy receives an API Gateway proxy event, transforms it into an http.Request
32+
// object, and sends it to the echo.Echo for routing.
33+
// It returns a proxy response object generated from the http.ResponseWriter.
34+
func (e *EchoLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
35+
echoRequest, err := e.ProxyEventToHTTPRequest(req)
36+
return e.proxyInternal(echoRequest, err)
37+
}
38+
39+
func (e *EchoLambda) ProxyV2(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
40+
echoRequest, err := e.ProxyEventToHTTPRequestV2(req)
41+
return e.proxyInternalV2(echoRequest, err)
42+
}
43+
44+
// ProxyWithContext receives context and an API Gateway proxy event,
45+
// transforms them into an http.Request object, and sends it to the echo.Echo for routing.
46+
// It returns a proxy response object generated from the http.ResponseWriter.
47+
func (e *EchoLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
48+
echoRequest, err := e.EventToRequestWithContext(ctx, req)
49+
return e.proxyInternal(echoRequest, err)
50+
}
51+
52+
func (e *EchoLambda) ProxyWithContextV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
53+
echoRequest, err := e.EventToRequestWithContextV2(ctx, req)
54+
return e.proxyInternalV2(echoRequest, err)
55+
}
56+
57+
func (e *EchoLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) {
58+
59+
if err != nil {
60+
return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err)
61+
}
62+
63+
respWriter := core.NewProxyResponseWriter()
64+
e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req)
65+
66+
proxyResponse, err := respWriter.GetProxyResponse()
67+
if err != nil {
68+
return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err)
69+
}
70+
71+
return proxyResponse, nil
72+
}
73+
74+
func (e *EchoLambda) proxyInternalV2(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) {
75+
76+
if err != nil {
77+
return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err)
78+
}
79+
80+
respWriter := core.NewProxyResponseWriter()
81+
e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req)
82+
83+
proxyResponse, err := respWriter.GetProxyResponseV2()
84+
if err != nil {
85+
return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err)
86+
}
87+
88+
return proxyResponse, nil
89+
}

echov4/echo_suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package echov4adapter_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestEcho(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Echo Suite")
13+
}

echov4/echolambda_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package echov4adapter_test
2+
3+
import (
4+
"log"
5+
6+
"github.com/aws/aws-lambda-go/events"
7+
"github.com/labstack/echo/v4"
8+
"github.com/awslabs/aws-lambda-go-api-proxy/echov4"
9+
10+
. "github.com/onsi/ginkgo"
11+
. "github.com/onsi/gomega"
12+
)
13+
14+
var _ = Describe("EchoLambda tests", func() {
15+
Context("Simple ping request", func() {
16+
It("Proxies the event correctly", func() {
17+
log.Println("Starting test")
18+
e := echo.New()
19+
e.GET("/ping", func(c echo.Context) error {
20+
log.Println("Handler!!")
21+
return c.String(200, "pong")
22+
})
23+
24+
adapter := echov4adapter.New(e)
25+
26+
req := events.APIGatewayProxyRequest{
27+
Path: "/ping",
28+
HTTPMethod: "GET",
29+
}
30+
31+
resp, err := adapter.Proxy(req)
32+
33+
Expect(err).To(BeNil())
34+
Expect(resp.StatusCode).To(Equal(200))
35+
})
36+
})
37+
})

0 commit comments

Comments
 (0)