edge

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

README

go-lambda-edge

A Go 1.27 module being built to serve AWS API Gateway requests through ordinary net/http handlers in AWS Lambda, with companion identity, authentication, and authorization packages.

The root package is edge. The buffered adapter is implemented for REST proxy, HTTP API payload 1.0 and HTTP API payload 2.0. It uses AWS Lambda Go v1.55.0 types and direct encoding/json/v2 processing. Raw and already typed entry points share HTTP conversion, response capture, identity policy, cancellation and cleanup.

Start with the complete orders dispatcher for verifier wiring, application grants, JSON errors and three transport entry points. The support matrix distinguishes implemented behavior, deferred scope and deployment qualifications; the testing guide shows how to test each consumer boundary.

Create an adapter with edge.New(handler, options...), handle the constructor error, then register it with lambda.Start(adapter). Pass the object, not adapter.Invoke, to retain the raw JSON v2 boundary. For a fixed payload family, use lambda.Start(adapter.HandleV1) or lambda.Start(adapter.HandleV2); these delegate envelope JSON to the SDK and do no envelope serialization inside edge. See the runnable registration examples.

Gateway identity is disabled by default. edge.WithGatewayIdentity(true) enables the reviewed native IAM and JWT/Cognito field mappings. Invalid, conflicting or unsupported assertions fail before the handler; custom-authorizer mapping remains unimplemented. Authorization headers are not independently authenticated. See the fixture coverage and fidelity qualifications.

The shared invocation error contract is implemented across those private stages: public categories support errors.Is, and errors.AsType[*edge.InvocationError] exposes sanitized operation and resource-limit diagnostics. HTTP error responses remain successful transport outcomes; operation, cleanup, cancellation, and late stream failures retain their accepted ownership and precedence. The shared claims API, caller/context construction, JWT normalization and resource limits are implemented in identity. Every invocation rejects an inherited caller, including when gateway identity is disabled. Constructors themselves do not authenticate credentials or authorize application actions.

The accepted authentication direction supports IAM credentials or an OAuth bearer token on the same route without built-in AWS_IAM authorization. IAM clients use iamproof.NewGenerator to generate a signed STS GetCallerIdentity proof; iamproof.NewVerifier submits that client-signed request to AWS and returns a verified IAM caller. Both are implemented using the latest stable SDK core v1.47.0 and STS v1.51.0, checked on 2026-09-16. See the IAM proof guide and runnable examples. The shared authn selector and required-authentication middleware are implemented. Configure Bearer and/or IAM-proof verifier functions; the authentication guide shows the required IAM error mapping and the custom dispatcher/HTTP response boundaries. authn.NewCognitoVerifier now provides RS256 access-token verification with direct JSON v2, explicit issuer/client/resource restrictions and bounded JWKS caching; see the Cognito guide. Wire its Verify method as the Bearer verifier. The authz package now provides exact IAM/JWT predicates, ordered All/Any rules and context-aware application grant checks. Consumers own their action registry, resource selection and HTTP responses; see the authorization guide and runnable dispatcher examples. No live Cognito/STS interoperability test has run.

REST response streaming is implemented through edge.NewStreaming(handler) and lambda.Start(adapter.Handle) or lambda.Start(adapter.HandleV1). It provides bounded sniffing, Flush/FlushError, raw body delivery, backpressure, cancellation, cleanup and sanitized terminal errors. Streaming never infers Content-Length. See the streaming guide and SSE, NDJSON, binary and gzip examples. Local SDK tests pass, but deployment support remains unverified: SDK v1.55.0's Runtime API mode header and connection handling differ from current AWS guidance. The deployment qualification remains open; no live API Gateway streaming test has run.

Header-selected actions

actionheader.NewSelector("Action") creates an immutable selector whose Parse(r.Header) method returns an action string or an error. The actionheader package uses only the standard library, so native HTTP servers do not link the Lambda adapter. Configure the header name explicitly. Selection preserves case, trims outer spaces/tabs, and requires a nonempty HTTP token. Repeated values and comma-bearing values are rejected, including Gateway payload 2.0's comma-combined duplicates. Use errors.Is with actionheader.ErrMissing, actionheader.ErrAmbiguous, or actionheader.ErrInvalid to distinguish request-input failures.

Use ordinary http.Handler middleware to process this and other custom headers. The consumer owns its action registry, metadata, authorization and HTTP error responses. Select once, authorize that selection, then execute it; do not select again from mutable headers. An action header does not authenticate a caller. The selector works with both ordinary Go HTTP servers and the Lambda adapter.

See the runnable middleware example and accepted header contract.

Response header budget

Response headers have a 256 KiB weighted resource budget, configurable with edge.WithResponseHeaderBudget(bytes) from 1 byte through 6 MiB. Each original value costs len(name) + len(value) + 32; nil/empty slices still cost a name plus 32, and generated headers consume remaining budget. This is separate from the complete buffered-envelope limit and the 16,000-byte streaming metadata prefix (including its delimiter). It is neither an AWS quota nor a total heap bound. Both writers apply the allowance at commitment. Raw buffered invocation also checks the complete serialized response against 6 MiB; typed callers own their final envelope serialization and size. Streaming checks the complete metadata prefix before handing off the reader; its body does not use the buffered limit.

Owned claims

identity.NewJWT and identity.NewIAM construct immutable, mutually exclusive caller views. Zero Caller is anonymous. Constructors validate facts and source attribution; they do not authenticate credentials or grant permissions. JWT issuer, subject, client ID, audience, scopes and Cognito groups stay distinct. Ambiguous gateway collections remain unavailable, while conflicting interpretable scope sets fail. IAM callers retain exact ARN paths and session names.

identity.NewContext installs one caller in a derived context and rejects replacement, even by an apparently identical caller. identity.FromContext returns anonymous when absent. See the caller examples.

identity.ParseClaims, identity.NewClaims, and identity.NewTextClaims accept raw JSON, decoded Go values, and gateway string maps respectively. Claims are immutable snapshots with checked accessors. Exact numeric text remains available where the input preserves it; a decoded float64 is never advertised as an exact original JSON number, and gateway text is never reparsed as an embedded array.

Processing uses encoding/json/jsontext directly. The only production encoding/json import recognizes its Number data type, supporting typed SDK handlers configured with lambda.WithUseNumber(true); no v1 codec is called. See the runnable examples.

The default claim allowance is 256 KiB, configurable with identity.WithClaimsBudget(bytes) up to 6 MiB. Each value costs 64 bytes plus object-name, string and exact-number text bytes. Raw JSON also has a wire-length limit of the same size. Nesting is limited to 64 containers. These are resource policies, not heap caps; measurements include allocation and retained-memory costs. edge.WithIdentityClaimsBudget(bytes) applies the matching allowance to gateway claims and dedicated scopes.

Handlers receive an owned request body and canceled context after invocation. Multipart files parsed on the served request are removed on success, error or panic. Middleware that parses multipart on a separate request copy owns cleanup of files attached only to that copy. Buffered and pre-handoff streaming panics propagate after cleanup; post-handoff streaming panics become terminal errors.

License

Copyright 2026 Andrew Beck. Licensed under the Apache License, Version 2.0.

Documentation

Overview

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.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidInvocation indicates an invalid direct invocation argument or adapter.
	ErrInvalidInvocation = errors.New("edge: invalid invocation")
	// ErrInvalidEvent indicates a malformed event or failed HTTP request conversion.
	ErrInvalidEvent = errors.New("edge: invalid event")
	// ErrUnsupportedEvent indicates an unsupported event family or payload version.
	ErrUnsupportedEvent = errors.New("edge: unsupported event")
	// ErrIdentity indicates failure to establish or isolate invocation identity.
	ErrIdentity = errors.New("edge: identity failure")
	// ErrResponse indicates an invalid or unrepresentable HTTP response.
	ErrResponse = errors.New("edge: response failure")
	// ErrLimitExceeded indicates an adapter-enforced resource limit.
	ErrLimitExceeded = errors.New("edge: resource limit exceeded")
	// ErrCleanup indicates failure to clean up invocation-owned request resources.
	ErrCleanup = errors.New("edge: request cleanup failure")
	// ErrStream indicates a stream lifecycle failure or a recovered late panic.
	ErrStream = errors.New("edge: stream failure")
)

