From 3b810b1db634116af8861a8c164e1a66d0c0c9da Mon Sep 17 00:00:00 2001 From: Universe Ops Date: Wed, 7 Oct 2026 22:05:17 +0300 Subject: [PATCH] fix(awsutil): carry IsBase64Encoded through ToLambdaFunctionURLResponse In BUFFERED invoke mode the response is rendered by aws-lambda-go-api-proxy, whose writer base64-encodes any body that is not valid UTF-8 and reports it via IsBase64Encoded. ToLambdaFunctionURLResponse copied the (already-encoded) Body but dropped the flag, so the Function URL delivered the base64 text itself to the client instead of the bytes. The request direction already decodes correctly (ToAPIGatewayRequest), which is why binary uploads work and only responses were affected. Latent so far because the two BUFFERED services in the fleet (forge-storage, forge-notifier) only ever return 302s and JSON, and utf8.Valid lets all text through unencoded. RESPONSE_STREAM services take the its-felix adapter, which handles the flag itself. --- pkg/awsutil/lambda.go | 8 +++-- pkg/awsutil/lambda_test.go | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 pkg/awsutil/lambda_test.go diff --git a/pkg/awsutil/lambda.go b/pkg/awsutil/lambda.go index 46b0f64..2c22e7c 100644 --- a/pkg/awsutil/lambda.go +++ b/pkg/awsutil/lambda.go @@ -19,8 +19,12 @@ func ToLambdaFunctionURLResponse(res events.APIGatewayProxyResponse) events.Lamb } return "" }), - Body: res.Body, - StatusCode: res.StatusCode, + // IsBase64Encoded MUST be carried over. The gin/apigw-proxy response writer + // base64-encodes any body that is not valid UTF-8 and sets this flag; dropping + // it here delivers a binary body to the client as literal base64 text. + Body: res.Body, + IsBase64Encoded: res.IsBase64Encoded, + StatusCode: res.StatusCode, } } diff --git a/pkg/awsutil/lambda_test.go b/pkg/awsutil/lambda_test.go new file mode 100644 index 0000000..92b7ad3 --- /dev/null +++ b/pkg/awsutil/lambda_test.go @@ -0,0 +1,62 @@ +package awsutil + +import ( + "encoding/base64" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aws/aws-lambda-go/events" +) + +// A BUFFERED-mode Lambda renders its response through aws-lambda-go-api-proxy, +// whose writer base64-encodes any body that is not valid UTF-8 and reports it +// via IsBase64Encoded. Dropping the flag during the conversion delivers the +// base64 text itself to the client instead of the bytes, so the round trip has +// to preserve it. +func TestToLambdaFunctionURLResponsePreservesBase64Flag(t *testing.T) { + // A 1x1 transparent PNG — genuinely non-UTF-8, which is what trips the + // proxy writer's utf8.Valid branch. + png := []byte{ + 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, + 0x00, 0x00, 0x00, 0x0d, 'I', 'H', 'D', 'R', + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, + 0x89, + } + encoded := base64.StdEncoding.EncodeToString(png) + + res := ToLambdaFunctionURLResponse(events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + Body: encoded, + IsBase64Encoded: true, + MultiValueHeaders: map[string][]string{ + "Content-Type": {"image/png"}, + }, + }) + + assert.True(t, res.IsBase64Encoded, "binary response must stay marked as base64") + assert.Equal(t, encoded, res.Body) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "image/png", res.Headers["Content-Type"]) + + decoded, err := base64.StdEncoding.DecodeString(res.Body) + require.NoError(t, err) + assert.Equal(t, png, decoded, "the client must be able to recover the exact bytes") +} + +func TestToLambdaFunctionURLResponseLeavesTextUnencoded(t *testing.T) { + res := ToLambdaFunctionURLResponse(events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + Body: `{"ok":true}`, + IsBase64Encoded: false, + MultiValueHeaders: map[string][]string{ + "Content-Type": {"application/json"}, + }, + }) + + assert.False(t, res.IsBase64Encoded) + assert.Equal(t, `{"ok":true}`, res.Body) +}