GoLang AWS Lambda Function Missing Body from API Gateway Request - go

I'm using an AWS Lambda function to handle a request from an AWS API Gateway call. I'm sending a payload on the request, and I can verify in CloudWatch that the payload is being passed from the gateway to the lambda function. However, the body of the request is null inside my Lambda function.
I looked at this question: AWS Lambda Go function not getting request body when called via API GW
I am trying to replicate the answer there by using this library: https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go, but I'm still not able to get the request body.
Here is my Lambda code:
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
fmt.Println("Body")
fmt.Println(request.Body)
fmt.Printf("Processing request data for request %s.\n", request.RequestContext.RequestID)
fmt.Printf("Processing request data for request %s.\n", request.RequestContext.RequestID)
fmt.Printf("Body size = %d.\n", len(request.Body))
fmt.Println("Headers:")
for key, value := range request.Headers {
fmt.Printf(" %s: %s\n", key, value)
}
return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil
}
func main() {
lambda.Start(handleRequest)
}
I'm expecting to see some data after "Body" in Cloudwatch, but there is nothing.

The code in the original question is correct. The second argument to the handleRequest is of the type APIGatewayProxyRequest. In API Gateway, I was sending a normal request, not a proxy request. I redeployed my API Gateway route as a proxy request and got the request body I was expecting. I'm still not really sure whether my original request was failing to send the body, or if the structure of a normal request passed into the handleRequest function is different from that of a proxy request and so the APIGatewayProxyRequest type was unable to parse its body.

Related

CognitoIdentityID is empty in Go lambda function with Cognito authentication and API Gateway

I have an AWS lambda function written in Go behind an API Gateway. I'm using Cognito with App integration and OAuth Client credentials grant for authentication. I need to know which App client sent the request inside the lambda function, but all the fields related to Cognito (CognitoIdentityID, CognitoIndetityPoolID, AccountID, etc.) are empty in the context and the request. I can only see the Bearer Authorization in the header of the request. I'm printing the values as follows:
var forwardRequest = func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
println(fmt.Sprintf("Request arrived: Stage: %s, Method: %s, Path: %s",
request.RequestContext.Stage, request.HTTPMethod, request.Path))
lc, _ := lambdacontext.FromContext(ctx)
println(fmt.Sprintf("lc: %+v", lc))
println(fmt.Sprintf("request: %+v", request))
...
}
func main() {
// Make the handler available for Remote Procedure Call by AWS Lambda
lambda.Start(forwardRequest)
}
I'm already using Lambda Proxy integration, as answered in similar questions. I attach a screenshot of the configuration of the API Gateway method. I can't enable "Invoke with caller credentials".
How can I know which client sent the request?.
I found the answer: Cognito's client ID is not in the Cognito* fields but in the Authorizer of the request:
func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
var clientID string
if claimsMap, ok := request.RequestContext.Authorizer["claims"].(map[string]interface{}); ok {
clientID, ok = claimsMap["client_id"].(string)
...
}
...

HTTP client receives status code 200 when server panics with gin-gonic and gin-contrib/gzip

When accessing the gin-gonic server below, the HTTP client should receive the code 500, but receives the code 200.
package main
import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
)
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(gzip.Gzip(gzip.DefaultCompression))
r.POST("/test", func(c *gin.Context) {
panic("test") // Server panic and client should receive code 500.
})
r.Run(":8080")
}
When accessing /test from a HTTP client, the go server log is as below and looks return the code 500.
[GIN] 2020/09/28 - 10:23:14 | 500 | 67.2995ms | ::1 | POST "/test"
2020/09/28 10:23:14 [Recovery] 2020/09/28 - 10:23:14 panic recovered:
test
C:/path/to/myproject/main.go:16 (0x8f193f)
main.func1: panic("test")
But HTTP client receives the code 200.
When I remove r.Use(gzip.Gzip(gzip.DefaultCompression)), the HTTP client receives the code 500.
Why the client receives code 200 with r.Use(gzip.Gzip(gzip.DefaultCompression)), How can I fix this?
I've reproduce your case. Postman got code 200 but the server results 500 instead.
The server will call c.Next() to execute 4 handlers when receive post requests. The sequence is as follow:
gin.Logger
gin.Recovery
gzip.Gzip(gzip.DefaultCompression)
your handler
Here is gin responseWriter writes response header and it will write header only once.
func (w *responseWriter) WriteHeaderNow() {
if !w.Written() {
w.size = 0
w.ResponseWriter.WriteHeader(w.status)
}
}
Both gzip.Gzip(gzip.DefaultCompression) and gin.Recovery has defer func to write response header. Golang's deferred calls are executed in last-in-first-out order. So gzip.Gzip(gzip.DefaultCompression) will write response header to 200, and gin.Recovery won't write reponse header to 500 as expected.
So to solve this problem, you should change the order of handlers and make sure gin.Recovery is the last handler to load.
Adding the recovery middleware last seems to fix this.
package main
import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
)
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger())
r.Use(gzip.Gzip(gzip.DefaultCompression))
r.Use(gin.Recovery())
r.POST("/test", func(c *gin.Context) {
panic("test") // Server panic and client should receive code 500.
})
r.Run(":8080")
}

Echo CORS w/ Proxy middlewares causes problems w/ Access-Allow-Origins response header

