Skip to content

Commit a921174

Browse files
MFCaballeroedsonmichaqueandyo-tykvladzabolotnyiVlad Zabolotnyi
authored
[TT-16767][Global] [Implementation] Centralised Error Overrides Infrastructure (#7867)
<!-- Provide a general summary of your changes in the Title above --> ## Description This PR introduces a centralized Error Overrides feature, allowing for the customization of error responses at the gateway level. This functionality enables users to standardize error formats, hide internal error details, and provide branded or localized messages for both gateway-generated errors (e.g., auth failures, rate limits) and upstream service errors (e.g., 4xx/5xx responses). The implementation is optimized for performance, featuring a near-zero overhead path when no overrides are configured. All matching rules, including regular expressions and inline templates, are pre-compiled at gateway startup to ensure minimal latency during error handling. <!-- Describe your changes in detail --> # Error Override Performance Benchmarks ## Executive Summary The error override feature adds **negligible performance overhead** to the error handling path: - **No overrides configured**: ~5 ns/op overhead (fast path with map length check) - **Direct message override**: ~5.8 μs/op vs ~11.1 μs/op baseline (48% **faster** than default template) - **With template execution**: ~7-11 μs/op (comparable to or faster than default template rendering) **Optimization:** The code checks if overrides exist before entering the override path, ensuring minimal impact on existing deployments. ## Detailed Results ### 1. ApplyOverride - Matching Performance Testing the core matching logic that determines if an override should be applied: ``` BenchmarkApplyOverride/no_overrides_configured 4.05 ns/op 0 B/op 0 allocs/op BenchmarkApplyOverride/exact_code_match 55.0 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/pattern_match_4xx 66.2 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/regex_pattern_match 219.8 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/regex_pattern_non-match 169.8 ns/op 0 B/op 0 allocs/op BenchmarkApplyOverride/JSON_body_field_match 226.9 ns/op 48 B/op 2 allocs/op BenchmarkApplyOverride/multiple_rules_-_first_match 235.5 ns/op 32 B/op 1 allocs/op ``` **Key Findings:** - **No configuration**: ~4 ns overhead - very low - **Exact code match**: ~55 ns (O(1) hash map lookup) - **Pattern match** (4xx/5xx): ~66 ns (prefix calculation + map lookup) - **Regex matching**: ~220 ns (compiled regex) - **JSON path matching**: ~227 ns (gjson library) ### 2. Flag-Based Matching (Error Classification) Flag matching uses the error classification system for semantic matching - matching by error *type* rather than text patterns: ``` BenchmarkApplyOverride/flag_match_-_exact_match 92.9 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/flag_match_-_no_classification 26.8 ns/op 0 B/op 0 allocs/op BenchmarkApplyOverride/flag_match_-_fallback_to_regex 193.9 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/multiple_flag_rules_-_first_match 104.7 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/multiple_flag_rules_-_catch_all 110.7 ns/op 32 B/op 1 allocs/op ``` **Flag vs Regex Performance Comparison:** ``` BenchmarkApplyOverride/flag_vs_regex_-_flag 75.1 ns/op 32 B/op 1 allocs/op BenchmarkApplyOverride/flag_vs_regex_-_regex 173.2 ns/op 0 B/op 0 allocs/op ``` **Key Findings:** - **Flag matching is ~2.3x faster than regex** (75 ns vs 173 ns) - **No classification in context**: 26.8 ns (very fast early exit) - **Multiple flag rules**: ~105-111 ns (efficient even with multiple rules) - **Fallback to regex**: ~194 ns (checks flag first, then regex) - Flag matching is a simple string comparison vs regex execution ### 3. WriteOverrideResponse vs WriteTemplateErrorResponse Direct comparison of error response writing: ``` Response Method Time Memory Allocs -------------------------------------------------------------------------------------- BenchmarkWriteOverrideResponse/direct_message 5.8 μs 7312 B 31 allocs BenchmarkWriteOverrideResponse/inline_template_JSON 9.5 μs 7745 B 49 allocs BenchmarkWriteOverrideResponse/inline_template_XML 7.4 μs 7513 B 38 allocs BenchmarkWriteOverrideResponse/file_template_JSON 10.2 μs 8001 B 51 allocs BenchmarkWriteOverrideResponse/file_template_XML 7.7 μs 7769 B 40 allocs BenchmarkWriteOverrideResponse/with_custom_headers 11.1 μs 8537 B 67 allocs BenchmarkWriteTemplateErrorResponse/default_JSON 11.1 μs 7532 B 41 allocs ``` **Key Findings:** - **Direct message writing**: 5.8 μs - **48% faster** than default template (11.1 μs) - Best performance when no templating needed - Writes JSON/XML response directly - **Inline template**: 7.4-9.5 μs - Also faster than default - Allows dynamic {{.StatusCode}} and {{.Message}} substitution - Very efficient - **File template**: 7.7-10.2 μs - Within acceptable range - Reuses existing template infrastructure - Performance varies by template complexity ### 4. Compilation Performance One-time cost during gateway startup or API reload: ``` BenchmarkCompileErrorOverrides/single_exact_code 0.76 μs 520 B 6 allocs BenchmarkCompileErrorOverrides/multiple_exact_codes 1.67 μs 1000 B 14 allocs BenchmarkCompileErrorOverrides/with_regex_patterns 0.76 μs 624 B 6 allocs BenchmarkCompileErrorOverrides/with_inline_templates 12.23 μs 8240 B 99 allocs BenchmarkCompileErrorOverrides/mixed_exact_and_patterns 2.01 μs 1374 B 17 allocs ``` **Key Findings:** - Simple configurations compile in ~0.8-2 μs - Regex compilation adds minimal overhead - Template compilation is more expensive (~12 μs) but happens only at startup - All costs are one-time during configuration load ## Fast Path Optimization Testing the entry point `tryWriteOverride` with empty vs configured overrides: ``` BenchmarkTryWriteOverride/empty_config_-_fast_path 5.14 ns/op 0 B/op 0 allocs/op BenchmarkTryWriteOverride/config_exists_-_match_with_direct_body 2450 ns/op 1824 B/op 22 allocs/op ``` **Key Findings:** - The optimization checks `len(e.Spec.GlobalConfig.ErrorOverrides) == 0` before proceeding - When empty (no overrides configured), returns immediately without atomic loads - Fast path overhead: ~5 ns with zero allocations - This is **476x faster** than processing a match (~2.5 μs) - This is **43x faster** than a regex match (~220 ns) ## Performance Impact Analysis ### Hot Path (Every Error Response) When **no overrides are configured** (most common case): - Overhead: **~5 ns** per error (fast path with map length check) - Memory: 0 bytes allocated - **Impact: Virtually zero** - immeasurable in real-world operations When **overrides are configured and match**: - Best case (direct message): **48% faster** than default template - Typical case (inline template): ~15-33% faster than default - Worst case (file template JSON): ~10 μs (comparable to default) ### Cold Path (Gateway Startup) Compilation happens once during: - Gateway initialization - API configuration reload - Overhead: ~0.8-12 μs depending on complexity - **Impact: Negligible** - happens infrequently ## Memory Allocation Analysis Memory allocations per error response: ``` Operation Allocations Bytes ---------------------------------------------------- No override check 0 allocs 0 B Exact code match 1 alloc 32 B Pattern match (4xx/5xx) 1 alloc 32 B Flag match (exact) 1 alloc 32 B Flag match (no classification) 0 allocs 0 B Regex pattern match 1 alloc 32 B Direct message write 31 allocs 7312 B Inline template execution 49 allocs 7745 B Default template 41 allocs 7532 B ``` **Key Findings:** - Zero allocations when no overrides configured - Zero allocations for flag match when no classification in context - Minimal allocation (32B) for matching logic - Direct message writing uses similar memory to default template - No memory leaks or unbounded allocations ## Scalability Considerations ### Large Body Handling ``` BenchmarkApplyOverride/large_body_truncation 1.28 μs 538 B 6 allocs ``` - Large bodies (>4KB) are truncated before pattern matching - Prevents performance degradation with large error responses - Adds ~1.3 μs overhead only when bodies exceed 4KB ### Multiple Rules ``` BenchmarkApplyOverride/multiple_rules_-_first_match 236 ns 32 B/op 1 allocs ``` - First-match semantics ensure O(n) worst case - In practice, most errors match within 1-2 rules - No performance degradation with reasonable rule counts ## Conclusions **Virtually zero overhead when disabled**: ~5 ns (fast path with map length check) - immeasurable in production **Optimized check path**: Early exit when no overrides configured prevents significant overhead **Status code matching is fast**: Both exact (~55 ns) and pattern (~66 ns) matching are very efficient **Flag matching is ~2.3x faster than regex**: 75 ns vs 173 ns for semantic error matching **Faster for simple overrides**: Direct message writing is 48% faster than default templates **Acceptable overhead for advanced features**: Template execution adds reasonable overhead for the flexibility gained **Efficient matching**: O(1) lookups for exact codes, fast flag comparison, pre-compiled regex **No memory leaks**: Bounded allocations, pre-compiled patterns ## Recommendations For **best performance**: 1. Use flag-based matching when possible (~2.3x faster than regex) 2. Use direct message writing (no template variables) when possible 3. Keep regex patterns simple 4. Pre-compile overrides at startup (already implemented) For **optimal flexibility**: 1. Use flag matching for semantic error types (e.g., `RLT` for rate limiting) 2. Use inline templates with {{.StatusCode}} and {{.Message}} 3. Use file templates for complex responses 4. Use JSON body field matching for structured upstream error responses 5. Use regex patterns as fallback when flag classification isn't available 6. Both exact and pattern (4xx/5xx) status code matching are efficient (~55-66 ns) --- **Test Environment:** - Machine: Apple M1 Pro - Go Version: 1.25.1 - OS: macOS (darwin/arm64) - Benchmark Duration: 3 seconds per benchmark ## Related Issue <!-- This project only accepts pull requests related to open issues. --> <!-- If suggesting a new feature or change, please discuss it in an issue first. --> <!-- If fixing a bug, there should be an issue describing it with steps to reproduce. --> <!-- OSS: Please link to the issue here. Tyk: please create/link the JIRA ticket. --> ## Motivation and Context <!-- Why is this change required? What problem does it solve? --> ## How This Has Been Tested <!-- Please describe in detail how you tested your changes --> <!-- Include details of your testing environment, and the tests --> <!-- you ran to see how your change affects other areas of the code, etc. --> <!-- This information is helpful for reviewers and QA. --> ## Screenshots (if appropriate) ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Refactoring or add test (improvements in base code or adds test coverage to functionality) ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply --> <!-- If there are no documentation updates required, mark the item as checked. --> <!-- Raise up any additional concerns not covered by the checklist. --> - [ ] I ensured that the documentation is up to date - [ ] I explained why this PR updates go.mod in detail with reasoning why it's required - [ ] I would like a code coverage CI quality gate exception and have explained why <!---TykTechnologies/jira-linter starts here--> ### Ticket Details <details> <summary> <a href="https://tyktech.atlassian.net/browse/TT-16767" title="TT-16767" target="_blank">TT-16767</a> </summary> | | | |---------|----| | Status | Ready for Testing | | Summary | [1a] Implement Centralised Error Overrides Infrastructure | Generated at: 2026-03-17 16:11:43 </details> <!---TykTechnologies/jira-linter ends here--> --------- Co-authored-by: Edson Michaque <edson@michaque.com> Co-authored-by: andyo-tyk <99968932+andyo-tyk@users.noreply.github.com> Co-authored-by: Vlad Zabolotnyi <109525963+vladzabolotnyi@users.noreply.github.com> Co-authored-by: Vlad Zabolotnyi <vlad.z@tyk.io> Co-authored-by: Leonid Bugaev <leonsbox@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: andrei-tyk <97896463+andrei-tyk@users.noreply.github.com> Co-authored-by: Laurentiu <6229829+lghiur@users.noreply.github.com> Co-authored-by: Burak Sezer <burak.sezer.developer@gmail.com>
1 parent 9792d07 commit a921174

File tree

97 files changed

+13583
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+13583
-27
lines changed

apidef/api_definitions.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,10 @@ type APIDefinition struct {
792792
// SecurityRequirements stores all OAS security requirements (auto-populated from OpenAPI description import)
793793
// When len(SecurityRequirements) > 1, OR logic is automatically applied
794794
SecurityRequirements [][]string `json:"security_requirements,omitempty" bson:"security_requirements,omitempty"`
795+
796+
// ErrorOverrides contains the configurations for error response customization.
797+
ErrorOverrides ErrorOverridesMap `bson:"error_overrides" json:"error_overrides"`
798+
ErrorOverridesDisabled bool `bson:"error_overrides_disabled" json:"error_overrides_disabled" `
795799
}
796800

797801
type JWK struct {
@@ -1601,6 +1605,26 @@ func DummyAPI() APIDefinition {
16011605
},
16021606
}
16031607

1608+
errorOverrides := ErrorOverridesMap{
1609+
"400": []ErrorOverride{
1610+
{
1611+
Match: &ErrorMatcher{
1612+
Flag: "RLT",
1613+
MessagePattern: ".*",
1614+
BodyField: "error",
1615+
BodyValue: "invalid",
1616+
},
1617+
Response: ErrorResponse{
1618+
StatusCode: 400,
1619+
Body: "Bad Request",
1620+
Message: "Error",
1621+
Template: "error",
1622+
Headers: map[string]string{"X-Error": "true"},
1623+
},
1624+
},
1625+
},
1626+
}
1627+
16041628
return APIDefinition{
16051629
VersionData: versionData,
16061630
ConfigData: map[string]interface{}{},
@@ -1632,9 +1656,10 @@ func DummyAPI() APIDefinition {
16321656
Proxy: ProxyConfig{
16331657
DisableStripSlash: true,
16341658
},
1635-
CORS: defaultCORSConfig,
1636-
Tags: []string{},
1637-
GraphQL: graphql,
1659+
CORS: defaultCORSConfig,
1660+
Tags: []string{},
1661+
GraphQL: graphql,
1662+
ErrorOverrides: errorOverrides,
16381663
}
16391664
}
16401665

apidef/api_definitions_test.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011

12+
"github.com/TykTechnologies/tyk/internal/errors"
1113
"github.com/TykTechnologies/tyk/internal/service/gojsonschema"
1214
)
1315

@@ -718,6 +720,258 @@ func TestAPIDefinition_IsBaseAPI(t *testing.T) {
718720
}
719721
}
720722

723+
func TestAPIDefinition_ErrorOverrides_UnmarshalJSON(t *testing.T) {
724+
t.Run("basic error override", func(t *testing.T) {
725+
jsonData := []byte(`{
726+
"name": "Test API Override Upstream",
727+
"api_id": "test-api-override-upstream",
728+
"org_id": "default",
729+
"use_keyless": true,
730+
"error_overrides_disabled": false,
731+
"error_overrides": {
732+
"404": [
733+
{
734+
"response": {
735+
"status_code": 420,
736+
"body": "{\"error\": \"api_level_upstream\", \"status\": 404}",
737+
"headers": {
738+
"X-Error-Flag": "UPSTREAM-API",
739+
"X-Override-Applied": "true"
740+
}
741+
}
742+
}
743+
]
744+
}
745+
}`)
746+
747+
var api APIDefinition
748+
err := json.Unmarshal(jsonData, &api)
749+
require.NoError(t, err)
750+
751+
require.NotNil(t, api.ErrorOverrides)
752+
require.Len(t, api.ErrorOverrides["404"], 1)
753+
assert.Equal(t, 420, api.ErrorOverrides["404"][0].Response.StatusCode)
754+
assert.False(t, api.ErrorOverridesDisabled)
755+
})
756+
757+
t.Run("complete error override with all fields", func(t *testing.T) {
758+
jsonData := []byte(`{
759+
"name": "Test API Complete Override",
760+
"api_id": "test-api-complete-override",
761+
"org_id": "default",
762+
"use_keyless": true,
763+
"error_overrides_disabled": false,
764+
"error_overrides": {
765+
"500": [
766+
{
767+
"match": {
768+
"flag": "TLE",
769+
"message_pattern": "timeout.*error",
770+
"body_field": "error.type",
771+
"body_value": "timeout"
772+
},
773+
"response": {
774+
"status_code": 503,
775+
"body": "{\"error\": \"Certificate expired\", \"retry_after\": 30}",
776+
"message": "Custom timeout error message",
777+
"template": "error_template.html",
778+
"headers": {
779+
"X-Custom-Error": "TIMEOUT",
780+
"Retry-After": "30",
781+
"Content-Type": "application/json"
782+
}
783+
}
784+
}
785+
],
786+
"401": [
787+
{
788+
"match": {
789+
"flag": "TLI",
790+
"body_field": "error.code",
791+
"body_value": "INVALID_TOKEN"
792+
},
793+
"response": {
794+
"status_code": 401,
795+
"body": "{\"error\": \"Certificate invalid\"}",
796+
"message": "Token validation failed",
797+
"headers": {
798+
"WWW-Authenticate": "Bearer",
799+
"X-Auth-Error": "true"
800+
}
801+
}
802+
}
803+
]
804+
}
805+
}`)
806+
807+
var api APIDefinition
808+
err := json.Unmarshal(jsonData, &api)
809+
require.NoError(t, err)
810+
811+
require.NotNil(t, api.ErrorOverrides)
812+
require.False(t, api.ErrorOverridesDisabled)
813+
require.Len(t, api.ErrorOverrides, 2)
814+
815+
require.Len(t, api.ErrorOverrides["500"], 1)
816+
override500 := api.ErrorOverrides["500"][0]
817+
818+
require.NotNil(t, override500.Match)
819+
assert.Equal(t, errors.TLE, override500.Match.Flag)
820+
assert.Equal(t, "timeout.*error", override500.Match.MessagePattern)
821+
assert.Equal(t, "error.type", override500.Match.BodyField)
822+
assert.Equal(t, "timeout", override500.Match.BodyValue)
823+
824+
assert.Equal(t, 503, override500.Response.StatusCode)
825+
assert.Equal(t, "{\"error\": \"Certificate expired\", \"retry_after\": 30}", override500.Response.Body)
826+
assert.Equal(t, "Custom timeout error message", override500.Response.Message)
827+
assert.Equal(t, "error_template.html", override500.Response.Template)
828+
require.Len(t, override500.Response.Headers, 3)
829+
assert.Equal(t, "TIMEOUT", override500.Response.Headers["X-Custom-Error"])
830+
assert.Equal(t, "30", override500.Response.Headers["Retry-After"])
831+
assert.Equal(t, "application/json", override500.Response.Headers["Content-Type"])
832+
833+
require.Len(t, api.ErrorOverrides["401"], 1)
834+
override401 := api.ErrorOverrides["401"][0]
835+
836+
require.NotNil(t, override401.Match)
837+
assert.Equal(t, errors.TLI, override401.Match.Flag)
838+
assert.Equal(t, "", override401.Match.MessagePattern)
839+
assert.Equal(t, "error.code", override401.Match.BodyField)
840+
assert.Equal(t, "INVALID_TOKEN", override401.Match.BodyValue)
841+
842+
assert.Equal(t, 401, override401.Response.StatusCode)
843+
assert.Equal(t, "{\"error\": \"Certificate invalid\"}", override401.Response.Body)
844+
assert.Equal(t, "Token validation failed", override401.Response.Message)
845+
assert.Equal(t, "", override401.Response.Template)
846+
require.Len(t, override401.Response.Headers, 2)
847+
assert.Equal(t, "Bearer", override401.Response.Headers["WWW-Authenticate"])
848+
assert.Equal(t, "true", override401.Response.Headers["X-Auth-Error"])
849+
})
850+
851+
t.Run("multiple overrides for same status code", func(t *testing.T) {
852+
jsonData := []byte(`{
853+
"name": "Test API Multiple Overrides",
854+
"api_id": "test-api-multiple-overrides",
855+
"org_id": "default",
856+
"use_keyless": true,
857+
"error_overrides_disabled": true,
858+
"error_overrides": {
859+
"400": [
860+
{
861+
"match": {
862+
"body_field": "error.type",
863+
"body_value": "validation"
864+
},
865+
"response": {
866+
"status_code": 422,
867+
"body": "{\"error\": \"Validation failed\"}"
868+
}
869+
},
870+
{
871+
"match": {
872+
"body_field": "error.type",
873+
"body_value": "malformed"
874+
},
875+
"response": {
876+
"status_code": 400,
877+
"body": "{\"error\": \"Malformed request\"}"
878+
}
879+
}
880+
]
881+
}
882+
}`)
883+
884+
var api APIDefinition
885+
err := json.Unmarshal(jsonData, &api)
886+
require.NoError(t, err)
887+
888+
require.NotNil(t, api.ErrorOverrides)
889+
require.True(t, api.ErrorOverridesDisabled)
890+
require.Len(t, api.ErrorOverrides["400"], 2)
891+
892+
assert.Equal(t, "validation", api.ErrorOverrides["400"][0].Match.BodyValue)
893+
assert.Equal(t, 422, api.ErrorOverrides["400"][0].Response.StatusCode)
894+
895+
assert.Equal(t, "malformed", api.ErrorOverrides["400"][1].Match.BodyValue)
896+
assert.Equal(t, 400, api.ErrorOverrides["400"][1].Response.StatusCode)
897+
})
898+
899+
t.Run("override without match criteria", func(t *testing.T) {
900+
jsonData := []byte(`{
901+
"name": "Test API No Match Override",
902+
"api_id": "test-api-no-match-override",
903+
"org_id": "default",
904+
"use_keyless": true,
905+
"error_overrides_disabled": false,
906+
"error_overrides": {
907+
"502": [
908+
{
909+
"response": {
910+
"status_code": 503,
911+
"body": "{\"error\": \"Service unavailable\"}",
912+
"message": "Upstream service is down",
913+
"template": "maintenance.html",
914+
"headers": {
915+
"X-Maintenance": "true"
916+
}
917+
}
918+
}
919+
]
920+
}
921+
}`)
922+
923+
var api APIDefinition
924+
err := json.Unmarshal(jsonData, &api)
925+
require.NoError(t, err)
926+
927+
require.NotNil(t, api.ErrorOverrides)
928+
require.False(t, api.ErrorOverridesDisabled)
929+
require.Len(t, api.ErrorOverrides["502"], 1)
930+
931+
override := api.ErrorOverrides["502"][0]
932+
assert.Nil(t, override.Match)
933+
assert.Equal(t, 503, override.Response.StatusCode)
934+
assert.Equal(t, "{\"error\": \"Service unavailable\"}", override.Response.Body)
935+
assert.Equal(t, "Upstream service is down", override.Response.Message)
936+
assert.Equal(t, "maintenance.html", override.Response.Template)
937+
assert.Equal(t, "true", override.Response.Headers["X-Maintenance"])
938+
})
939+
940+
t.Run("empty error overrides", func(t *testing.T) {
941+
jsonData := []byte(`{
942+
"name": "Test API Empty Overrides",
943+
"api_id": "test-api-empty-overrides",
944+
"org_id": "default",
945+
"use_keyless": true,
946+
"error_overrides": {}
947+
}`)
948+
949+
var api APIDefinition
950+
err := json.Unmarshal(jsonData, &api)
951+
require.NoError(t, err)
952+
953+
require.NotNil(t, api.ErrorOverrides)
954+
require.False(t, api.ErrorOverridesDisabled)
955+
assert.Len(t, api.ErrorOverrides, 0)
956+
})
957+
958+
t.Run("nil error overrides", func(t *testing.T) {
959+
jsonData := []byte(`{
960+
"name": "Test API Nil Overrides",
961+
"api_id": "test-api-nil-overrides",
962+
"org_id": "default",
963+
"use_keyless": true
964+
}`)
965+
966+
var api APIDefinition
967+
err := json.Unmarshal(jsonData, &api)
968+
require.NoError(t, err)
969+
970+
assert.Nil(t, api.ErrorOverrides)
971+
require.False(t, api.ErrorOverridesDisabled)
972+
})
973+
}
974+
721975
func TestAPIDefinition_IsBaseAPIWithVersioning(t *testing.T) {
722976
tests := []struct {
723977
name string

0 commit comments

Comments
 (0)