protocol

package
v0.0.1-alpha.30 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package protocol provides shared AWS protocol helpers used by all service handlers: error serialisation, request IDs, ARN construction, and response writing. Nothing in this package is service-specific.

Index

Constants

View Source
const MaxQueryFormBody = 1 << 20 // 1 MiB

MaxQueryFormBody bounds how much of a request body is ever read into memory in order to treat it as an AWS Query form — by ParseFormPreservingBody here, and by the Query codec's direct-body-read fallback. Real Query requests are far smaller: the largest inline payloads AWS accepts on this protocol are SNS Publish and SQS SendMessageBatch at 256 KiB and a CloudFormation inline TemplateBody at 51,200 bytes. This is generous headroom, not a realistic limit for legitimate traffic. Anything past it is not a Query request, and buffering it would mean holding an arbitrarily large S3 upload in memory.

Variables

View Source
var (
	// ErrNotImplemented is returned for endpoints that exist in the routing
	// table but have not yet been implemented. The x-emulator-unsupported
	// header is added automatically by WriteXMLError / WriteJSONError.
	ErrNotImplemented = &AWSError{
		Code:       "NotImplemented",
		Message:    "This operation is not yet emulated. Check docs/services/ for the support matrix.",
		HTTPStatus: http.StatusNotImplemented,
	}

	// ErrMethodNotAllowed is returned for a method AWS itself rejects on a
	// resource — not for something Overcast has yet to implement. The two are
	// different claims: NotImplemented tells a client the emulator is
	// incomplete and invites a workaround, when real AWS would refuse the same
	// request. S3's wording, verbatim.
	ErrMethodNotAllowed = &AWSError{
		Code:       "MethodNotAllowed",
		Message:    "The specified method is not allowed against this resource.",
		HTTPStatus: http.StatusMethodNotAllowed,
	}

	// ErrInternalError is returned when the emulator itself encounters an
	// unexpected failure (state backend error, serialisation failure, etc.).
	// Always Wrap() this with the underlying error for log context.
	ErrInternalError = &AWSError{
		Code:       "InternalError",
		Message:    "An internal error occurred.",
		HTTPStatus: http.StatusInternalServerError,
	}

	// ErrStorageMigrating is returned by middleware.NotReady while the
	// storage backend is still completing a one-time schema migration on
	// startup (see internal/state/migrate.go) and cannot yet serve requests
	// reliably — without this check, a request in this window would either
	// block indefinitely (persistent mode) or silently observe incomplete
	// state as if it simply didn't exist (hybrid mode's TierHot reads,
	// before the post-migration seed has populated memory). "ServiceUnavailable"
	// is a real AWS error code multiple services return for their own
	// transient unavailability, so AWS SDKs already retry it automatically
	// per their standard retry policy — a client normally needs no special
	// handling for this.
	ErrStorageMigrating = &AWSError{
		Code:       "ServiceUnavailable",
		Message:    "Overcast is completing a one-time database migration and is not yet ready to serve requests. This happens after an upgrade or first startup against existing data and should resolve within moments — retry the request.",
		HTTPStatus: http.StatusServiceUnavailable,
	}
)
View Source
var ErrFormBodyTooLarge = errors.New("protocol: request body exceeds the AWS Query form parse limit")

ErrFormBodyTooLarge reports that a request body exceeded MaxQueryFormBody, so its form fields were not parsed. The body is left intact and readable.

Functions

func APIV2ARN

func APIV2ARN(region, apiID string) string

APIV2ARN builds an API Gateway v2 (HTTP/WebSocket) API ARN. Format: arn:aws:apigateway:{region}::/apis/{apiId}.

func ARN

func ARN(region, accountID, service, resource string) string

ARN builds an AWS ARN string. Format: arn:aws:<service>:<region>:<accountID>:<resource>

Examples:

ARN("us-east-1", "000000000000", "s3", "my-bucket")
  → "arn:aws:s3:::my-bucket"  (S3 omits region and account)

ARN("us-east-1", "000000000000", "sqs", "my-queue")
  → "arn:aws:sqs:us-east-1:000000000000:my-queue"

func Cause

func Cause(aerr *AWSError) error

Cause returns the underlying cause of aerr, or nil if none was set. Prefer errors.Is / errors.As over Cause for most use cases — they traverse arbitrarily deep chains. Use Cause only when you need the immediate cause.

func ContextWithRequestID

func ContextWithRequestID(ctx context.Context, id string) context.Context

ContextWithRequestID returns a new context carrying the given request ID. Called by the request-ID middleware on every incoming request.

func DistributionARN

func DistributionARN(accountID, distributionID string) string

DistributionARN builds a CloudFront distribution ARN. CloudFront ARNs omit the region — this is an AWS quirk. Format: arn:aws:cloudfront::{accountID}:distribution/{distributionID}.

func LambdaARN

func LambdaARN(region, accountID, functionName string) string

LambdaARN builds a Lambda function ARN.

func LambdaCodeSigningConfigARN

func LambdaCodeSigningConfigARN(region, accountID, cscID string) string

LambdaCodeSigningConfigARN builds a Lambda code signing configuration ARN. Format: arn:aws:lambda:{region}:{account}:code-signing-config:{cscID}.

func LambdaVersionARN

func LambdaVersionARN(region, accountID, functionName string, version int) string

LambdaVersionARN builds a Lambda function ARN with a numeric version qualifier. Format: arn:aws:lambda:{region}:{account}:function:{name}:{version}.

func LayerARN

func LayerARN(region, accountID, layerName string) string

LayerARN builds the unversioned Lambda layer ARN (no version suffix). Format: arn:aws:lambda:{region}:{account}:layer:{name}.

func LayerVersionARN

func LayerVersionARN(region, accountID, layerName string, version int) string

LayerVersionARN builds a Lambda layer version ARN. Format: arn:aws:lambda:{region}:{account}:layer:{name}:{version}.

func LogGroupARN

func LogGroupARN(region, accountID, groupName string) string

LogGroupARN builds a CloudWatch Logs log group ARN.

func LogStreamARN

func LogStreamARN(region, accountID, groupName, streamName string) string

LogStreamARN builds a CloudWatch Logs log stream ARN.

func MethodNotAllowedXML

func MethodNotAllowedXML(w http.ResponseWriter, r *http.Request)

MethodNotAllowedXML is a convenience handler for a method AWS rejects on a resource, for use as a dispatch fallback where no subresource selects an operation.

func NewRequestID

func NewRequestID() string

NewRequestID generates a new unique AWS-style request ID. AWS uses UUID v4 format without hyphens in some services, with hyphens in others. We always use with-hyphens — both forms are accepted by SDKs.

func NotImplementedEC2QueryXML

func NotImplementedEC2QueryXML(w http.ResponseWriter, r *http.Request)

NotImplementedEC2QueryXML writes a 501 for unimplemented EC2 Query operations.

func NotImplementedJSON

func NotImplementedJSON(w http.ResponseWriter, r *http.Request)

NotImplementedJSON is a convenience handler for unimplemented JSON endpoints.

func NotImplementedQueryXML

func NotImplementedQueryXML(w http.ResponseWriter, r *http.Request)

NotImplementedQueryXML writes a 501 for unimplemented Query-protocol operations.

func NotImplementedXML

func NotImplementedXML(w http.ResponseWriter, r *http.Request)

NotImplementedXML is a convenience handler for unimplemented S3 endpoints.

func ParseFormPreservingBody

func ParseFormPreservingBody(r *http.Request) error

ParseFormPreservingBody populates r.Form and r.PostForm the way (*http.Request).ParseForm does, but leaves r.Body readable afterwards.

This exists because Content-Type is not a routing decision. The standard library's ParseForm drains r.Body for any POST/PUT/PATCH labelled application/x-www-form-urlencoded, so shared code that sniffs a request for AWS Query fields — protocol detection, IAM action extraction — would silently swallow the payload of a request that turned out to belong to a REST service. S3 PutObject is the case that bites: `curl --data-binary` sends that content type by default, and AWS stores the body regardless.

Callers that only ever see genuine Query traffic can keep using ParseForm; anything running ahead of routing must use this.

func QueueARN

func QueueARN(region, accountID, queueName string) string

QueueARN builds an SQS queue ARN from its components.

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext retrieves the request ID stored in ctx. Returns a freshly generated ID if none was set — this is a safety net and should not happen in normal operation (the middleware always sets one).

func RestAPIARN

func RestAPIARN(region, apiID string) string

RestAPIARN builds an API Gateway REST API ARN. API Gateway ARNs omit the account ID — this is an AWS quirk. Format: arn:aws:apigateway:{region}::/restapis/{apiId}.

func ServiceFromARN

func ServiceFromARN(arn string) string

ServiceFromARN extracts the service name (e.g. "appsync", "kafka") from an ARN's third colon-delimited segment ("arn:partition:service:..."). Returns "" if arn is not a well-formed ARN string.

This is the authoritative way to identify which service owns a resource when a path is shared between services (e.g. /v1/tags/{resourceArn}) — the ARN is self-describing, unlike the request's SigV4 credential scope, which callers may not always set to match (hand-built requests, or resource-group-style tagging tools that sign generically).

func TableARN

func TableARN(region, accountID, tableName string) string

TableARN builds a DynamoDB table ARN.

func TopicARN

func TopicARN(region, accountID, topicName string) string

TopicARN builds an SNS topic ARN.

func WriteAWSJSON

func WriteAWSJSON(w http.ResponseWriter, r *http.Request, status int, v any, contentType string)

WriteAWSJSON serialises v as JSON and writes it with the provided AWS JSON content type and request ID headers.

Pass an explicit content type per service protocol, for example:

  • application/x-amz-json-1.0
  • application/x-amz-json-1.1

If contentType is empty, application/x-amz-json-1.0 is used.

func WriteEC2QueryXMLError

func WriteEC2QueryXMLError(w http.ResponseWriter, r *http.Request, aerr *AWSError)

WriteEC2QueryXMLError writes an EC2 Query-protocol XML error response.

func WriteEmpty

func WriteEmpty(w http.ResponseWriter, r *http.Request, status int)

WriteEmpty writes a response with no body and the standard request ID header. Used for operations like DeleteObject which return 204 or an empty 200.

func WriteJSON

func WriteJSON(w http.ResponseWriter, r *http.Request, status int, v any)

WriteJSON serialises v as JSON and writes it with the correct Content-Type and request ID headers. Services call this for successful responses.

func WriteJSONError

func WriteJSONError(w http.ResponseWriter, r *http.Request, aerr *AWSError)

WriteJSONError writes an AWS JSON-protocol error response. The cause, if any, is NOT included in the response body.

func WriteQueryXML

func WriteQueryXML(w http.ResponseWriter, r *http.Request, status int, v any)

WriteQueryXML serialises v as XML with text/xml content type. Used by Query-protocol services (SNS, STS, IAM). The request ID is set as both a response header and should be embedded in the response struct's ResponseMetadata. The request body is drained so the HTTP/1.1 connection can be reused by the SDK client.

func WriteQueryXMLError

func WriteQueryXMLError(w http.ResponseWriter, r *http.Request, aerr *AWSError)

WriteQueryXMLError writes an AWS Query-protocol XML error response (SNS format).

func WriteXML

func WriteXML(w http.ResponseWriter, r *http.Request, status int, v any)

WriteXML serialises v as XML and writes it with the correct Content-Type and request ID headers. Services call this for successful responses.

func WriteXMLError

func WriteXMLError(w http.ResponseWriter, r *http.Request, aerr *AWSError)

WriteXMLError writes an AWS REST-XML error response (S3 format). The cause, if any, is NOT included in the response — it stays server-side for logging. The x-emulator-unsupported header is set automatically for 501.

Types

type AWSError

type AWSError struct {
	// Code is the AWS error code string, e.g. "NoSuchBucket", "QueueDoesNotExist".
	Code string
	// Message is the human-readable description sent to the client.
	Message string
	// HTTPStatus is the HTTP status code to send, e.g. 404, 400, 500.
	HTTPStatus int
	// contains filtered or unexported fields
}

AWSError represents a structured AWS API error that maps to an HTTP response.

It implements the standard error interface and supports Go's error wrapping convention, equivalent to JavaScript's `new Error("msg", { cause: err })`.

Wrapping pattern — use Wrap() to attach an underlying cause while presenting a clean AWS error to callers:

// Service code:
raw, found, err := s.store.Get(ctx, ns, key)
if err != nil {
    return protocol.Wrap(protocol.ErrInternalError, err)
}

// The HTTP layer sees ErrInternalError (clean AWS error code + message).
// The original err is preserved in the chain for logging and debugging:
logger.Error("state read failed", zap.Error(aerr))
// → logs both "InternalError" and the underlying storage error

Inspection pattern — anywhere in the call chain:

var aerr *protocol.AWSError
if errors.As(err, &aerr) {
    // aerr.Code, aerr.HTTPStatus available
}

func AsAWSError

func AsAWSError(err error) *AWSError

AsAWSError extracts the first *AWSError from the error chain. Returns nil if no *AWSError is found.

This is a convenience wrapper around errors.As for the common case of checking whether an error from a helper function is an AWSError:

aerr := protocol.AsAWSError(err)
if aerr != nil {
    protocol.WriteJSONError(w, r, aerr)
    return
}

func ErrInvalidArgument

func ErrInvalidArgument(msg string) *AWSError

ErrInvalidArgument returns a 400 error for malformed input.

func ErrMissingParameter

func ErrMissingParameter(param string) *AWSError

ErrMissingParameter returns a 400 error for a missing required parameter.

func ErrSerialization

func ErrSerialization(msg string) *AWSError

ErrSerialization returns the 400 error real AWS JSON-protocol services use for a request body that cannot be parsed at all — DynamoDB and other coral-framework services return __type com.amazon.coral.service#SerializationException, and Smithy's malformed-request protocol tests pin 400 + x-amzn-errortype: SerializationException. Use this for parse-level failures only; semantic validation of a successfully parsed request stays ValidationException / InvalidArgument / service-specific codes.

func Wrap

func Wrap(template *AWSError, cause error) *AWSError

Wrap returns a new *AWSError with the same Code, Message, and HTTPStatus as template, but with cause attached as the underlying error.

This is the primary way to preserve error context in service code:

func (s *s3Store) getBucket(ctx context.Context, name string) (*Bucket, *AWSError) {
    raw, found, err := s.store.Get(ctx, nsBuckets, name)
    if err != nil {
        // ErrInternalError is shown to the client.
        // err (e.g. "sqlite: no such table") is preserved for logging.
        return nil, protocol.Wrap(ErrInternalError, err)
    }
    ...
}

Wrap never modifies the template — it always returns a new AWSError value.

func (*AWSError) Error

func (e *AWSError) Error() string

Error implements the error interface. Returns the AWS error code and message; the cause is accessible via Unwrap.

func (*AWSError) Unwrap

func (e *AWSError) Unwrap() error

Unwrap returns the underlying cause, enabling errors.Is / errors.As to traverse the full error chain. This is the Go equivalent of error.cause in JavaScript.

// Check if a specific underlying error occurred:
if errors.Is(aerr, sql.ErrNoRows) { ... }

// Extract a specific error type from anywhere in the chain:
var pgErr *pgconn.PgError
if errors.As(aerr, &pgErr) { ... }

type ResponseMetadata

type ResponseMetadata struct {
	RequestID string `xml:"RequestId"`
}

ResponseMetadata is embedded in AWS Query-protocol XML responses. It carries the request ID that SDKs surface as response.ResultMetadata.

func QueryResponseMetadata

func QueryResponseMetadata(r *http.Request) ResponseMetadata

QueryResponseMetadata returns a ResponseMetadata populated from the request context.

Directories

Path Synopsis
Package codec defines the wire-protocol abstraction used by Overcast's typed operation dispatcher.
Package codec defines the wire-protocol abstraction used by Overcast's typed operation dispatcher.
Package op defines the typed operation dispatcher used by Overcast's Smithy-aligned services.
Package op defines the typed operation dispatcher used by Overcast's Smithy-aligned services.

Jump to

Keyboard shortcuts

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