I'm using LabStack's Golang Echo Framework to build out a service.
One of the routes, needs to proxy requests and responses to and from a backend service.
But I also need CORS to work on this service as well.
So I'm using middleware.CORSWithConfig along w/ a middleware.ProxyWithConfig in my request/response stack.
I'm seeing some oddness w/ the Access-Control-Allow-Origins header where the value for that header on the response from the proxied service to my Echo server *, but once it passes through the proxy, it changes to *, * by the time it gets back into the client.
Upon which I start seeing the following browser errors related to CORS violations:
VM1627:362 Access to XMLHttpRequest at 'http://localhost:6273/' from origin 'http://localhost:8002' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.
Has anyone come across this? Anyone have any idea why this might be happening and maybe a way around it?
Here's some example code:
package main
func singleTargetBalancer(url *url.URL) middleware.ProxyBalancer {
targetURL := []*middleware.ProxyTarget{
{
URL: url,
},
}
return middleware.NewRoundRobinBalancer(targetURL)
}
func Noop(ctx echo.Context) (err error) {
ctx.String(
http.StatusNotImplemented,
"No op handler should never be reached!",
)
return err
}
func main() {
e := echo.New()
e.HideBanner = true
e.Use(
middleware.CORSWithConfig(middlewares.CustomCorsConfig),
middlewares.ThriftMetrics(),
)
// Have to use a Noop handler since we're not trying to set up a full-on proxy for the backend service. We only want this one route to be proxied.
e.POST(
"/",
handlers.Noop,
middleware.ProxyWithConfig(middleware.ProxyConfig{
Balancer: singleTargetBalancer("[backend service URL]"),
})
)
}
I ultimately solved this by writing a custom Echo middleware to hook into the response before Echo's proxy middleware could send the headers back to the client.
func setResponseACAOHeaderFromRequest (req http.Request, resp echo.Response) {
resp.Header().Set(echo.HeaderAccessControlAllowOrigin,
req.Header.Get(echo.HeaderOrigin))
}
func ACAOHeaderOverwriteMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
ctx.Response().Before(func() {
setResponseACAOHeaderFromRequest(*ctx.Request(), *ctx.Response())
})
return next(ctx)
}
}
Then just put this middleware in e.Use() right before your proxy middleware:
e.POST(
"/",
handlers.Noop,
ACAOHeaderOverwriteMiddleware,
middleware.ProxyWithConfig(middleware.ProxyConfig{
Balancer: singleTargetBalancer("[backend service URL]"),
})
)
Docs for Echo's Request::Before() hook: https://echo.labstack.com/guide/response#before-response

Why isn't the Response field of this HTTP Request being populated?

The comment for the field Response in the type http.Request is as follows.
// Response is the redirect response which caused this request
// to be created. This field is only populated during client
// redirects.
Response *Response
However, it seems to me that this field is not being populated during requests, as it is implied that it is. Consider the following example:
package main
import (
"net/http"
"log"
"fmt"
)
func handleA(writer http.ResponseWriter, request *http.Request) {
http.Redirect(writer, request, "/b", http.StatusSeeOther)
}
func handleB(writer http.ResponseWriter, request *http.Request) {
fmt.Println(request.Response)
}
func main() {
http.HandleFunc("/a", handleA)
http.HandleFunc("/b", handleB)
log.Fatal(http.ListenAndServe(":8080", nil))
}
If I compile and run this code and navigate to localhost:8080/a, then I get redirected to localhost:8080/b and the server prints <nil> to the console. But shouldn't it be printing out a non-nil value, since the request is coming as the result of a redirect?
In your sample, the redirect is happening in the browser; the server doesn't get to know what response generated the redirect. That field is populated when making HTTP requests from a Go app; for example, if you use http.Client to request a URL, and the response is a redirect, it generates a new Request for the redirect URL, and in that Request, the Response field will be populated with the response that triggered that redirect.
This is evidenced in the source for http.Client: https://golang.org/src/net/http/client.go#L669

AWS Lambda in Go behing API Gateway: custom error message

I'm trying to handle an error in a Lambda function written in Go.
The Lambda is triggered by API Gateway.
When I respond with a 200, I get the correct response.
But when I respond with a 500 code, I always receive {"message": "Internal server error"}
Here is part of the code:
func newErrReponse(message string) (events.APIGatewayProxyResponse, error) {
return events.APIGatewayProxyResponse{
Body: message,
StatusCode: 500,
}, errors.New(message)
}
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
return newErrReponse("some error")
}
func main() {
lambda.Start(handleRequest)
}
I was expecting "some error", but I always get the internal server error. I tried JSON in the body but that didn't help.
The integration request is of type LAMBDA_PROXY. That was the default.
How can I control the error response?
The lambda handler can return 2 values. interface{} and error:
https://godoc.org/github.com/aws/aws-lambda-go/lambda
Because I use API Gateway, The interface is of type APIGatewayProxyResponse:
https://godoc.org/github.com/aws/aws-lambda-go/events#APIGatewayProxyResponse
If the Lambda succeeds, the API Gateway will return the values from APIGatewayProxyResponse.
But if the Lambda don't succeeds, then you will get an internal server error.
If you return an error other then nil, then the Lambda failed.
When there is a panic() or os.Exit() then the Lambda also failed.
This means a log.Fatal also fails a Lambda.
Here is more information about errors:
https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-errors.html
Lambda logs a panic (and other output) in CloudWatch

Resources