-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
#814 Add Transport Layer: AWS Lambda #815
Conversation
@suekto-andreas Why not accept a generic event and then let the user decided how to decode and encode the request and response? |
@dimiro1 by generic event, would it means change the request & response type into The essential issue is due to AWS SDK behavior that if we provide with This root cause brings up the following impediments
I think mainly because using |
Hey @suekto-andreas, I was thinking about relying on the lambda.Handler as the entry point. A few days ago I was playing with something very similar, this is what I am using in a Pet project. In the end it is not very different of the go-kit http package. package lambda
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/log"
)
type DecodeRequestFunc func(context.Context, []byte) (interface{}, error)
type EncodeResponseFunc func(context.Context, interface{}) ([]byte, error)
type ServerRequestFunc func(ctx context.Context, payload []byte) context.Context
type ServerResponseFunc func(ctx context.Context, response interface{}) context.Context
type Server struct {
e endpoint.Endpoint
dec DecodeRequestFunc
enc EncodeResponseFunc
before []ServerRequestFunc
after []ServerResponseFunc
errorEncoder ErrorEncoder
logger log.Logger
}
// ServerOption sets an optional parameter for servers.
type ServerOption func(*Server)
// ErrorEncoder is responsible for handling custom errors for AWS Lambda.
type ErrorEncoder func(error) error
// DefaultErrorEncoder the default behaviour is just return the same error.
func DefaultErrorEncoder(e error) error { return e }
// ServerErrorEncoder is used to return custom errors
// it can be used to control the state of the state machine in AWS Step Functions
//
// See: https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-errors.html
// See: https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-handling-error-conditions.html
func ServerErrorEncoder(e ErrorEncoder) ServerOption {
return func(s *Server) { s.errorEncoder = e }
}
// ServerBefore functions are executed on the AWS Lambda event before the event is decoded.
func ServerBefore(before ...ServerRequestFunc) ServerOption {
return func(s *Server) { s.before = append(s.before, before...) }
}
// ServerAfter functions are executed on the response after the
// endpoint is invoked, but before anything is sent to the client.
func ServerAfter(after ...ServerResponseFunc) ServerOption {
return func(s *Server) { s.after = append(s.after, after...) }
}
func NewServer(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...ServerOption) *Server {
s := &Server{
e: e,
dec: dec,
enc: enc,
errorEncoder: DefaultErrorEncoder,
logger: log.NewNopLogger(),
}
for _, option := range options {
option(s)
}
return s
}
// Handler it is the AWS Lambda entry point.
// This function is the one you have to pass to lambda.Start function.
func (s *Server) Invoke(ctx context.Context, payload []byte) ([]byte, error) {
var (
request interface{}
response interface{}
)
for _, f := range s.before {
ctx = f(ctx, payload)
}
request, err := s.dec(ctx, payload)
if err != nil {
_ = s.logger.Log("err", err)
return []byte{}, s.errorEncoder(err)
}
response, err = s.e(ctx, request)
if err != nil {
_ = s.logger.Log("err", err)
return []byte{}, s.errorEncoder(err)
}
bytes, err := s.enc(ctx, response)
if err != nil {
_ = s.logger.Log("err", err)
return []byte{}, s.errorEncoder(err)
}
for _, f := range s.after {
ctx = f(ctx, response)
}
return bytes, nil
} package myendpoint
import (
awslambda "github.com/aws/aws-lambda-go/lambda"
"my/endpoints"
)
func NewAWSLambdaHandler(endpoints Endpoints) awslambda.Handler {
return lambda.NewServer(endpoints.MyServiceEndpoint, decodeAWSLambdaRequest, encodeAWSLambdaResponse)
}
func decodeAWSLambdaRequest(_ context.Context, payload []byte) (interface{}, error) {
var req myRequestType
err := json.Unmarshal(payload, &req)
if err != nil {
return req, err
}
return req, nil
}
func encodeAWSLambdaResponse(_ context.Context, response interface{}) ([]byte, error) {
bytes, err := json.Marshal(response)
if err != nil {
return []byte{}, err
}
return bytes, nil
} package main
func main() {
handler := NewAWSLambdaHandler(myendpoints)
awslambda.StartHandler(handler)
} |
Many Thanks @dimiro1 ! Yes this is great, this makes the component much more generic. I am refactoring the code as per your advice above. Additionally want to add something like wrapper for the encoder/decoder so that it can be an equivalent of the AWS Lambda SDK's btw Merry Chrismast ! Still tidying a bit and designing some wrapper.
|
…d for AWS APIGateway event handling, as if we return error not nil to lambda it shall treated as error, and the API response would not be pretty
Hi @dimiro1 - have updated the PR, could you help to share your input/feedback ? |
From my POV these What do you think @peterbourgon and @ChrisHines? |
😄 Hi @dimiro1, @peterbourgon, @ChrisHines - let me know if there is anything I can assist with, would love to refactor my stuff if this is ok |
I'm still away for the holidays, I'll take a look in a week or so. |
Thanks @peterbourgon have a good holiday ! |
// ready to be sent as AWS lambda response. | ||
type EncodeResponseFunc func(context.Context, interface{}) ([]byte, error) | ||
|
||
// ErrorEncoder is responsible for encoding an error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To what? Is the []byte returned just like a response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are u referring to ErrorEncoder
? if it is - then yes, the []byte is for controlling the response, for example in the context of building an API, we might want to response gracefully with a body of JSON describing detail of the error to Consumer.
transport/awslambda/wrapper.go
Outdated
// The decoderSymbol function signature has to receive 2 args, which is the | ||
// context.Context and event request to decode. | ||
// It has also to return 2 values, the user-domain object and error. | ||
func DecodeRequestWrapper(decoderSymbol interface{}) DecodeRequestFunc { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what the functions in wrapper.go do, but I don't understand why they're useful. Can you provide some more context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation is to bring the Decode
function purpose closer as decoder to user-domain object.
Without the wrapper function, in some use cases, the user actually needs to decode for 2 purposes, first into the AWS corresponding events, then next they decode it further to their business domain.
The idea actually coming from the lambda.Start()
in aws-lambda-go. With this convenient function it become so simple to implement a lambda handler.
func show(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
...
}
func main() {
lambda.Start(show)
}
With the wrapper it could help the consumer to focus on converting their user-domain from the familiar AWS events package.
// DecodeUppercaseRequest is a decoder of the JSON body into upperRequest.
func DecodeUppercaseRequest(
_ context.Context,
apigwReq events.APIGatewayProxyRequest,
) (
UppercaseRequest,
error,
) {
request := UppercaseRequest{}
err := json.Unmarshal([]byte(apigwReq.Body), &request)
return request, err
}
and start the server like this
uppercaseHandler := awslambdatransport.NewServer(
stringsvc.MakeUppercaseEndpoint(svc),
awslambdatransport.DecodeRequestWrapper(stringsvc.DecodeUppercaseRequest),
awslambdatransport.EncodeResponseWrapper(stringsvc.EncodeResponse),
awslambdatransport.ServerErrorEncoder(
awslambdatransport.ErrorEncoderWrapper(stringsvc.EncodeError),
),
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the use case for not using this wrapper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use case for not using wrapper:
- If performance matters more than convenient. As the wrapper is using reflections.
- Provide the most possible open for extension. For example if we want to provide custom message format into Lambda.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, thanks for the clarification. I'm not OK with a function in the public API that takes an empty interface, which must be one of a pre-ordained number of function types. I understand this is how the AWS lambda.Start function works, but that doesn't excuse the bad practice.
I'd like to see this whole layer removed. The most obvious way I can think of is to provide separate, explicit wrapper functions for each of the supported function signatures. If there were a way to remove the need for this altogether, I'd prefer that, but I can't really judge if there's an acceptable way to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point, yes you are right with API that takes empty interface{} its actually not trivial to understand its usage just from looking at the package itself, only if one see a whole lot of example of usages which means the package cant stand by its own.
OK - removing the wrapper. Thanks @peterbourgon !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated 5a7cbf4
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
transport/awslambda/server.go
Outdated
) | ||
|
||
// Server wraps an endpoint. | ||
type Server struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to call this struct Handler
since this is the interface it implements. The rpc server is started by the lambda sdk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are totally right ! Thanks, refactoring it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated here: 6a69716
transport/awslambda/server.go
Outdated
|
||
if resp, err = s.enc(ctx, response); err != nil { | ||
s.logger.Log("err", err) | ||
if s.errorEncoder != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not initialize errorEncoder
to a noop and eliminate the need for the nil
checks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep that makes it prettier, and will name it DefaultErrorEncoder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated 19d5afe
transport/awslambda/handler.go
Outdated
enc EncodeResponseFunc, | ||
options ...HandlerOption, | ||
) *Handler { | ||
s := &Handler{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably now h
instead of s
. Same goes for the methods below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated e831b16
transport/awslambda/handler.go
Outdated
if resp, err = s.enc(ctx, response); err != nil { | ||
s.logger.Log("err", err) | ||
resp, err = s.errorEncoder(ctx, err) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taste: Maybe now just return s.errorEncoder(ctx, err)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep that looks nicer - updated 1be9848
|
||
// DefaultErrorEncoder defines the default behavior of encoding an error response, | ||
// where it returns nil, and the error itself. | ||
func DefaultErrorEncoder(ctx context.Context, err error) ([]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this code is now uncovered by the tests? Though I obviously don't expect it to have bugs 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add it in 1a31338
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com>
Thanks for the contribution and sorry for the delay! |
Follows up on issue #814, this is a proposal code on transport layer for building API service with AWS Serverless stack.
Kindly help to peruse, as for me personally this is very useful addition for developing in serverless stack.
Based on discussion with @dimiro1 we expand the scope of this PR to provide a more generic transport layer for awslambda by relying on lambda.Handler interface. Therefore the title is updated into
#814 Add Transport Layer: AWS Lambda