Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/check-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
schedule:
- cron: "0 16 * * 1-5" # min h d Mo DoW / 9am PST M-F

permissions:
issues: write

jobs:
check-for-vulnerabilities:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/integ-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
- develop
- main

permissions:
contents: read

jobs:
go-tests:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ on:
description: "Information about the release"
required: true
default: "New release"
permissions:
contents: write

jobs:
Release:
environment: Release
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/aws/aws-lambda-runtime-interface-emulator

go 1.25
go 1.25.7

require (
github.com/aws/aws-lambda-go v1.46.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ func (h *HTTPHandler) invoke(w http.ResponseWriter, r *http.Request) {
return
}

invokeReq := rieinvoke.NewRieInvokeRequest(r, w)
invokeReq, err := rieinvoke.NewRieInvokeRequest(r, w)
if err != nil {
h.respondWithError(w, err)
return
}
ctx := logging.WithInvokeID(r.Context(), invokeReq.InvokeID())

metrics := invoke.NewInvokeMetrics(nil, &noOpCounter{})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package invoke

const (
RequestIdHeader = "X-Amzn-RequestId"

ClientContextHeader = "X-Amz-Client-Context"

CognitoIdentityHeader = "X-Amz-Cognito-Identity"
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
package invoke

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"time"

Expand All @@ -16,6 +20,11 @@ import (
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda-managed-instances/rapid/model"
)

type cognitoIdentity struct {
CognitoIdentityID string `json:"cognitoIdentityId"`
CognitoIdentityPoolID string `json:"cognitoIdentityPoolId"`
}

type rieInvokeRequest struct {
request *http.Request
writer http.ResponseWriter
Expand All @@ -35,18 +44,47 @@ type rieInvokeRequest struct {
functionVersionID string
}

func NewRieInvokeRequest(request *http.Request, writer http.ResponseWriter) *rieInvokeRequest {
func NewRieInvokeRequest(request *http.Request, writer http.ResponseWriter) (*rieInvokeRequest, model.AppError) {

contentType := request.Header.Get(invoke.СontentTypeHeader)
if contentType == "" {
contentType = "application/json"
}

invokeID := request.Header.Get("X-Amzn-RequestId")
invokeID := request.Header.Get(RequestIdHeader)
if invokeID == "" {
invokeID = uuid.New().String()
}

clientContext := ""
if encodedClientContext := request.Header.Get(ClientContextHeader); encodedClientContext != "" {
decodedClientContext, err := base64.StdEncoding.DecodeString(encodedClientContext)
if err != nil {
slog.Warn("Failed to decode X-Amz-Client-Context header", "err", err)
return nil, model.NewClientError(
fmt.Errorf("X-Amz-Client-Context must be a valid base64 encoded string: %w", err),
model.ErrorSeverityInvalid,
model.ErrorMalformedRequest,
)
}
clientContext = string(decodedClientContext)
}

var cognitoIdentityId, cognitoIdentityPoolId string
if cognitoIdentityHeader := request.Header.Get(CognitoIdentityHeader); cognitoIdentityHeader != "" {
var cognito cognitoIdentity
if err := json.Unmarshal([]byte(cognitoIdentityHeader), &cognito); err != nil {
slog.Warn("Failed to parse X-Amz-Cognito-Identity header", "err", err)
return nil, model.NewClientError(
fmt.Errorf("X-Amz-Cognito-Identity must be a valid JSON string: %w", err),
model.ErrorSeverityInvalid,
model.ErrorMalformedRequest,
)
}
cognitoIdentityId = cognito.CognitoIdentityID
cognitoIdentityPoolId = cognito.CognitoIdentityPoolID
}

req := &rieInvokeRequest{
request: request,
writer: writer,
Expand All @@ -56,13 +94,13 @@ func NewRieInvokeRequest(request *http.Request, writer http.ResponseWriter) *rie
responseBandwidthRate: 2 * 1024 * 1024,
responseBandwidthBurstSize: 6 * 1024 * 1024,
traceId: request.Header.Get(invoke.TraceIdHeader),
cognitoIdentityId: "",
cognitoIdentityPoolId: "",
clientContext: request.Header.Get("X-Amz-Client-Context"),
cognitoIdentityId: cognitoIdentityId,
cognitoIdentityPoolId: cognitoIdentityPoolId,
clientContext: clientContext,
responseMode: request.Header.Get(invoke.ResponseModeHeader),
}

return req
return req, nil
}

func (r *rieInvokeRequest) ContentType() string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda-managed-instances/rapid/model"
)

func TestNewRieInvokeRequest(t *testing.T) {
tests := []struct {
name string
request func() *http.Request
writer http.ResponseWriter
want *rieInvokeRequest
name string
request func() *http.Request
writer http.ResponseWriter
want *rieInvokeRequest
wantError bool
wantErrorContain string
}{
{
name: "no_headers_in_request",
Expand All @@ -37,6 +41,7 @@ func TestNewRieInvokeRequest(t *testing.T) {
cognitoIdentityPoolId: "",
clientContext: "",
},
wantError: false,
},
{
name: "all_headers_present_in_request",
Expand All @@ -46,6 +51,7 @@ func TestNewRieInvokeRequest(t *testing.T) {
r.Header.Set("X-Amzn-Trace-Id", "Root=1-5e1b4151-5ac6c58f3375aa3c7c6b73c9")
r.Header.Set("X-Amz-Client-Context", "eyJjdXN0b20iOnsidGVzdCI6InZhbHVlIn19")
r.Header.Set("X-Amzn-RequestId", "test-invoke-id")
r.Header.Set("X-Amz-Cognito-Identity", `{"cognitoIdentityId":"us-east-1:12345678-1234-1234-1234-123456789012","cognitoIdentityPoolId":"us-east-1:87654321-4321-4321-4321-210987654321"}`)
require.NoError(t, err)
return r
},
Expand All @@ -57,16 +63,80 @@ func TestNewRieInvokeRequest(t *testing.T) {
responseBandwidthRate: 2 * 1024 * 1024,
responseBandwidthBurstSize: 6 * 1024 * 1024,
traceId: "Root=1-5e1b4151-5ac6c58f3375aa3c7c6b73c9",
cognitoIdentityId: "",
cognitoIdentityId: "us-east-1:12345678-1234-1234-1234-123456789012",
cognitoIdentityPoolId: "us-east-1:87654321-4321-4321-4321-210987654321",
clientContext: `{"custom":{"test":"value"}}`,
},
wantError: false,
},
{
name: "malformed_cognito_identity_header",
request: func() *http.Request {
r, err := http.NewRequest("GET", "http://localhost/", nil)
r.Header.Set("X-Amzn-RequestId", "test-invoke-id")
r.Header.Set("X-Amz-Cognito-Identity", "not-valid-json{")
require.NoError(t, err)
return r
},
writer: httptest.NewRecorder(),
want: nil,
wantError: true,
wantErrorContain: "X-Amz-Cognito-Identity must be a valid JSON string",
},
{
name: "malformed_client_context_header",
request: func() *http.Request {
r, err := http.NewRequest("GET", "http://localhost/", nil)
r.Header.Set("X-Amzn-RequestId", "test-invoke-id")
r.Header.Set("X-Amz-Client-Context", "not-valid-base64!!!")
require.NoError(t, err)
return r
},
writer: httptest.NewRecorder(),
want: nil,
wantError: true,
wantErrorContain: "X-Amz-Client-Context must be a valid base64 encoded string",
},
{
name: "partial_cognito_identity_header",
request: func() *http.Request {
r, err := http.NewRequest("GET", "http://localhost/", nil)
r.Header.Set("X-Amzn-RequestId", "test-invoke-id")
r.Header.Set("X-Amz-Cognito-Identity", `{"cognitoIdentityId":"us-east-1:only-id"}`)
require.NoError(t, err)
return r
},
writer: httptest.NewRecorder(),
want: &rieInvokeRequest{
invokeID: "test-invoke-id",
contentType: "application/json",
maxPayloadSize: 6*1024*1024 + 100,
responseBandwidthRate: 2 * 1024 * 1024,
responseBandwidthBurstSize: 6 * 1024 * 1024,
traceId: "",
cognitoIdentityId: "us-east-1:only-id",
cognitoIdentityPoolId: "",
clientContext: "eyJjdXN0b20iOnsidGVzdCI6InZhbHVlIn19",
clientContext: "",
},
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := tt.request()
got := NewRieInvokeRequest(r, tt.writer)
got, err := NewRieInvokeRequest(r, tt.writer)

if tt.wantError {
assert.NotNil(t, err)
assert.Nil(t, got)
assert.Equal(t, model.ErrorMalformedRequest, err.ErrorType())
assert.Equal(t, http.StatusBadRequest, err.ReturnCode())
assert.Contains(t, err.Error(), tt.wantErrorContain)
return
}

assert.Nil(t, err)
require.NotNil(t, got)

tt.want.request = r
tt.want.writer = tt.writer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

//go:build test

package test

import (
Expand Down
8 changes: 4 additions & 4 deletions internal/lambda-managed-instances/interop/error_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ func TestBuildStatusFromError(t *testing.T) {
expected ResponseStatus
}{
{
name: "nil error",
name: "nilError",
err: nil,
expected: Success,
},
{
name: "sandbox timeout error",
name: "sandboxTimeoutError",
err: model.NewCustomerError(model.ErrorSandboxTimedout),
expected: Timeout,
},
{
name: "customer error",
name: "customerError",
err: model.NewCustomerError(model.ErrorFunctionUnknown),
expected: Error,
},
{
name: "runtime error",
name: "platformError",
err: model.NewPlatformError(nil, model.ErrorReasonUnknownError),
expected: Failure,
},
Expand Down
2 changes: 1 addition & 1 deletion internal/lambda-managed-instances/invoke/invoke_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (ir *InvokeRouter) Invoke(ctx context.Context, initData interop.InitStaticD

if !ir.runningInvokes.SetIfAbsent(invokeReq.InvokeID(), idleRuntime) {
logging.Warn(ctx, "InvokeRouter error: duplicated invokeId")
return model.NewClientError(ErrInvokeIdAlreadyExists, model.ErrorSeverityError, model.ErrorDublicatedInvokeId), false
return model.NewClientError(ErrInvokeIdAlreadyExists, model.ErrorSeverityError, model.ErrorDuplicatedInvokeId), false
}

defer ir.runningInvokes.Remove(invokeReq.InvokeID())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestInvokeFailure_DublicatedInvokeId(t *testing.T) {

err = <-ch
assert.Error(t, err)
assert.Equal(t, model.ErrorDublicatedInvokeId, err.ErrorType())
assert.Equal(t, model.ErrorDuplicatedInvokeId, err.ErrorType())

close(respChannel)
err = <-ch
Expand Down
3 changes: 2 additions & 1 deletion internal/lambda-managed-instances/invoke/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ func (e *invokeMetrics) buildMetrics() []servicelogs.Metric {
switch e.error.(type) {
case model.ClientError:
clientErrCnt = 1
if e.error.ErrorType() != model.ErrorRuntimeUnavailable {
if e.error.ErrorType() != model.ErrorRuntimeUnavailable &&
e.error.ErrorType() != model.ErrorDuplicatedInvokeId {

nonCustomerErrCnt = 1
}
Expand Down
28 changes: 28 additions & 0 deletions internal/lambda-managed-instances/invoke/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ func Test_invokeMetrics_ServiceLogs(t *testing.T) {
{Type: servicelogs.CounterType, Key: "NonCustomerError", Value: 0},
},
},
{
name: "duplicated_invoke_id_error",
expectedBytes: 0,
metricFlow: func(ev *invokeMetrics, mocks *invokeMetricsMocks) {
mocks.timeStamp = mocks.timeStamp.Add(time.Second)
ev.AttachInvokeRequest(&mocks.invokeReq)
ev.AttachDependencies(&mocks.initData, &mocks.eventsApi)
ev.UpdateConcurrencyMetrics(5, 3)
mocks.error = model.NewClientError(nil, model.ErrorSeverityError, model.ErrorDuplicatedInvokeId)
},
expectedProps: []servicelogs.Property{
{Name: "RequestId", Value: "invoke-id"},
},
expectedDims: []servicelogs.Dimension{
{Name: "RequestMode", Value: "Streaming"},
},
expectedMetrics: []servicelogs.Metric{
{Type: servicelogs.TimerType, Key: "TotalDuration", Value: 1000000},
{Type: servicelogs.CounterType, Key: "InflightRequestCount", Value: 5},
{Type: servicelogs.CounterType, Key: "IdleRuntimesCount", Value: 3},
{Type: servicelogs.TimerType, Key: "PlatformOverheadDuration", Value: 1000000},
{Type: servicelogs.CounterType, Key: "ClientError", Value: 1},
{Type: servicelogs.CounterType, Key: "CustomerError", Value: 0},
{Type: servicelogs.CounterType, Key: "PlatformError", Value: 0},
{Type: servicelogs.CounterType, Key: "ClientErrorReason-Client.DuplicatedInvokeId", Value: 1},
{Type: servicelogs.CounterType, Key: "NonCustomerError", Value: 0},
},
},
{
name: "runtime_timeout_flow",
expectedBytes: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const (
ErrorInitIncomplete ErrorType = "Client.InitIncomplete"
ErrorEnvironmentUnhealthy ErrorType = "Client.ExecutionEnvironmentUnhealthy"
ErrorRuntimeUnavailable ErrorType = "Runtime.Unavailable"
ErrorDublicatedInvokeId ErrorType = "Client.DuplicatedInvokeId"
ErrorDuplicatedInvokeId ErrorType = "Client.DuplicatedInvokeId"
ErrorInvalidFunctionVersion ErrorType = "ErrInvalidFunctionVersion"
ErrorInvalidMaxPayloadSize ErrorType = "ErrInvalidMaxPayloadSize"
ErrorInvalidResponseBandwidthRate ErrorType = "ErrInvalidResponseBandwidthRate"
Expand Down
Loading