Invocation failure categories support errors.Is through wrapping and joined errors. Application HTTP responses, including 4xx and 5xx, are not invocation failures when the transport completes successfully.

Functions

This section is empty.

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

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; a shared handler must be safe for concurrent use.

func New

func New(handler http.Handler, opts ...Option) (*Adapter, error)

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.

func (*Adapter) HandleV1

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 (*Adapter) HandleV2

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.

Example
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/aws/aws-lambda-go/events"

	edge "github.com/asteroid-computing/go-lambda-edge"
)

func main() {
	adapter, err := edge.New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusAccepted)
	}))
	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.
	response, err := adapter.HandleV2(context.Background(), events.APIGatewayV2HTTPRequest{
		Version:        "2.0",
		RawPath:        "/",
		RequestContext: events.APIGatewayV2HTTPRequestContext{APIID: "example", HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{Method: "POST"}},
	})
	fmt.Println(response.StatusCode, err)
}
Output:
202 <nil>

func (*Adapter) Invoke

func (a *Adapter) Invoke(ctx context.Context, payload []byte) ([]byte, error)

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.

Example
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/aws/aws-lambda-go/lambda"

	edge "github.com/asteroid-computing/go-lambda-edge"
)

func main() {
	adapter, err := edge.New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}))
	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.
	handler := lambda.NewHandler(adapter)
	response, err := handler.Invoke(context.Background(), []byte(`{"httpMethod":"GET","path":"/","requestContext":{"apiId":"example"}}`))
	fmt.Println(len(response) > 0, err)
}
Output:
true <nil>

type InvocationError

type InvocationError struct {
	// contains filtered or unexported fields
}

InvocationError describes a sanitized transport failure. Use errors.Is to inspect its category and errors.AsType[*InvocationError] for its diagnostics. Its text is diagnostic, not a stable identifier or an HTTP response body. It does not expose event values, application data, or unsafe underlying errors. A zero InvocationError describes an unclassified failure.

Operation failures precede any joined cleanup failures. Parent cancellation is returned as the standard context error when there is no operation or cleanup failure. Application panics propagated to the runtime are not InvocationErrors.

func (*InvocationError) Error

func (e *InvocationError) Error() string

Error returns a sanitized description of the failed operation.

func (*InvocationError) Limit

func (e *InvocationError) Limit() (resource Resource, maximum int64, ok bool)

Limit reports the exhausted resource, as one of the Resource constants, and its effective maximum in bytes for a limit failure. A response header or identity claims budget measures the documented charge; raw claims also have a wire-length limit. Neither budget measures heap use. Other failures return "", 0, false.

func (*InvocationError) Operation

func (e *InvocationError) Operation() Operation

Operation identifies the failed operation as one of the Operation constants. A zero error returns "".

func (*InvocationError) Unwrap

func (e *InvocationError) Unwrap() error

Unwrap exposes the failure category and any explicitly preserved safe causes.

type Operation

type Operation string

Operation identifies the invocation stage that failed.

const (
	OperationValidate Operation = "validate" // adapter, argument or event envelope validation
	OperationDecode   Operation = "decode"   // event JSON decoding
	OperationRequest  Operation = "request"  // HTTP request conversion
	OperationIdentity Operation = "identity" // gateway identity construction or isolation
	OperationResponse Operation = "response" // HTTP response capture
	OperationEncode   Operation = "encode"   // response envelope or stream prefix encoding
	OperationCleanup  Operation = "cleanup"  // invocation-owned request cleanup
	OperationStream   Operation = "stream"   // stream lifecycle after handoff
)

Invocation stages reported by InvocationError.Operation.

type Option

type Option func(*config)

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.

func WithGatewayIdentity

func WithGatewayIdentity(enabled bool) Option

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. Unsupported or conflicting assertions fail before running the HTTP handler.

func WithIdentityClaimsBudget

func WithIdentityClaimsBudget(bytes int) Option

WithIdentityClaimsBudget sets the gateway identity claims allowance in bytes. 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.

func WithResponseHeaderBudget

func WithResponseHeaderBudget(bytes int) Option

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.

func WithStreamErrorReporter

func WithStreamErrorReporter(report func(context.Context, error)) Option

WithStreamErrorReporter reports a sanitized terminal failure once after producer cleanup and before stream completion. It applies only to NewStreaming; New rejects it. A final nil callback is invalid. The callback receives the invocation context, possibly canceled, and must return promptly. Pre-handoff failures are returned directly and not reported. A callback panic is recovered and adds a sanitized ErrStream fault without replacing the original failure. It must not read or close the same stream: reporting precedes its completion.

type Resource

type Resource string

Resource names an adapter-enforced limit reported by InvocationError.Limit.

const (
	ResourceResponseHeaders  Resource = "response_headers"  // weighted response header budget
	ResourceBufferedBody     Resource = "buffered_body"     // buffered body, including base64 expansion and HEAD byte counts
	ResourceBufferedEnvelope Resource = "buffered_envelope" // complete serialized buffered response
	ResourceStreamMetadata   Resource = "stream_metadata"   // streaming metadata prefix
	ResourceIdentityClaims   Resource = "identity_claims"   // weighted gateway identity claims allowance
)

Resources whose limits the adapter enforces.

type StreamingAdapter

type StreamingAdapter struct {
	// contains filtered or unexported fields
}

StreamingAdapter serves REST API proxy events through an ordinary HTTP handler and returns a metadata-prefixed body stream. Construct it with NewStreaming; its zero value is unusable. Configuration is immutable; the application owns handler concurrency safety. HTTP APIs and request streaming are unsupported.

Deployment requires a REST STREAM integration using InvokeWithResponseStream. SDK Runtime API compatibility remains unverified against live AWS; see the module's streaming guide for the documented SDK/header/connection qualification.

func NewStreaming

func NewStreaming(handler http.Handler, opts ...Option) (*StreamingAdapter, error)

NewStreaming validates handler and options without starting I/O or goroutines. It shares New's handler, header-budget, identity-budget and gateway defaults. Streaming never infers Content-Length; supply it explicitly when needed. Authenticate and authorize before writing or flushing any response.

Example (Binary)
adapter, err := edge.NewStreaming(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/octet-stream")
	_, _ = w.Write([]byte{0, 255, 1})
}))
if err != nil {
	panic(err)
}
stream, err := adapter.HandleV1(context.Background(), exampleRESTEvent())
if err != nil {
	panic(err)
}
fmt.Printf("%v\n", readExampleStream(stream))
Output:
[0 255 1]
Example (Gzip)
adapter, err := edge.NewStreaming(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	w.Header().Set("Content-Encoding", "gzip")
	// The example always emits gzip;
	// a real route negotiates Accept-Encoding.
	compressed := gzip.NewWriter(w)
	defer compressed.Close()
	for _, chunk := range []string{"first\n", "last\n"} {
		if r.Context().Err() != nil {
			return
		}
		if _, err := io.WriteString(compressed, chunk); err != nil {
			return
		}
		// Flush the encoder first, then the underlying HTTP response.
		if err := compressed.Flush(); err != nil {
			return
		}
		if err := http.NewResponseController(w).Flush(); err != nil {
			return
		}
	}
}))
if err != nil {
	panic(err)
}
stream, err := adapter.HandleV1(context.Background(), exampleRESTEvent())
if err != nil {
	panic(err)
}
reader, err := gzip.NewReader(bytes.NewReader(readExampleStream(stream)))
if err != nil {
	panic(err)
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
	panic(err)
}
fmt.Print(string(data))
Output:
first
last

func (*StreamingAdapter) Handle

func (a *StreamingAdapter) Handle(ctx context.Context, event jsontext.Value) (io.ReadCloser, error)

Handle validates raw REST event JSON with JSON v2 and returns a stream that callers must consume and Close. Register lambda.Start(adapter.Handle), not the adapter object or the buffered Handler.Invoke boundary. The SDK owns its initial jsontext.Value capture; edge owns strict event decoding and prefix encoding. Explicit HTTP API version envelopes are rejected. Do not mutate event during the call. Request resources remain owned until producer cleanup completes.

Pre-handoff failures return a nil stream. Early producer panics propagate after cleanup; late panics become sanitized terminal errors. After handoff, failures cannot replace status or bytes. Close cancels and joins the producer; a handler ignoring cancellation/write errors can delay Close until invocation timeout.

Example
adapter, err := edge.NewStreaming(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	// Protected applications authenticate and authorize before this point.
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	controller := http.NewResponseController(w)
	for _, message := range []string{"ready", "done"} {
		if r.Context().Err() != nil {
			return
		}
		if _, err := fmt.Fprintf(w, "data: %s\n\n", message); err != nil {
			return
		}
		if err := controller.Flush(); err != nil {
			return
		}
	}
}))
if err != nil {
	panic(err)
}
// A Lambda main registers lambda.Start(adapter.Handle).
// This direct call demonstrates reader ownership locally;
// it makes no AWS calls.
stream, err := adapter.Handle(context.Background(), jsontext.Value(`{"httpMethod":"GET","path":"/","requestContext":{"apiId":"example"}}`))
if err != nil {
	panic(err)
}
fmt.Print(string(readExampleStream(stream)))
Output:
data: ready

data: done

func (*StreamingAdapter) HandleV1

HandleV1 accepts an already typed REST event, avoiding request envelope JSON work inside edge. Register lambda.Start(adapter.HandleV1). The caller/upstream codec owns wire validation and numeric fidelity. The V1 type also represents HTTP API 1.0; it cannot prove the deployment is a REST streaming integration. Its stream ownership and error contract match Handle. Inputs must not change during the call. Streaming metadata still requires direct JSON v2 encoding.

Example
adapter, err := edge.NewStreaming(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/x-ndjson")
	for _, value := range []string{"ready", "done"} {
		if r.Context().Err() != nil {
			return
		}
		if err := json.MarshalWrite(w, struct {
			State string `json:"state"`
		}{value}); err != nil {
			return
		}
		if _, err := io.WriteString(w, "\n"); err != nil {
			return
		}
		if err := http.NewResponseController(w).Flush(); err != nil {
			return
		}
	}
}))
if err != nil {
	panic(err)
}
// A Lambda main may register lambda.Start(adapter.HandleV1).
// Input envelope decoding then belongs to the SDK;
// output metadata still uses JSON v2 in edge.
stream, err := adapter.HandleV1(context.Background(), exampleRESTEvent())
if err != nil {
	panic(err)
}
fmt.Print(string(readExampleStream(stream)))
Output:
{"state":"ready"}
{"state":"done"}

Directories

Path Synopsis
Package actionheader selects one application action from a configured HTTP request header.
Package actionheader selects one application action from a configured HTTP request header.
Package authn selects one configured Bearer or EdgeIAM verifier from an HTTP Authorization header and transports the verified caller to protected handlers.
Package authn selects one configured Bearer or EdgeIAM verifier from an HTTP Authorization header and transports the verified caller to protected handlers.
Package authz authorizes explicit caller, action and resource facts using immutable rules.
Package authz authorizes explicit caller, action and resource facts using immutable rules.
examples
dispatcher/buffered command
Command buffered registers the orders application for REST or HTTP API events.
Command buffered registers the orders application for REST or HTTP API events.
dispatcher/internal/orders
Package orders is application code for the dispatcher example, not an edge API.
Package orders is application code for the dispatcher example, not an edge API.
dispatcher/native command
Command native serves the buffered orders application on loopback.
Command native serves the buffered orders application on loopback.
dispatcher/streaming command
Command streaming registers the orders application for REST response streaming.
Command streaming registers the orders application for REST response streaming.
Package iamproof generates short-lived, audience-bound GetCallerIdentity proofs and verifies them online with regional AWS STS.
Package iamproof generates short-lived, audience-bound GetCallerIdentity proofs and verifies them online with regional AWS STS.
Package identity provides immutable callers, owned claims and context transport.
Package identity provides immutable callers, owned claims and context transport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL