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
214 changes: 99 additions & 115 deletions README.md

Large diffs are not rendered by default.

40 changes: 21 additions & 19 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,48 @@ var (
// An absent header and a nil or empty value slice are all missing.
ErrActionMissing = errors.New("edge: missing action")

// ErrActionAmbiguous means the header has multiple represented values or a
// value containing a comma. Gateway payload 2.0 combines duplicates with
// commas; a literal comma cannot be distinguished from combined values.
// ErrActionAmbiguous means the header has multiple represented values or a value containing a comma.
// Gateway payload 2.0 combines duplicates with commas;
// a literal comma cannot be distinguished from combined values.
ErrActionAmbiguous = errors.New("edge: ambiguous action")

// ErrActionInvalid means the single action value is empty after trimming
// outer spaces and tabs, or does not satisfy the HTTP token grammar.
// ErrActionInvalid means the single action value is empty after trimming outer spaces and tabs, or does not satisfy the HTTP token grammar.
ErrActionInvalid = errors.New("edge: invalid action")
)

// ActionHeader selects an action from a configured request header.
// Construct it with [NewActionHeader]; its zero value is unconfigured.
// Construct it with [NewActionHeader];
// its zero value is unconfigured.
// An ActionHeader is immutable, safe to copy, and safe for concurrent use.
type ActionHeader struct {
name string
}

// NewActionHeader validates name as an HTTP field name and returns a selector.
// The name is required and matched case-insensitively; no default is inferred.
// On error, the returned selector is the zero value. Configuration errors are
// separate from [ErrActionMissing], [ErrActionAmbiguous] and [ErrActionInvalid].
// The name is required and matched case-insensitively;
// no default is inferred.
// On error, the returned selector is the zero value.
// Configuration errors are separate from [ErrActionMissing], [ErrActionAmbiguous] and [ErrActionInvalid].
func NewActionHeader(name string) (ActionHeader, error) {
if !validToken(name) {
return ActionHeader{}, errors.New("edge: invalid action header name")
}
return ActionHeader{name: http.CanonicalHeaderKey(name)}, nil
}

