diff --git a/adapter_invoke.go b/adapter_invoke.go index 80d5493..fb32253 100644 --- a/adapter_invoke.go +++ b/adapter_invoke.go @@ -96,7 +96,7 @@ func (a *Adapter) HandleV2(ctx context.Context, event events.APIGatewayV2HTTPReq func (a *Adapter) valid() error { if a == nil || a.handler == nil { - return invocationError("validate", ErrInvalidInvocation, "uninitialized adapter") + return invocationError(OperationValidate, ErrInvalidInvocation, "uninitialized adapter") } return nil } @@ -135,7 +135,7 @@ func prepareRequest(ctx context.Context, inv *invocation, event decodedEvent, ra } ctx, err = identity.NewContext(ctx, caller) if err != nil { - return nil, invocationError("identity", ErrIdentity, "caller installation failed", identity.ErrConflict) + return nil, invocationError(OperationIdentity, 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. diff --git a/buffered.go b/buffered.go index 6ae367c..fdbb4ad 100644 --- a/buffered.go +++ b/buffered.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-lambda-go/events" ) -var errInformationalResponse = invocationError("response", ErrResponse, "informational response is unsupported", http.ErrNotSupported) +var errInformationalResponse = invocationError(OperationResponse, ErrResponse, "informational response is unsupported", http.ErrNotSupported) // bufferedWriter belongs to one handler invocation. // Only Header, WriteHeader and Write form its HTTP surface; @@ -48,7 +48,7 @@ func (w *bufferedWriter) WriteHeader(status int) { return } if status < 200 || status > 599 { - w.err = invocationError("response", ErrResponse, "invalid final response status") + w.err = invocationError(OperationResponse, ErrResponse, "invalid final response status") return } w.status = status @@ -79,11 +79,11 @@ func (w *bufferedWriter) Write(p []byte) (int, error) { return 0, http.ErrBodyNotAllowed } if w.hasLength && int64(len(p)) > w.declaredLength-w.written { - w.err = invocationError("response", ErrResponse, "response exceeds declared content length", http.ErrContentLength) + w.err = invocationError(OperationResponse, ErrResponse, "response exceeds declared content length", http.ErrContentLength) return 0, w.err } if int64(len(p)) > math.MaxInt64-w.written { - w.err = limitError("response", "buffered_body", math.MaxInt64, errResponseTooLarge) + w.err = limitError(OperationResponse, ResourceBufferedBody, math.MaxInt64, errResponseTooLarge) return 0, w.err } data := p @@ -91,7 +91,7 @@ func (w *bufferedWriter) Write(p []byte) (int, error) { data = p[:min(len(p), 512-len(w.body.data))] } if _, err := w.body.Write(data); err != nil { - w.err = limitError("response", "buffered_body", maxResponseBytes, errResponseTooLarge) + w.err = limitError(OperationResponse, ResourceBufferedBody, maxResponseBytes, errResponseTooLarge) return 0, w.err } w.written += int64(len(p)) @@ -116,7 +116,7 @@ func (w *bufferedWriter) finish() (*bufferedResponse, error) { return nil, err } if w.method != http.MethodHead && w.hasLength && w.written != w.declaredLength { - return nil, invocationError("response", ErrResponse, "response body length mismatch", http.ErrContentLength) + return nil, invocationError(OperationResponse, ErrResponse, "response body length mismatch", http.ErrContentLength) } if responseBodyAllowed(w.status) { if len(w.body.data) > 0 && w.headers.canSniffType() { diff --git a/decode.go b/decode.go index eb31bd8..088a101 100644 --- a/decode.go +++ b/decode.go @@ -28,22 +28,22 @@ func decodeEvent(payload []byte) (decodedEvent, error) { if err := json.Unmarshal(payload, &envelope); err != nil { // JSON errors can contain object names and input values. // Do not expose them, even through an unwrap chain, at this credential-bearing boundary. - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "invalid event JSON") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "invalid event JSON") } if envelope == nil { - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "event must be a JSON object") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "event must be a JSON object") } var version string if len(envelope.Version) != 0 { if err := json.Unmarshal(envelope.Version, &version); err != nil { - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "invalid payload version") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "invalid payload version") } if version != "1.0" && version != "2.0" { - return decodedEvent{}, invocationError("decode", ErrUnsupportedEvent, "unsupported payload version") + return decodedEvent{}, invocationError(OperationDecode, ErrUnsupportedEvent, "unsupported payload version") } } if len(envelope.IsBase64Encoded) != 0 && envelope.IsBase64Encoded.Kind() != 't' && envelope.IsBase64Encoded.Kind() != 'f' { - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "isBase64Encoded must be a boolean") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "isBase64Encoded must be a boolean") } if version == "2.0" { @@ -55,7 +55,7 @@ func decodeEvent(payload []byte) (decodedEvent, error) { } `json:"requestContext"` } if err := json.Unmarshal(payload, &wire, eventStringOptions); err != nil { - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "invalid v2 event fields") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "invalid v2 event fields") } event := wire.APIGatewayV2HTTPRequest event.RequestContext = wire.RequestContext.APIGatewayV2HTTPRequestContext @@ -73,7 +73,7 @@ func decodeEvent(payload []byte) (decodedEvent, error) { } `json:"requestContext"` } if err := json.Unmarshal(payload, &wire, eventStringOptions); err != nil { - return decodedEvent{}, invocationError("decode", ErrInvalidEvent, "invalid v1 event fields") + return decodedEvent{}, invocationError(OperationDecode, ErrInvalidEvent, "invalid v1 event fields") } event := wire.APIGatewayProxyRequest event.RequestContext = wire.RequestContext.APIGatewayProxyRequestContext @@ -85,29 +85,29 @@ func decodeEvent(payload []byte) (decodedEvent, error) { func validateV1(event events.APIGatewayProxyRequest) error { if event.RequestContext.APIID == "" { - return invocationError("validate", ErrInvalidEvent, "missing requestContext.apiId") + return invocationError(OperationValidate, ErrInvalidEvent, "missing requestContext.apiId") } if event.HTTPMethod == "" { - return invocationError("validate", ErrInvalidEvent, "missing httpMethod") + return invocationError(OperationValidate, ErrInvalidEvent, "missing httpMethod") } if event.Path == "" { - return invocationError("validate", ErrInvalidEvent, "missing path") + return invocationError(OperationValidate, ErrInvalidEvent, "missing path") } return nil } func validateV2(event events.APIGatewayV2HTTPRequest) error { if event.Version != "" && event.Version != "2.0" { - return invocationError("validate", ErrInvalidEvent, "conflicting v2 payload version") + return invocationError(OperationValidate, ErrInvalidEvent, "conflicting v2 payload version") } if event.RequestContext.APIID == "" { - return invocationError("validate", ErrInvalidEvent, "missing requestContext.apiId") + return invocationError(OperationValidate, ErrInvalidEvent, "missing requestContext.apiId") } if event.RequestContext.HTTP.Method == "" { - return invocationError("validate", ErrInvalidEvent, "missing requestContext.http.method") + return invocationError(OperationValidate, ErrInvalidEvent, "missing requestContext.http.method") } if event.RawPath == "" { - return invocationError("validate", ErrInvalidEvent, "missing rawPath") + return invocationError(OperationValidate, ErrInvalidEvent, "missing rawPath") } return nil } diff --git a/docs/plan.md b/docs/plan.md index 10b05f7..5de7711 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -379,7 +379,7 @@ these are placement, naming, typing and documentation changes made before the fi - [x] E1: move the action-header selector to the standard-library-only `actionheader` package as `Selector`/`NewSelector`. - [x] E2: rename the moved sentinels to `ErrMissing`, `ErrAmbiguous` and `ErrInvalid`. - [x] E3: rename `identity.WithCaller` to `identity.NewContext`. -- [ ] E4: type the `InvocationError` operation and limit names with exported constants. +- [x] E4: type the `InvocationError` operation and limit names with exported constants. - [ ] E5: add `String` methods to the `identity` enumerations. - [ ] E6: complete sentinel docs, doc links, callback parameter names and `Claim.Bool` result names. diff --git a/errors.go b/errors.go index ac17cb7..723e880 100644 --- a/errors.go +++ b/errors.go @@ -37,23 +37,50 @@ var ( // 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. type InvocationError struct { - operation string + operation Operation detail string cause error limit *invocationLimit } type invocationLimit struct { - name string - maximum int64 + resource Resource + maximum int64 } +// Operation identifies the invocation stage that failed. +type Operation string + +// Invocation stages reported by [InvocationError.Operation]. +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 +) + +// Resource names an adapter-enforced limit reported by [InvocationError.Limit]. +type Resource string + +// Resources whose limits the adapter enforces. +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 +) + // Error returns a sanitized description of the failed operation. func (e *InvocationError) Error() string { if e == nil || e.operation == "" { return "edge: invocation failed" } - return "edge: " + e.operation + ": " + e.detail + return "edge: " + string(e.operation) + ": " + e.detail } // Unwrap exposes the failure category and any explicitly preserved safe causes. @@ -64,33 +91,31 @@ func (e *InvocationError) Unwrap() error { return e.cause } -// Operation identifies the failed operation: validate, decode, request, identity, response, encode, cleanup, or stream. +// Operation identifies the failed operation as one of the Operation constants. // A zero error returns "". -func (e *InvocationError) Operation() string { +func (e *InvocationError) Operation() Operation { if e == nil { return "" } return e.operation } -// Limit reports the resource name and its effective maximum in bytes for a limit failure. -// Names are response_headers, buffered_body, buffered_envelope, stream_metadata, and identity_claims. +// 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. -// buffered_body covers base64 expansion and the representable HEAD byte count. // Other failures return "", 0, false. -func (e *InvocationError) Limit() (name string, maximum int64, ok bool) { +func (e *InvocationError) Limit() (resource Resource, maximum int64, ok bool) { if e == nil || e.limit == nil { return "", 0, false } - return e.limit.name, e.limit.maximum, true + return e.limit.resource, e.limit.maximum, true } // invocationError accepts only package-owned diagnostic strings and causes. // Never pass parser errors, application errors, or input values here. // Error() sanitization alone would not make an unsafe Unwrap chain safe. -func invocationError(operation string, category error, detail string, causes ...error) *InvocationError { +func invocationError(operation Operation, category error, detail string, causes ...error) *InvocationError { cause := category if len(causes) != 0 { cause = errors.Join(append([]error{category}, causes...)...) @@ -99,9 +124,9 @@ func invocationError(operation string, category error, detail string, causes ... } // limitError has the same safe-input requirements as invocationError. -func limitError(operation, name string, maximum int64, reason error) *InvocationError { +func limitError(operation Operation, resource Resource, maximum int64, reason error) *InvocationError { err := invocationError(operation, ErrLimitExceeded, "resource limit exceeded", reason) - err.limit = &invocationLimit{name: name, maximum: maximum} + err.limit = &invocationLimit{resource: resource, maximum: maximum} return err } @@ -110,10 +135,10 @@ func limitError(operation, name string, maximum int64, reason error) *Invocation // Gateway preparation uses this boundary for raw and typed claim construction. func claimsError(err error) *InvocationError { if limit, ok := errors.AsType[*identity.ClaimsLimitError](err); ok { - return limitError("identity", "identity_claims", limit.Maximum(), errors.Join(ErrIdentity, identity.ErrClaimsLimit)) + return limitError(OperationIdentity, ResourceIdentityClaims, limit.Maximum(), errors.Join(ErrIdentity, identity.ErrClaimsLimit)) } if errors.Is(err, identity.ErrInvalidClaims) { - return invocationError("identity", ErrIdentity, "invalid claims", identity.ErrInvalidClaims) + return invocationError(OperationIdentity, ErrIdentity, "invalid claims", identity.ErrInvalidClaims) } - return invocationError("identity", ErrIdentity, "claims construction failed") + return invocationError(OperationIdentity, ErrIdentity, "claims construction failed") } diff --git a/errors_test.go b/errors_test.go index 27d820d..5ed9d1e 100644 --- a/errors_test.go +++ b/errors_test.go @@ -16,7 +16,7 @@ func TestInvocationErrorCategories(t *testing.T) { name string fail func(*testing.T) error category error - operation string + operation Operation standard error }{ {name: "nil_context", category: ErrInvalidInvocation, operation: "validate", fail: func(t *testing.T) error { @@ -129,8 +129,8 @@ func TestInvocationErrorLimits(t *testing.T) { for _, tt := range []struct { name string fail func(*testing.T) error - operation string - limit string + operation Operation + limit Resource maximum int64 }{ {name: "original_headers", operation: "response", limit: "response_headers", maximum: 32, fail: func(t *testing.T) error { diff --git a/gateway_identity.go b/gateway_identity.go index b966245..58b14ab 100644 --- a/gateway_identity.go +++ b/gateway_identity.go @@ -27,10 +27,10 @@ func (c gatewayCandidates) caller() (identity.Caller, error) { } } if count > 1 { - return identity.Caller{}, invocationError("identity", ErrIdentity, "conflicting gateway assertions", identity.ErrConflict) + return identity.Caller{}, invocationError(OperationIdentity, ErrIdentity, "conflicting gateway assertions", identity.ErrConflict) } if c.custom || c.unknown { - return identity.Caller{}, invocationError("identity", ErrIdentity, "unsupported gateway assertion", errors.ErrUnsupported) + return identity.Caller{}, invocationError(OperationIdentity, ErrIdentity, "unsupported gateway assertion", errors.ErrUnsupported) } var caller identity.Caller var err error @@ -45,7 +45,7 @@ func (c gatewayCandidates) caller() (identity.Caller, error) { return identity.Caller{}, err // A package-owned preflight limit. } if errors.Is(err, identity.ErrInvalidCaller) && !errors.Is(err, identity.ErrClaimsLimit) { - return identity.Caller{}, invocationError("identity", ErrIdentity, "invalid gateway caller", identity.ErrInvalidCaller) + return identity.Caller{}, invocationError(OperationIdentity, ErrIdentity, "invalid gateway caller", identity.ErrInvalidCaller) } return identity.Caller{}, claimsError(err) } @@ -88,7 +88,7 @@ func nonnull(raw jsontext.Value) bool { return len(raw) != 0 && raw.Kind() != 'n func rawGatewayCaller(event decodedEvent, budget int) (identity.Caller, error) { fields, err := rawObject(event.authorizer) if err != nil { - return identity.Caller{}, invocationError("identity", ErrIdentity, "invalid authorizer object", identity.ErrInvalidCaller) + return identity.Caller{}, invocationError(OperationIdentity, ErrIdentity, "invalid authorizer object", identity.ErrInvalidCaller) } var candidates gatewayCandidates if event.v1 != nil { @@ -193,7 +193,7 @@ func rawGatewayScopes(raw jsontext.Value, maximum int) ([]string, error) { func gatewayScopeLimit(maximum int) error { // Keep byte-limit metadata without constructing identity's opaque error type. - return limitError("identity", "identity_claims", int64(maximum), errors.Join(ErrIdentity, identity.ErrClaimsLimit)) + return limitError(OperationIdentity, ResourceIdentityClaims, int64(maximum), errors.Join(ErrIdentity, identity.ErrClaimsLimit)) } func typedGatewayCaller(event decodedEvent, budget int) (identity.Caller, error) { diff --git a/invocation.go b/invocation.go index 99e15d4..f14181a 100644 --- a/invocation.go +++ b/invocation.go @@ -28,12 +28,12 @@ func (inv *invocation) ownRequest(r *http.Request) { func (inv *invocation) cleanup(err error) error { if inv.body != nil { if closeErr := inv.body.Close(); closeErr != nil { - err = errors.Join(err, invocationError("cleanup", ErrCleanup, "request body cleanup failed")) + err = errors.Join(err, invocationError(OperationCleanup, ErrCleanup, "request body cleanup failed")) } } if inv.request != nil && inv.request.MultipartForm != nil { if removeErr := inv.request.MultipartForm.RemoveAll(); removeErr != nil { - err = errors.Join(err, invocationError("cleanup", ErrCleanup, "multipart cleanup failed")) + err = errors.Join(err, invocationError(OperationCleanup, ErrCleanup, "multipart cleanup failed")) } } return err @@ -41,13 +41,13 @@ func (inv *invocation) cleanup(err error) error { func invocationContext(parent context.Context) (context.Context, context.CancelFunc, error) { if parent == nil { - return nil, nil, invocationError("validate", ErrInvalidInvocation, "nil invocation context") + return nil, nil, invocationError(OperationValidate, ErrInvalidInvocation, "nil invocation context") } if err := parent.Err(); err != nil { return nil, nil, err } if identity.FromContext(parent).Kind() != identity.KindAnonymous { - return nil, nil, invocationError("identity", ErrIdentity, "invocation context already has a caller", identity.ErrConflict) + return nil, nil, invocationError(OperationIdentity, ErrIdentity, "invocation context already has a caller", identity.ErrConflict) } ctx, cancel := context.WithCancel(parent) return ctx, cancel, nil diff --git a/request.go b/request.go index 9400b24..abeadee 100644 --- a/request.go +++ b/request.go @@ -21,7 +21,7 @@ func requestV1(ctx context.Context, event events.APIGatewayProxyRequest) (*http. return nil, err } if !strings.HasPrefix(event.Path, "/") { - return nil, invocationError("request", ErrInvalidEvent, "path must begin with a slash") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "path must begin with a slash") } query := make(url.Values) for key, values := range event.MultiValueQueryStringParameters { @@ -52,22 +52,22 @@ func requestV2(ctx context.Context, event events.APIGatewayV2HTTPRequest) (*http return nil, err } if !strings.HasPrefix(event.RawPath, "/") { - return nil, invocationError("request", ErrInvalidEvent, "rawPath must begin with a slash") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "rawPath must begin with a slash") } path, err := url.PathUnescape(event.RawPath) if err != nil { - return nil, invocationError("request", ErrInvalidEvent, "invalid rawPath encoding") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid rawPath encoding") } u := &url.URL{Path: path, RawPath: event.RawPath, RawQuery: event.RawQueryString} // EscapedPath ignores an invalid RawPath hint. // Reject rather than silently replacing raw delimiters, whitespace, or an invalid encoded spelling. if u.EscapedPath() != event.RawPath { - return nil, invocationError("request", ErrInvalidEvent, "invalid rawPath syntax") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid rawPath syntax") } // Query decoding remains the handler's job, but a request target cannot contain literal ASCII whitespace or control bytes on an HTTP request line. for i := 0; i < len(event.RawQueryString); i++ { if event.RawQueryString[i] <= ' ' || event.RawQueryString[i] == 0x7f { - return nil, invocationError("request", ErrInvalidEvent, "invalid raw query syntax") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid raw query syntax") } } header, err := requestHeaders(event.Headers, nil) @@ -77,7 +77,7 @@ func requestV2(ctx context.Context, event events.APIGatewayV2HTTPRequest) (*http if len(event.Cookies) != 0 { cookie := strings.Join(event.Cookies, "; ") if !validHeaderValue(cookie) { - return nil, invocationError("request", ErrInvalidEvent, "invalid cookie header") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid cookie header") } header.Set("Cookie", cookie) } @@ -95,12 +95,12 @@ func requestHeaders(single map[string]string, multi map[string][]string) (http.H header := make(http.Header) for _, key := range slices.Sorted(maps.Keys(multi)) { if !validToken(key) { - return nil, invocationError("request", ErrInvalidEvent, "invalid header name") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid header name") } name := http.CanonicalHeaderKey(key) for _, value := range multi[key] { if !validHeaderValue(value) { - return nil, invocationError("request", ErrInvalidEvent, "invalid header value") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid header value") } header[name] = append(header[name], value) } @@ -110,11 +110,11 @@ func requestHeaders(single map[string]string, multi map[string][]string) (http.H multiValues := header.Clone() for _, key := range slices.Sorted(maps.Keys(single)) { if !validToken(key) { - return nil, invocationError("request", ErrInvalidEvent, "invalid header name") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid header name") } value := single[key] if !validHeaderValue(value) { - return nil, invocationError("request", ErrInvalidEvent, "invalid header value") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid header value") } name := http.CanonicalHeaderKey(key) if !slices.Contains(multiValues[name], value) { @@ -126,21 +126,21 @@ func requestHeaders(single map[string]string, multi map[string][]string) (http.H func newRequest(ctx context.Context, r *http.Request, body string, binary bool) (*http.Request, error) { if ctx == nil { - return nil, invocationError("validate", ErrInvalidInvocation, "nil invocation context") + return nil, invocationError(OperationValidate, ErrInvalidInvocation, "nil invocation context") } if !validToken(r.Method) { - return nil, invocationError("request", ErrInvalidEvent, "invalid HTTP method") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid HTTP method") } host := r.Host header := r.Header if hosts, present := header["Host"]; present { if len(hosts) != 1 || hosts[0] == "" { - return nil, invocationError("request", ErrInvalidEvent, "invalid Host header") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid Host header") } host = hosts[0] } if host != "" && !validAuthority(host) { - return nil, invocationError("request", ErrInvalidEvent, "invalid request host") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid request host") } delete(header, "Host") if r.Proto == "" { @@ -148,14 +148,14 @@ func newRequest(ctx context.Context, r *http.Request, body string, binary bool) } major, minor, ok := http.ParseHTTPVersion(r.Proto) if !ok { - return nil, invocationError("request", ErrInvalidEvent, "invalid HTTP protocol") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid HTTP protocol") } var data []byte if binary { var err error data, err = base64.StdEncoding.DecodeString(body) if err != nil { - return nil, invocationError("request", ErrInvalidEvent, "invalid base64 request body") + return nil, invocationError(OperationRequest, ErrInvalidEvent, "invalid base64 request body") } } else { data = []byte(body) diff --git a/response_encoding.go b/response_encoding.go index ae4bd32..9047138 100644 --- a/response_encoding.go +++ b/response_encoding.go @@ -22,11 +22,11 @@ func marshalResponse(response any) ([]byte, error) { var out responseBuffer if err := json.MarshalWrite(&out, response); err != nil { if errors.Is(err, errResponseTooLarge) { - return nil, limitError("encode", "buffered_envelope", maxResponseBytes, errResponseTooLarge) + return nil, limitError(OperationEncode, ResourceBufferedEnvelope, maxResponseBytes, errResponseTooLarge) } // Codec errors can include application values. // Do not expose that chain. - return nil, invocationError("encode", ErrResponse, "response JSON encoding failed") + return nil, invocationError(OperationEncode, ErrResponse, "response JSON encoding failed") } return out.data, nil } @@ -63,7 +63,7 @@ func (b *responseBuffer) Write(p []byte) (int, error) { // It changes only the gateway envelope representation, never the content bytes. func encodeResponseBody(body []byte, header http.Header) (string, bool, error) { if len(body) > maxResponseBytes { - return "", false, limitError("response", "buffered_body", maxResponseBytes, errResponseTooLarge) + return "", false, limitError(OperationResponse, ResourceBufferedBody, maxResponseBytes, errResponseTooLarge) } if len(body) == 0 { return "", false, nil @@ -72,7 +72,7 @@ func encodeResponseBody(body []byte, header http.Header) (string, bool, error) { return string(body), false, nil } if base64.StdEncoding.EncodedLen(len(body)) > maxResponseBytes { - return "", false, limitError("response", "buffered_body", maxResponseBytes, errResponseTooLarge) + return "", false, limitError(OperationResponse, ResourceBufferedBody, maxResponseBytes, errResponseTooLarge) } return base64.StdEncoding.EncodeToString(body), true, nil } diff --git a/response_headers.go b/response_headers.go index 103ffc0..db4a832 100644 --- a/response_headers.go +++ b/response_headers.go @@ -13,10 +13,10 @@ const defaultResponseHeaderBudget = 256 * 1024 var ( errResponseHeaderBudget = errors.New("edge: response header budget exceeded") - errResponseHeaders = invocationError("response", ErrResponse, "invalid response headers") - errResponseTrailers = invocationError("response", ErrResponse, "response trailers are unsupported", http.ErrNotSupported) - errResponseUpgrade = invocationError("response", ErrResponse, "response upgrade is unsupported", http.ErrNotSupported) - errResponseHeaderRepeat = invocationError("response", ErrResponse, "repeated response field cannot be represented in payload 2.0") + errResponseHeaders = invocationError(OperationResponse, ErrResponse, "invalid response headers") + errResponseTrailers = invocationError(OperationResponse, ErrResponse, "response trailers are unsupported", http.ErrNotSupported) + errResponseUpgrade = invocationError(OperationResponse, ErrResponse, "response upgrade is unsupported", http.ErrNotSupported) + errResponseHeaderRepeat = invocationError(OperationResponse, ErrResponse, "repeated response field cannot be represented in payload 2.0") ) // responseHeaders owns its map and value slices. @@ -79,11 +79,11 @@ func chargeResponseHeaders(fields http.Header, remaining int) (int, error) { func snapshotResponseHeaders(fields http.Header, budget int) (*responseHeaders, error) { if budget <= 0 || budget > maxResponseBytes { - return nil, invocationError("validate", ErrInvalidInvocation, "invalid response header budget") + return nil, invocationError(OperationValidate, ErrInvalidInvocation, "invalid response header budget") } remaining, err := chargeResponseHeaders(fields, budget) if err != nil { - return nil, limitError("response", "response_headers", int64(budget), errResponseHeaderBudget) + return nil, limitError(OperationResponse, ResourceResponseHeaders, int64(budget), errResponseHeaderBudget) } keys := make([]string, 0, len(fields)) for name, values := range fields { @@ -167,7 +167,7 @@ func (h *responseHeaders) automatic(name, value string) error { } remaining, err := chargeResponseHeaders(http.Header{name: {value}}, h.remaining) if err != nil { - return limitError("response", "response_headers", int64(h.budget), errResponseHeaderBudget) + return limitError(OperationResponse, ResourceResponseHeaders, int64(h.budget), errResponseHeaderBudget) } h.fields[name] = []string{value} h.remaining = remaining diff --git a/stream.go b/stream.go index 2a50177..8bb68d7 100644 --- a/stream.go +++ b/stream.go @@ -9,9 +9,9 @@ import ( ) var ( - errStreamPanic = invocationError("stream", ErrStream, "stream producer panicked") - errStreamReporter = invocationError("stream", ErrStream, "stream error reporter panicked") - errStreamUnpublished = invocationError("stream", ErrStream, "stream producer ended without publishing a response") + errStreamPanic = invocationError(OperationStream, ErrStream, "stream producer panicked") + errStreamReporter = invocationError(OperationStream, ErrStream, "stream error reporter panicked") + errStreamUnpublished = invocationError(OperationStream, ErrStream, "stream producer ended without publishing a response") ) // streamOutput belongs to the producer. @@ -256,5 +256,5 @@ func (*responseStream) ContentType() string { // MarshalJSON deliberately declines serialization so the SDK selects its io.Reader response path. // Edge's framing codec uses JSON v2 separately. func (*responseStream) MarshalJSON() ([]byte, error) { - return nil, invocationError("encode", ErrStream, "streaming response cannot be marshaled as JSON") + return nil, invocationError(OperationEncode, ErrStream, "streaming response cannot be marshaled as JSON") } diff --git a/stream_encoding.go b/stream_encoding.go index b761897..4419cb2 100644 --- a/stream_encoding.go +++ b/stream_encoding.go @@ -13,7 +13,7 @@ var errStreamPrefixTooLarge = errors.New("edge: response metadata exceeds stream func encodeStreamPrefix(status int, headers *responseHeaders) ([]byte, error) { if status < 200 || status > 599 { - return nil, invocationError("response", ErrResponse, "invalid final response status") + return nil, invocationError(OperationResponse, ErrResponse, "invalid final response status") } metadata := struct { StatusCode int `json:"statusCode"` @@ -22,9 +22,9 @@ func encodeStreamPrefix(status int, headers *responseHeaders) ([]byte, error) { out := responseBuffer{limit: maxStreamPrefixBytes - 8} if err := json.MarshalWrite(&out, metadata); err != nil { if errors.Is(err, errResponseTooLarge) { - return nil, limitError("encode", "stream_metadata", maxStreamPrefixBytes, errStreamPrefixTooLarge) + return nil, limitError(OperationEncode, ResourceStreamMetadata, maxStreamPrefixBytes, errStreamPrefixTooLarge) } - return nil, invocationError("encode", ErrResponse, "streaming metadata JSON encoding failed") + return nil, invocationError(OperationEncode, ErrResponse, "streaming metadata JSON encoding failed") } // Allocate exactly the framed length. // The final zeroed bytes are the delimiter. diff --git a/streaming_adapter.go b/streaming_adapter.go index c956886..1905c47 100644 --- a/streaming_adapter.go +++ b/streaming_adapter.go @@ -79,7 +79,7 @@ func (a *StreamingAdapter) Handle(ctx context.Context, event jsontext.Value) (io return decodedEvent{}, err } if decoded.version != "" || decoded.v1 == nil { - return decodedEvent{}, invocationError("validate", ErrUnsupportedEvent, "streaming requires a REST proxy event") + return decodedEvent{}, invocationError(OperationValidate, ErrUnsupportedEvent, "streaming requires a REST proxy event") } return decoded, nil }, true) @@ -102,7 +102,7 @@ func (a *StreamingAdapter) HandleV1(ctx context.Context, event events.APIGateway func (a *StreamingAdapter) valid() error { if a == nil || a.handler == nil { - return invocationError("validate", ErrInvalidInvocation, "uninitialized streaming adapter") + return invocationError(OperationValidate, ErrInvalidInvocation, "uninitialized streaming adapter") } return nil } diff --git a/streaming_writer.go b/streaming_writer.go index 636c884..f60fab5 100644 --- a/streaming_writer.go +++ b/streaming_writer.go @@ -43,7 +43,7 @@ func (w *streamingWriter) WriteHeader(status int) { return } if status < 200 || status > 599 { - w.err = invocationError("response", ErrResponse, "invalid final response status") + w.err = invocationError(OperationResponse, ErrResponse, "invalid final response status") return } w.status = status @@ -81,11 +81,11 @@ func (w *streamingWriter) Write(p []byte) (int, error) { return 0, http.ErrBodyNotAllowed } if w.hasLength && int64(len(p)) > w.declaredLength-w.written { - w.err = invocationError("response", ErrResponse, "response exceeds declared content length", http.ErrContentLength) + w.err = invocationError(OperationResponse, ErrResponse, "response exceeds declared content length", http.ErrContentLength) return 0, w.err } if int64(len(p)) > math.MaxInt64-w.written { - w.err = invocationError("response", ErrResponse, "response byte count overflow") + w.err = invocationError(OperationResponse, ErrResponse, "response byte count overflow") return 0, w.err } if w.method == http.MethodHead { @@ -165,7 +165,7 @@ func (w *streamingWriter) writeBody(p []byte) (int, error) { case err == io.ErrClosedPipe: w.err = err // The bridge translates its own interruption safely. default: - w.err = invocationError("stream", ErrStream, "response body write failed") + w.err = invocationError(OperationStream, ErrStream, "response body write failed") } } return n, w.err @@ -183,7 +183,7 @@ func (w *streamingWriter) finish() error { return w.err } if w.method != http.MethodHead && w.hasLength && w.written != w.declaredLength { - w.err = invocationError("response", ErrResponse, "response body length mismatch", http.ErrContentLength) + w.err = invocationError(OperationResponse, ErrResponse, "response body length mismatch", http.ErrContentLength) return w.err } return w.publish()