Skip to content
4 changes: 2 additions & 2 deletions cmd/workflow/simulate/chain/evm/limited_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ func NewLimitedEVMChain(inner evmserver.ClientCapability, limits chain.Limits) *
func (l *LimitedEVMChain) WriteReport(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) {
if l.limits.ReportSize > 0 && input.Report != nil && len(input.Report.RawReport) > l.limits.ReportSize {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), l.limits.ReportSize),
fmt.Errorf("EVM chain write report of %d bytes exceeds the simulation limit of %d bytes. This limit mirrors a production constraint.\nReduce the report size written to chain. Use 'cre workflow limits export' to customize limits, or --limits=none to disable", len(input.Report.RawReport), l.limits.ReportSize),
caperrors.ResourceExhausted,
)
}

if l.limits.GasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > l.limits.GasLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, l.limits.GasLimit),
fmt.Errorf("EVM gas of %d gas units exceeds the simulation limit of %d gas units. This limit mirrors a production constraint.\nReduce gas_config.gas_limit in your chain write step. Use 'cre workflow limits export' to customize limits, or --limits=none to disable", input.GasConfig.GasLimit, l.limits.GasLimit),
caperrors.ResourceExhausted,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) {
})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "chain write report size 5 bytes exceeds limit of 4 bytes")
assert.Contains(t, err.Error(), "EVM chain write report of 5 bytes exceeds the simulation limit of 4 bytes")
assert.Equal(t, 0, inner.writeReportCalls)
}

Expand All @@ -113,7 +113,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) {
})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "EVM gas limit 11 exceeds maximum of 10")
assert.Contains(t, err.Error(), "EVM gas of 11 gas units exceeds the simulation limit of 10 gas units")
assert.Equal(t, 0, inner.writeReportCalls)
}

Expand Down
62 changes: 62 additions & 0 deletions cmd/workflow/simulate/limit_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package simulate

import "fmt"

// LimitKind identifies a specific simulation limit type, allowing callers to
// distinguish limit-exceeded failures from other errors via errors.As.
type LimitKind string

const (
LimitWASMBinary LimitKind = "wasm_binary_size"
LimitWASMCompressedBinary LimitKind = "wasm_compressed_binary_size"
LimitHTTPRequest LimitKind = "http_request_size"
LimitHTTPResponse LimitKind = "http_response_size"
LimitConfHTTPRequest LimitKind = "confidential_http_request_size"
LimitConfHTTPResponse LimitKind = "confidential_http_response_size"
LimitConsensusObservation LimitKind = "consensus_observation_size"
LimitChainWriteReport LimitKind = "chain_write_report_size"
LimitEVMGas LimitKind = "evm_gas"
)

// LimitExceededError is a typed error for simulation limit violations.
// Callers can use errors.As to retrieve the Kind and distinguish limit
// failures from other errors without string matching.
type LimitExceededError struct {
Kind LimitKind
Msg string
}

func (e *LimitExceededError) Error() string { return e.Msg }

// limitExceeded builds a user-facing error for a byte-size simulation limit
// violation. mirrorsProd should be true when the limit directly maps to a
// production runtime constraint.
func limitExceeded(kind LimitKind, resource string, actual, limit uint64, mirrorsProd bool, remediation string) *LimitExceededError {
prod := " This limit mirrors a production constraint."
if !mirrorsProd {
prod = ""
}
return &LimitExceededError{
Kind: kind,
Msg: fmt.Sprintf(
"%s of %d bytes exceeds the simulation limit of %d bytes.%s\n%s. Use 'cre workflow limits export' to customize limits, or --limits=none to disable.",
resource, actual, limit, prod, remediation,
),
}
}

// limitExceededUnit builds a user-facing error for a non-byte simulation limit
// (e.g., EVM gas units).
func limitExceededUnit(kind LimitKind, resource string, actual, limit uint64, unit string, mirrorsProd bool, remediation string) *LimitExceededError {
prod := " This limit mirrors a production constraint."
if !mirrorsProd {
prod = ""
}
return &LimitExceededError{
Kind: kind,
Msg: fmt.Sprintf(
"%s of %d %s exceeds the simulation limit of %d %s.%s\n%s. Use 'cre workflow limits export' to customize limits, or --limits=none to disable.",
resource, actual, unit, limit, unit, prod, remediation,
),
}
}
151 changes: 151 additions & 0 deletions cmd/workflow/simulate/limit_errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package simulate

import (
"errors"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLimitExceededMessageFormat(t *testing.T) {
t.Parallel()

tests := []struct {
name string
kind LimitKind
resource string
actual uint64
limit uint64
mirrorsProd bool
remediation string
wantSubstrs []string
notWant []string
}{
{
name: "WASM binary",
kind: LimitWASMBinary,
resource: "WASM binary",
actual: 200,
limit: 100,
mirrorsProd: true,
remediation: "Reduce compiled binary size",
wantSubstrs: []string{
"WASM binary",
"200 bytes",
"100 bytes",
"production",
"cre workflow limits export",
"--limits=none",
"Reduce compiled binary size",
},
},
{
name: "HTTP request",
kind: LimitHTTPRequest,
resource: "HTTP request body",
actual: 5000,
limit: 1000,
mirrorsProd: true,
remediation: "Reduce the request payload",
wantSubstrs: []string{
"HTTP request body",
"5000 bytes",
"1000 bytes",
"production",
"cre workflow limits export",
"--limits=none",
},
},
{
name: "consensus observation",
kind: LimitConsensusObservation,
resource: "Consensus observation",
actual: 30000,
limit: 25000,
mirrorsProd: true,
remediation: "Reduce data passed",
wantSubstrs: []string{
"Consensus observation",
"30000 bytes",
"25000 bytes",
"production",
},
},
{
name: "non-prod mirror limit",
kind: LimitChainWriteReport,
resource: "Some limit",
actual: 10,
limit: 5,
mirrorsProd: false,
remediation: "Do something",
wantSubstrs: []string{
"Some limit",
"10 bytes",
"5 bytes",
},
notWant: []string{"production"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := limitExceeded(tc.kind, tc.resource, tc.actual, tc.limit, tc.mirrorsProd, tc.remediation)

require.NotNil(t, err)
assert.Equal(t, tc.kind, err.Kind)

for _, want := range tc.wantSubstrs {
assert.True(t, strings.Contains(err.Error(), want),
"expected %q to contain %q", err.Error(), want)
}
for _, notWant := range tc.notWant {
assert.False(t, strings.Contains(err.Error(), notWant),
"expected %q NOT to contain %q", err.Error(), notWant)
}
})
}
}

func TestLimitExceededUnitMessageFormat(t *testing.T) {
t.Parallel()

err := limitExceededUnit(LimitEVMGas, "EVM gas", 6_000_000, 5_000_000, "gas units", true, "Reduce gas_config.gas_limit")

require.NotNil(t, err)
assert.Equal(t, LimitEVMGas, err.Kind)
assert.Contains(t, err.Error(), "EVM gas")
assert.Contains(t, err.Error(), "6000000 gas units")
assert.Contains(t, err.Error(), "5000000 gas units")
assert.Contains(t, err.Error(), "production")
assert.Contains(t, err.Error(), "cre workflow limits export")
assert.Contains(t, err.Error(), "--limits=none")
}

func TestLimitExceededErrorsAs(t *testing.T) {
t.Parallel()

err := limitExceeded(LimitHTTPResponse, "HTTP response body", 200, 100, true, "Filter the response")

// LimitExceededError is directly usable with errors.As.
var limitErr *LimitExceededError
require.True(t, errors.As(err, &limitErr))
assert.Equal(t, LimitHTTPResponse, limitErr.Kind)
}

func TestLimitExceededWrappedErrorsAs(t *testing.T) {
t.Parallel()

// Verify that wrapping in fmt.Errorf still allows errors.As unwrapping.
inner := limitExceeded(LimitWASMBinary, "WASM binary", 200, 100, true, "Reduce size")
wrapped := errors.New(inner.Error()) // simulate wrapping without %w

// When wrapped without %w, errors.As won't traverse; verify the direct case works.
var limitErr *LimitExceededError
assert.True(t, errors.As(inner, &limitErr))
assert.Equal(t, LimitWASMBinary, limitErr.Kind)
_ = wrapped // just to avoid unused variable
}
16 changes: 10 additions & 6 deletions cmd/workflow/simulate/limited_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package simulate

import (
"context"
"fmt"
"time"

"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -39,7 +38,8 @@ func (l *LimitedHTTPAction) SendRequest(ctx context.Context, metadata commonCap.
reqLimit := l.limits.HTTPRequestSizeLimit()
if reqLimit > 0 && len(input.GetBody()) > reqLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: HTTP request body size %d bytes exceeds limit of %d bytes", len(input.GetBody()), reqLimit),
limitExceeded(LimitHTTPRequest, "HTTP request body", uint64(len(input.GetBody())), uint64(reqLimit), true,
"Reduce the request payload in your http_action step"),
caperrors.ResourceExhausted,
)
}
Expand All @@ -62,7 +62,8 @@ func (l *LimitedHTTPAction) SendRequest(ctx context.Context, metadata commonCap.
respLimit := l.limits.HTTPResponseSizeLimit()
if resp != nil && resp.Response != nil && respLimit > 0 && len(resp.Response.GetBody()) > respLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: HTTP response body size %d bytes exceeds limit of %d bytes", len(resp.Response.GetBody()), respLimit),
limitExceeded(LimitHTTPResponse, "HTTP response body", uint64(len(resp.Response.GetBody())), uint64(respLimit), true,
"The upstream returned an oversized response; filter or paginate before consuming"),
caperrors.ResourceExhausted,
)
}
Expand Down Expand Up @@ -102,7 +103,8 @@ func (l *LimitedConfidentialHTTPAction) SendRequest(ctx context.Context, metadat
reqSize := len(input.GetRequest().GetBodyString()) + len(input.GetRequest().GetBodyBytes())
if reqSize > reqLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: confidential HTTP request body size %d bytes exceeds limit of %d bytes", reqSize, reqLimit),
limitExceeded(LimitConfHTTPRequest, "Confidential HTTP request body", uint64(reqSize), uint64(reqLimit), true,
"Reduce the payload to the confidential_http_action step"),
caperrors.ResourceExhausted,
)
}
Expand All @@ -126,7 +128,8 @@ func (l *LimitedConfidentialHTTPAction) SendRequest(ctx context.Context, metadat
respLimit := l.limits.ConfHTTPResponseSizeLimit()
if resp != nil && resp.Response != nil && respLimit > 0 && len(resp.Response.GetBody()) > respLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: confidential HTTP response body size %d bytes exceeds limit of %d bytes", len(resp.Response.GetBody()), respLimit),
limitExceeded(LimitConfHTTPResponse, "Confidential HTTP response body", uint64(len(resp.Response.GetBody())), uint64(respLimit), true,
"The upstream returned an oversized response; filter before consuming"),
caperrors.ResourceExhausted,
)
}
Expand Down Expand Up @@ -168,7 +171,8 @@ func (l *LimitedConsensusNoDAG) Simple(ctx context.Context, metadata commonCap.R
inputSize := proto.Size(input)
if inputSize > obsLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: consensus observation size %d bytes exceeds limit of %d bytes", inputSize, obsLimit),
limitExceeded(LimitConsensusObservation, "Consensus observation", uint64(inputSize), uint64(obsLimit), true, //nolint:gosec // proto.Size always returns non-negative
"Reduce data passed as observations to the consensus step"),
caperrors.ResourceExhausted,
)
}
Expand Down
10 changes: 5 additions & 5 deletions cmd/workflow/simulate/limited_capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func TestLimitedHTTPActionRejectsOversizedRequest(t *testing.T) {
resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{Body: []byte("12345")})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "HTTP request body size 5 bytes exceeds limit of 4 bytes")
assert.Contains(t, err.Error(), "HTTP request body of 5 bytes exceeds the simulation limit of 4 bytes")
assert.Equal(t, 0, inner.sendRequestCalls)
}

Expand Down Expand Up @@ -158,7 +158,7 @@ func TestLimitedHTTPActionRejectsOversizedResponse(t *testing.T) {
resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "HTTP response body size 4 bytes exceeds limit of 3 bytes")
assert.Contains(t, err.Error(), "HTTP response body of 4 bytes exceeds the simulation limit of 3 bytes")
assert.Equal(t, 1, inner.sendRequestCalls)
}

Expand Down Expand Up @@ -197,7 +197,7 @@ func TestLimitedConfidentialHTTPActionRejectsOversizedRequest(t *testing.T) {
})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "confidential HTTP request body size 5 bytes exceeds limit of 4 bytes")
assert.Contains(t, err.Error(), "Confidential HTTP request body of 5 bytes exceeds the simulation limit of 4 bytes")
assert.Equal(t, 0, inner.sendRequestCalls)
}

Expand Down Expand Up @@ -251,7 +251,7 @@ func TestLimitedConfidentialHTTPActionRejectsOversizedResponse(t *testing.T) {
resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &confidentialhttp.ConfidentialHTTPRequest{})
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "confidential HTTP response body size 4 bytes exceeds limit of 3 bytes")
assert.Contains(t, err.Error(), "Confidential HTTP response body of 4 bytes exceeds the simulation limit of 3 bytes")
assert.Equal(t, 1, inner.sendRequestCalls)
}

Expand All @@ -271,7 +271,7 @@ func TestLimitedConsensusNoDAGSimpleRejectsOversizedObservation(t *testing.T) {
resp, err := wrapper.Simple(context.Background(), commonCap.RequestMetadata{}, input)
require.Error(t, err)
assert.Nil(t, resp)
assert.Contains(t, err.Error(), "consensus observation size")
assert.Contains(t, err.Error(), "Consensus observation of")
assert.Equal(t, 0, inner.simpleCalls)
}

Expand Down
Loading
Loading