// Parse returns the single action in headers, trimming only outer SP/HTAB and
// preserving case. An action must be a nonempty HTTP token (RFC 9110 §5.6.2).
// Parse returns the single action in headers, trimming only outer SP/HTAB and preserving case.
// An action must be a nonempty HTTP token (RFC 9110 §5.6.2).
// Parse inspects all case-insensitive aliases, including noncanonical map keys.
// Multiple values are ambiguous even if identical; a comma is also ambiguous.
// Multiple values are ambiguous even if identical;
// a comma is also ambiguous.
// Multiplicity and comma checks take precedence over token validation.
// Duplicates already lost by a gateway or earlier proxy cannot be detected.
//
// On failure Parse returns an empty string and [ErrActionMissing],
// [ErrActionAmbiguous] or [ErrActionInvalid], matchable with [errors.Is]. An
// unconfigured selector instead returns a configuration error. Errors do not
// include supplied header values. Parse does not modify headers or write an
// HTTP response. Callers must not mutate headers concurrently with Parse.
// On failure Parse returns an empty string and [ErrActionMissing], [ErrActionAmbiguous] or [ErrActionInvalid], matchable with [errors.Is].
// An unconfigured selector instead returns a configuration error.
// Errors do not include supplied header values.
// Parse does not modify headers or write an HTTP response.
// Callers must not mutate headers concurrently with Parse.
// Select once and use that same action for authorization and execution.
func (h ActionHeader) Parse(headers http.Header) (string, error) {
if h.name == "" {
Expand All @@ -59,8 +61,8 @@ func (h ActionHeader) Parse(headers http.Header) (string, error) {
var value string
found := false
for name, values := range headers {
// CanonicalHeaderKey folds only valid ASCII field names. In particular,
// Unicode case equivalents must not alias the configured HTTP name.
// CanonicalHeaderKey folds only valid ASCII field names.
// In particular, Unicode case equivalents must not alias the configured HTTP name.
if http.CanonicalHeaderKey(name) != h.name || len(values) == 0 {
continue
}
Expand Down
4 changes: 2 additions & 2 deletions action_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func ExampleActionHeader() {
http.Error(w, "missing action metadata", http.StatusInternalServerError)
return
}
// This example operation is public. Protected operations must authorize
// this same selection before executing or publishing streaming output.
// This example operation is public.
// Protected operations must authorize this same selection before executing or publishing streaming output.
switch action {
case "service.ping":
fmt.Fprintln(w, "pong")
Expand Down
4 changes: 2 additions & 2 deletions action_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func actionGatewayRequest(t *testing.T, format string, values []string) *http.Re
v1.Headers = map[string]string{"Custom-Trace": "keep"}
v2.Headers = map[string]string{"custom-trace": "keep"}
if len(values) != 0 {
// A value mirrored by V1's single and multivalue maps is not an extra
// field line. Actual repeats in the multivalue list must survive.
// A value mirrored by V1's single and multivalue maps is not an extra field line.
// Actual repeats in the multivalue list must survive.
v1.Headers["action"] = values[0]
v1.MultiValueHeaders = map[string][]string{"ACTION": values}
v2.Headers["action"] = strings.Join(values, ",")
Expand Down
63 changes: 36 additions & 27 deletions adapter.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Package edge adapts AWS API Gateway Lambda proxy events to net/http handlers.
// It supports raw JSON v2 and already typed AWS events with optional native
// gateway identity. Buffered and REST streaming responses share request and
// identity policies; each has its own response and invocation boundary.
// It supports raw JSON v2 and already typed AWS events with optional native gateway identity.
// Buffered and REST streaming responses share request and identity policies;
// each has its own response and invocation boundary.
package edge

import (
Expand All @@ -13,8 +13,10 @@ import (
)

// Adapter holds an HTTP handler and its API Gateway adaptation configuration.
// Construct an Adapter with [New]; its zero value is not usable.
// Configuration is fixed at construction. Calls own separate request state;
// Construct an Adapter with [New];
// its zero value is not usable.
// Configuration is fixed at construction.
// Calls own separate request state;
// a shared handler must be safe for concurrent use.
type Adapter struct {
handler http.Handler
Expand All @@ -30,14 +32,17 @@ type config struct {
}

// Option configures an [Adapter] or [StreamingAdapter] during construction.
// Use the option functions provided by this package. A nil Option is invalid.
// Options apply in argument order; the last assignment to a scalar setting wins.
// Use the option functions provided by this package.
// A nil Option is invalid.
// Options apply in argument order;
// the last assignment to a scalar setting wins.
type Option func(*config)

// WithGatewayIdentity configures whether gateway assertions establish identity.
// The default is false. When enabled, the policy covers native IAM and
// JWT/Cognito assertions; it does not configure authorization in AWS or enable
// local token verification. Custom-authorizer contexts need an explicit mapper.
// The default is false.
// When enabled, the policy covers native IAM and JWT/Cognito assertions;
// it does not configure authorization in AWS or enable local token verification.
// Custom-authorizer contexts need an explicit mapper.
// Unsupported or conflicting assertions fail before running the HTTP handler.
func WithGatewayIdentity(enabled bool) Option {
return func(c *config) {
Expand All @@ -46,36 +51,40 @@ func WithGatewayIdentity(enabled bool) Option {
}

// WithIdentityClaimsBudget sets the gateway identity claims allowance in bytes.
// The default is 256 KiB; New and NewStreaming reject settings outside 1..6 MiB.
// The default is 256 KiB;
// New and NewStreaming reject settings outside 1..6 MiB.
// Each claim value costs 64 bytes plus name, string and exact-number text bytes.
// Dedicated gateway scopes consume the same allowance. Raw claim JSON is also
// limited to this byte length. This is a resource policy, not a heap cap or AWS
// quota. It does not enable gateway identity.
// Dedicated gateway scopes consume the same allowance.
// Raw claim JSON is also limited to this byte length.
// This is a resource policy, not a heap cap or AWS quota.
// It does not enable gateway identity.
func WithIdentityClaimsBudget(bytes int) Option {
return func(c *config) {
c.identityClaimsBudget = bytes
}
}

// WithResponseHeaderBudget sets the response header resource budget in bytes.
// The default is 256 KiB. Both constructors reject settings outside 1..6 MiB;
// zero does not disable the bound. Each original value costs its name length,
// value length and 32 bytes; nil/empty slices cost their name length and 32.
// Generated headers also consume the budget. This is a library resource policy,
// not an AWS quota or a bound on total heap usage. It does not change the separate
// buffered envelope or streaming metadata limits.
// The default is 256 KiB.
// Both constructors reject settings outside 1..6 MiB;
// zero does not disable the bound.
// Each original value costs its name length, value length and 32 bytes;
// nil/empty slices cost their name length and 32.
// Generated headers also consume the budget.
// This is a library resource policy, not an AWS quota or a bound on total heap usage.
// It does not change the separate buffered envelope or streaming metadata limits.
func WithResponseHeaderBudget(bytes int) Option {
return func(c *config) {
c.responseHeaderBudget = bytes
}
}

// New validates handler and opts and returns an Adapter without performing I/O
// or invoking handler. Nil handlers, including typed nil handlers, and nil
// options are rejected. On error, the returned Adapter is nil.
// New validates handler and opts and returns an Adapter without performing I/O or invoking handler.
// Nil handlers, including typed nil handlers, and nil options are rejected.
// On error, the returned Adapter is nil.
//
// The caller retains ownership of handler and is responsible for its concurrency
// safety when sharing it. New does not use [http.DefaultServeMux].
// The caller retains ownership of handler and is responsible for its concurrency safety when sharing it.
// New does not use [http.DefaultServeMux].
func New(handler http.Handler, opts ...Option) (*Adapter, error) {
cfg, err := configureAdapter(handler, opts)
if err != nil {
Expand All @@ -91,8 +100,8 @@ func configureAdapter(handler http.Handler, opts []Option) (config, error) {
if handler == nil {
return config{}, errors.New("edge: nil HTTP handler")
}
// An interface containing a typed nil is non-nil. Check nil-capable kinds
// before accepting a handler that would defer a startup fault to invocation.
// An interface containing a typed nil is non-nil.
// Check nil-capable kinds before accepting a handler that would defer a startup fault to invocation.
v := reflect.ValueOf(handler)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Slice:
Expand Down
16 changes: 8 additions & 8 deletions adapter_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func ExampleAdapter_Invoke() {
if err != nil {
panic(err)
}
// Production main calls lambda.Start(adapter). Passing adapter.Invoke as a
// reflected function would give []byte ordinary JSON/base64 semantics.
// Production main calls lambda.Start(adapter).
// Passing adapter.Invoke as a reflected function would give []byte ordinary JSON/base64 semantics.
handler := lambda.NewHandler(adapter)
response, err := handler.Invoke(context.Background(), []byte(`{"httpMethod":"GET","path":"/","requestContext":{"apiId":"example"}}`))
fmt.Println(len(response) > 0, err)
Expand All @@ -33,13 +33,13 @@ func ExampleAdapter_HandleV2() {
if err != nil {
panic(err)
}
// Production main may call lambda.Start(adapter.HandleV2). Its runtime then
// owns envelope JSON. This direct typed call performs no envelope JSON work.
// Production main may call lambda.Start(adapter.HandleV2).
// Its runtime then owns envelope JSON.
// This direct typed call performs no envelope JSON work.
response, err := adapter.HandleV2(context.Background(), events.APIGatewayV2HTTPRequest{
Version: "2.0", RawPath: "/",
RequestContext: events.APIGatewayV2HTTPRequestContext{
APIID: "example", HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{Method: "POST"},
},
Version: "2.0",
RawPath: "/",
RequestContext: events.APIGatewayV2HTTPRequestContext{APIID: "example", HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{Method: "POST"}},
})
fmt.Println(response.StatusCode, err)
// Output: 202 <nil>
Expand Down
31 changes: 16 additions & 15 deletions adapter_invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"github.com/asteroid-computing/go-lambda-edge/identity"
)

// Invoke adapts a REST proxy, HTTP API 1.0 or HTTP API 2.0 event using JSON v2
// directly. Register the Adapter object with lambda.Start(adapter), not its
// Invoke method as a reflected function. Transport/identity failures return no
// usable payload; ordinary HTTP error responses remain successful invocations.
// Panics propagate after cleanup. The caller must not mutate payload during use.
// Invoke adapts a REST proxy, HTTP API 1.0 or HTTP API 2.0 event using JSON v2 directly.
// Register the Adapter object with lambda.Start(adapter), not its Invoke method as a reflected function.
// Transport/identity failures return no usable payload;
// ordinary HTTP error responses remain successful invocations.
// Panics propagate after cleanup.
// The caller must not mutate payload during use.
func (a *Adapter) Invoke(ctx context.Context, payload []byte) ([]byte, error) {
if err := a.valid(); err != nil {
return nil, err
Expand Down Expand Up @@ -46,11 +47,12 @@ func (a *Adapter) Invoke(ctx context.Context, payload []byte) ([]byte, error) {
return result, nil
}

// HandleV1 adapts an already decoded REST/HTTP API 1.0 event without serializing
// envelopes inside edge. It can be registered as lambda.Start(adapter.HandleV1).
// The upstream decoder owns wire validation and numeric fidelity; the caller or
// runtime owns final response serialization and its complete envelope size.
// On failure it returns a zero response. Inputs must not change during the call.
// HandleV1 adapts an already decoded REST/HTTP API 1.0 event without serializing envelopes inside edge.
// It can be registered as lambda.Start(adapter.HandleV1).
// The upstream decoder owns wire validation and numeric fidelity;
// the caller or runtime owns final response serialization and its complete envelope size.
// On failure it returns a zero response.
// Inputs must not change during the call.
func (a *Adapter) HandleV1(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if err := a.valid(); err != nil {
return events.APIGatewayProxyResponse{}, err
Expand All @@ -70,9 +72,9 @@ func (a *Adapter) HandleV1(ctx context.Context, event events.APIGatewayProxyRequ
return result, nil
}

// HandleV2 adapts an already decoded HTTP API 2.0 event. Its ownership, error and
// serialization contract matches HandleV1. V2 SDK string claims cannot recover
// arrays or exact numeric values discarded by an upstream decoder.
// HandleV2 adapts an already decoded HTTP API 2.0 event.
// Its ownership, error and serialization contract matches HandleV1.
// V2 SDK string claims cannot recover arrays or exact numeric values discarded by an upstream decoder.
func (a *Adapter) HandleV2(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
if err := a.valid(); err != nil {
return events.APIGatewayV2HTTPResponse{}, err
Expand Down Expand Up @@ -136,8 +138,7 @@ func prepareRequest(ctx context.Context, inv *invocation, event decodedEvent, ra
return nil, invocationError("identity", ErrIdentity, "caller installation failed", identity.ErrConflict)
}
request = request.WithContext(ctx)
// Track the actual served request for multipart cleanup, retaining the
// original transport body separately even if the handler replaces it.
// Track the actual served request for multipart cleanup, retaining the original transport body separately even if the handler replaces it.
inv.request = request
}
return request, nil
Expand Down
7 changes: 5 additions & 2 deletions adapter_invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ func TestAdapterStandardHandlers(t *testing.T) {
var body, location string
if v2 {
response, err := a.HandleV2(t.Context(), events.APIGatewayV2HTTPRequest{
RawPath: path, Headers: map[string]string{"Range": "bytes=1-3"},
RawPath: path,
Headers: map[string]string{"Range": "bytes=1-3"},
RequestContext: events.APIGatewayV2HTTPRequestContext{APIID: "api", HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{Method: "GET"}},
})
if err != nil {
Expand All @@ -304,7 +305,9 @@ func TestAdapterStandardHandlers(t *testing.T) {
}
} else {
response, err := a.HandleV1(t.Context(), events.APIGatewayProxyRequest{
Path: path, HTTPMethod: "GET", Headers: map[string]string{"Range": "bytes=1-3"},
Path: path,
HTTPMethod: "GET",
Headers: map[string]string{"Range": "bytes=1-3"},
RequestContext: events.APIGatewayProxyRequestContext{APIID: "api"},
})
if err != nil {
Expand Down
Loading
Loading