Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions pkg/lambda/grpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ import (
"google.golang.org/protobuf/proto"
)

// lambdaInvoker is the subset of *lambda.Client that the transport needs. It
// exists so RoundTrip can be unit-tested against a fake invoker.
type lambdaInvoker interface {
Invoke(ctx context.Context, params *lambda.InvokeInput, optFns ...func(*lambda.Options)) (*lambda.InvokeOutput, error)
}

type lambdaTransport struct {
lambdaClient *lambda.Client
lambdaClient lambdaInvoker
functionName string
}

Expand Down Expand Up @@ -68,11 +74,16 @@ func (l *lambdaTransport) RoundTrip(ctx context.Context, req *Request) (*Respons
}
// If a third case is ever added to this, put the logic in its own function and add some test cases.

// The function was invoked but its own code returned an error (vs. an
// infra/invoke failure above). That's a caller/function-state fault, not a
// server fault: code it FailedPrecondition so it doesn't surface as a
// gRPC Unknown (which otel counts as a 5xx).
if filteredLogs != "" {
return nil, fmt.Errorf("%s", filteredLogs)
return nil, status.Errorf(codes.FailedPrecondition, "%s", filteredLogs)
}

return nil, fmt.Errorf(
return nil, status.Errorf(
codes.FailedPrecondition,
Comment thread
arreyder marked this conversation as resolved.
"lambda_transport: function returned error: %s; status code: %d",
*invokeResp.FunctionError,
invokeResp.StatusCode,
Expand Down
55 changes: 55 additions & 0 deletions pkg/lambda/grpc/client_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,66 @@
package grpc

import (
"context"
"encoding/base64"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type fakeInvoker struct {
resp *lambda.InvokeOutput
err error
}

func (f *fakeInvoker) Invoke(_ context.Context, _ *lambda.InvokeInput, _ ...func(*lambda.Options)) (*lambda.InvokeOutput, error) {
return f.resp, f.err
}

// A function that runs but returns an error (FunctionError set) is a
// caller/function-state fault, not a server fault — RoundTrip must surface it
// as FailedPrecondition so it never shows up as a gRPC Unknown (counted 5xx).
func TestRoundTrip_FunctionErrorIsFailedPrecondition(t *testing.T) {
cases := []struct {
name string
payload string
logs string
}{
{
name: "no meaningful logs",
payload: "{}",
logs: "START RequestId: abc\nEND RequestId: abc\n",
},
{
name: "meaningful logs present",
payload: "{}",
logs: "Unhandled: connector failed to initialize\n",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
tr := &lambdaTransport{
functionName: "fn",
lambdaClient: &fakeInvoker{resp: &lambda.InvokeOutput{
FunctionError: aws.String("Unhandled"),
StatusCode: 200,
Payload: []byte(c.payload),
LogResult: aws.String(base64.StdEncoding.EncodeToString([]byte(c.logs))),
}},
}

_, err := tr.RoundTrip(context.Background(), &Request{})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition, status.Code(err), "function-returned error must be FailedPrecondition, got: %v", err)
})
}
}

func TestExtractMeaningfulLogLines(t *testing.T) {
cases := []struct {
name string
Expand Down
Loading