Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
908b5e0
init
MFCaballero Mar 9, 2026
00e175c
init
MFCaballero Mar 9, 2026
5f8807f
Merge branch 'master' of github.com:TykTechnologies/tyk into TT-16767
MFCaballero Mar 9, 2026
f825b1c
fixes
MFCaballero Mar 9, 2026
3d39188
linters
MFCaballero Mar 9, 2026
6ef8286
Merge branch 'master' into TT-16767
MFCaballero Mar 10, 2026
39d1335
update bench
MFCaballero Mar 10, 2026
87ec5c8
Merge branch 'master' into TT-16767
MFCaballero Mar 10, 2026
d97dc15
Merge branch 'master' into TT-16767
edsonmichaque Mar 11, 2026
fa9dfdb
Merge branch 'master' into TT-16767
MFCaballero Mar 11, 2026
5cee70f
simplified comments
MFCaballero Mar 11, 2026
05c6909
Apply suggestion from @andyo-tyk
MFCaballero Mar 12, 2026
df7e005
Apply suggestions from code review
MFCaballero Mar 12, 2026
acf9089
Merge branch 'master' into TT-16767
MFCaballero Mar 12, 2026
d98c0cd
refactor: move error overrides types and methods to apidef to avoid i…
vladzabolotnyi Mar 16, 2026
b2b0220
unify to explicit config naming status_code
MFCaballero Mar 17, 2026
05eb46b
fix format issues
MFCaballero Mar 17, 2026
a1ef045
[TT-16775] Testing Centralised ErrorOverrides Infrastructure (#7878)
MFCaballero Mar 31, 2026
8e3e920
Merge branch 'master' into TT-16767
MFCaballero Mar 31, 2026
6088067
Merge branch 'master' into TT-16767
MFCaballero Apr 1, 2026
9bfdc18
Merge branch 'master' into TT-16767
MFCaballero Apr 2, 2026
bf6cc3d
[TT-16772] Implement Upstream Error Response Overrides (#7896)
MFCaballero Apr 7, 2026
fca75d0
Merge branch 'master' into TT-16767
MFCaballero Apr 7, 2026
6071b1a
Merge branch 'master' into TT-16767
MFCaballero Apr 13, 2026
b259fa8
[TT-16770] Support Template Data & RFC 7807 Context for Validation Er…
buraksezer Apr 13, 2026
36aa606
Merge branch 'master' of github.com:TykTechnologies/tyk into TT-16767
MFCaballero Apr 20, 2026
8842fa5
Merge branch 'TT-16767' of github.com:TykTechnologies/tyk into TT-16767
MFCaballero Apr 20, 2026
9246a6a
Merge branch 'master' into TT-16767
MFCaballero Apr 20, 2026
fbe9f8a
fix sonarqube
MFCaballero Apr 20, 2026
41b4354
Merge branch 'TT-16767' of github.com:TykTechnologies/tyk into TT-16767
MFCaballero Apr 20, 2026
75f0afd
Merge branch 'master' into TT-16767
MFCaballero Apr 20, 2026
0e739a5
fix fmt
MFCaballero Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,10 @@ type APIDefinition struct {
// SecurityRequirements stores all OAS security requirements (auto-populated from OpenAPI description import)
// When len(SecurityRequirements) > 1, OR logic is automatically applied
SecurityRequirements [][]string `json:"security_requirements,omitempty" bson:"security_requirements,omitempty"`

// ErrorOverrides contains the configurations for error response customization.
ErrorOverrides ErrorOverridesMap `bson:"error_overrides" json:"error_overrides"`
ErrorOverridesDisabled bool `bson:"error_overrides_disabled" json:"error_overrides_disabled" `
}

type JWK struct {
Expand Down Expand Up @@ -1601,6 +1605,26 @@ func DummyAPI() APIDefinition {
},
}

errorOverrides := ErrorOverridesMap{
"400": []ErrorOverride{
{
Match: &ErrorMatcher{
Flag: "RLT",
MessagePattern: ".*",
BodyField: "error",
BodyValue: "invalid",
},
Response: ErrorResponse{
StatusCode: 400,
Body: "Bad Request",
Message: "Error",
Template: "error",
Headers: map[string]string{"X-Error": "true"},
},
},
},
}

return APIDefinition{
VersionData: versionData,
ConfigData: map[string]interface{}{},
Expand Down Expand Up @@ -1632,9 +1656,10 @@ func DummyAPI() APIDefinition {
Proxy: ProxyConfig{
DisableStripSlash: true,
},
CORS: defaultCORSConfig,
Tags: []string{},
GraphQL: graphql,
CORS: defaultCORSConfig,
Tags: []string{},
GraphQL: graphql,
ErrorOverrides: errorOverrides,
}
}

Expand Down
254 changes: 254 additions & 0 deletions apidef/api_definitions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"testing"

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

"github.com/TykTechnologies/tyk/internal/errors"
"github.com/TykTechnologies/tyk/internal/service/gojsonschema"
)

Expand Down Expand Up @@ -718,6 +720,258 @@ func TestAPIDefinition_IsBaseAPI(t *testing.T) {
}
}

func TestAPIDefinition_ErrorOverrides_UnmarshalJSON(t *testing.T) {
t.Run("basic error override", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API Override Upstream",
"api_id": "test-api-override-upstream",
"org_id": "default",
"use_keyless": true,
"error_overrides_disabled": false,
"error_overrides": {
"404": [
{
"response": {
"status_code": 420,
"body": "{\"error\": \"api_level_upstream\", \"status\": 404}",
"headers": {
"X-Error-Flag": "UPSTREAM-API",
"X-Override-Applied": "true"
}
}
}
]
}
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

require.NotNil(t, api.ErrorOverrides)
require.Len(t, api.ErrorOverrides["404"], 1)
assert.Equal(t, 420, api.ErrorOverrides["404"][0].Response.StatusCode)
assert.False(t, api.ErrorOverridesDisabled)
})

t.Run("complete error override with all fields", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API Complete Override",
"api_id": "test-api-complete-override",
"org_id": "default",
"use_keyless": true,
"error_overrides_disabled": false,
"error_overrides": {
"500": [
{
"match": {
"flag": "TLE",
"message_pattern": "timeout.*error",
"body_field": "error.type",
"body_value": "timeout"
},
"response": {
"status_code": 503,
"body": "{\"error\": \"Certificate expired\", \"retry_after\": 30}",
"message": "Custom timeout error message",
"template": "error_template.html",
"headers": {
"X-Custom-Error": "TIMEOUT",
"Retry-After": "30",
"Content-Type": "application/json"
}
}
}
],
"401": [
{
"match": {
"flag": "TLI",
"body_field": "error.code",
"body_value": "INVALID_TOKEN"
},
"response": {
"status_code": 401,
"body": "{\"error\": \"Certificate invalid\"}",
"message": "Token validation failed",
"headers": {
"WWW-Authenticate": "Bearer",
"X-Auth-Error": "true"
}
}
}
]
}
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

require.NotNil(t, api.ErrorOverrides)
require.False(t, api.ErrorOverridesDisabled)
require.Len(t, api.ErrorOverrides, 2)

require.Len(t, api.ErrorOverrides["500"], 1)
override500 := api.ErrorOverrides["500"][0]

require.NotNil(t, override500.Match)
assert.Equal(t, errors.TLE, override500.Match.Flag)
assert.Equal(t, "timeout.*error", override500.Match.MessagePattern)
assert.Equal(t, "error.type", override500.Match.BodyField)
assert.Equal(t, "timeout", override500.Match.BodyValue)

assert.Equal(t, 503, override500.Response.StatusCode)
assert.Equal(t, "{\"error\": \"Certificate expired\", \"retry_after\": 30}", override500.Response.Body)
assert.Equal(t, "Custom timeout error message", override500.Response.Message)
assert.Equal(t, "error_template.html", override500.Response.Template)
require.Len(t, override500.Response.Headers, 3)
assert.Equal(t, "TIMEOUT", override500.Response.Headers["X-Custom-Error"])
assert.Equal(t, "30", override500.Response.Headers["Retry-After"])
assert.Equal(t, "application/json", override500.Response.Headers["Content-Type"])

require.Len(t, api.ErrorOverrides["401"], 1)
override401 := api.ErrorOverrides["401"][0]

require.NotNil(t, override401.Match)
assert.Equal(t, errors.TLI, override401.Match.Flag)
assert.Equal(t, "", override401.Match.MessagePattern)
assert.Equal(t, "error.code", override401.Match.BodyField)
assert.Equal(t, "INVALID_TOKEN", override401.Match.BodyValue)

assert.Equal(t, 401, override401.Response.StatusCode)
assert.Equal(t, "{\"error\": \"Certificate invalid\"}", override401.Response.Body)
assert.Equal(t, "Token validation failed", override401.Response.Message)
assert.Equal(t, "", override401.Response.Template)
require.Len(t, override401.Response.Headers, 2)
assert.Equal(t, "Bearer", override401.Response.Headers["WWW-Authenticate"])
assert.Equal(t, "true", override401.Response.Headers["X-Auth-Error"])
})

t.Run("multiple overrides for same status code", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API Multiple Overrides",
"api_id": "test-api-multiple-overrides",
"org_id": "default",
"use_keyless": true,
"error_overrides_disabled": true,
"error_overrides": {
"400": [
{
"match": {
"body_field": "error.type",
"body_value": "validation"
},
"response": {
"status_code": 422,
"body": "{\"error\": \"Validation failed\"}"
}
},
{
"match": {
"body_field": "error.type",
"body_value": "malformed"
},
"response": {
"status_code": 400,
"body": "{\"error\": \"Malformed request\"}"
}
}
]
}
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

require.NotNil(t, api.ErrorOverrides)
require.True(t, api.ErrorOverridesDisabled)
require.Len(t, api.ErrorOverrides["400"], 2)

assert.Equal(t, "validation", api.ErrorOverrides["400"][0].Match.BodyValue)
assert.Equal(t, 422, api.ErrorOverrides["400"][0].Response.StatusCode)

assert.Equal(t, "malformed", api.ErrorOverrides["400"][1].Match.BodyValue)
assert.Equal(t, 400, api.ErrorOverrides["400"][1].Response.StatusCode)
})

t.Run("override without match criteria", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API No Match Override",
"api_id": "test-api-no-match-override",
"org_id": "default",
"use_keyless": true,
"error_overrides_disabled": false,
"error_overrides": {
"502": [
{
"response": {
"status_code": 503,
"body": "{\"error\": \"Service unavailable\"}",
"message": "Upstream service is down",
"template": "maintenance.html",
"headers": {
"X-Maintenance": "true"
}
}
}
]
}
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

require.NotNil(t, api.ErrorOverrides)
require.False(t, api.ErrorOverridesDisabled)
require.Len(t, api.ErrorOverrides["502"], 1)

override := api.ErrorOverrides["502"][0]
assert.Nil(t, override.Match)
assert.Equal(t, 503, override.Response.StatusCode)
assert.Equal(t, "{\"error\": \"Service unavailable\"}", override.Response.Body)
assert.Equal(t, "Upstream service is down", override.Response.Message)
assert.Equal(t, "maintenance.html", override.Response.Template)
assert.Equal(t, "true", override.Response.Headers["X-Maintenance"])
})

t.Run("empty error overrides", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API Empty Overrides",
"api_id": "test-api-empty-overrides",
"org_id": "default",
"use_keyless": true,
"error_overrides": {}
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

require.NotNil(t, api.ErrorOverrides)
require.False(t, api.ErrorOverridesDisabled)
assert.Len(t, api.ErrorOverrides, 0)
})

t.Run("nil error overrides", func(t *testing.T) {
jsonData := []byte(`{
"name": "Test API Nil Overrides",
"api_id": "test-api-nil-overrides",
"org_id": "default",
"use_keyless": true
}`)

var api APIDefinition
err := json.Unmarshal(jsonData, &api)
require.NoError(t, err)

assert.Nil(t, api.ErrorOverrides)
require.False(t, api.ErrorOverridesDisabled)
})
}

func TestAPIDefinition_IsBaseAPIWithVersioning(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading