Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs-website/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
]
},
"router/relaxed-field-selection-merging-nullability",
"router/string-literals-for-enums",
Comment thread
devsergiy marked this conversation as resolved.
"router/file-upload",
"router/access-logs",
"router/profiling",
Expand Down
2 changes: 2 additions & 0 deletions docs-website/router/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,7 @@ Configure the GraphQL Execution Engine of the Router.
| ENGINE_SUBSCRIPTION_FETCH_TIMEOUT | subscription_fetch_timeout | <Icon icon="square" /> | The maximum time a subscription fetch can take before it is considered timed out. | 30s |
| ENGINE_ENABLE_DEFER | enable_defer | <Icon icon="square" /> | Enables support for the `@defer` directive, allowing clients to defer parts of a query so that the initial response is returned faster and deferred fields are streamed incrementally. | false |
| ENGINE_RELAX_SUBGRAPH_OPERATION_FIELD_SELECTION_MERGING_NULLABILITY | relax_subgraph_operation_field_selection_merging_nullability | <Icon icon="square" /> | Relaxes nullability validation for [field selection merging](/router/relaxed-field-selection-merging-nullability) across union member types. | false |
| ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS | allow_string_literals_for_enums | <Icon icon="square" /> | Accepts a [string literal where an enum value is expected](/router/string-literals-for-enums) if the string content matches one of the enum's values. This is a deviation from the GraphQL specification. | false |
Comment thread
devsergiy marked this conversation as resolved.

<Warning>
**Subject to change.** `enable_net_poll`, `websocket_server_poll_timeout`, and `websocket_server_conn_buffer_size` may be removed or changed in a future release without a standard deprecation cycle. Avoid depending on them.
Expand Down Expand Up @@ -2098,6 +2099,7 @@ engine:
enable_subgraph_fetch_operation_name: true
subscription_fetch_timeout: 30s
relax_subgraph_operation_field_selection_merging_nullability: false
allow_string_literals_for_enums: false
```

### Debug Configuration
Expand Down
71 changes: 71 additions & 0 deletions docs-website/router/string-literals-for-enums.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: "String Literals for Enums"
icon: "quotes"
description: "Accept string literals for enum values as an opt-in deviation from the GraphQL specification."
---

The GraphQL specification distinguishes between enum literals and string literals in an operation document. An enum argument accepts the enum literal `VALUE1`, but not the string literal `"VALUE1"`. By default, the router rejects a string literal used for an enum type with a validation error:

```
Enum "SomeEnum" cannot represent non-enum value: "VALUE1".
```

The `allow_string_literals_for_enums` option accepts such string literals when the string content matches one of the enum's values. Strings that do not match a value are still rejected:

```
Value "NOPE" does not exist in "SomeEnum" enum.
```

<Warning>
This is a deviation from the GraphQL specification. Servers built on graphql-js reject string literals for enums. Prefer fixing clients to send enum literals. Use this option to keep existing clients working while they migrate.
</Warning>

## Examples

Given the schema:

```graphql
enum SomeEnum {
VALUE1
VALUE2
}

type Query {
field(arg: SomeEnum): String
}
```

The following queries behave as listed:

```graphql
{ field(arg: VALUE1) } # Valid. Enum literal.
{ field(arg: "VALUE1") } # Invalid by default. Valid with this option enabled.
{ field(arg: "NOPE") } # Invalid. "NOPE" is not a value of SomeEnum.
```

Variables are not affected by this option. JSON has no enum type, so a string is the standard representation for an enum variable value and is always accepted when it matches an enum value:

```graphql
query ($arg: SomeEnum) { field(arg: $arg) }
# variables: {"arg": "VALUE1"} is valid with or without this option
```

## Configuration

<Tabs>
<Tab title="Environment Variable">
```bash
ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS=true
```
</Tab>
<Tab title="YAML">
```yaml config.yaml
version: "1"

engine:
allow_string_literals_for_enums: true
```
</Tab>
</Tabs>

This option is disabled by default. See the [Router Configuration reference](/router/configuration#router-engine-configuration) for all engine flags.
70 changes: 70 additions & 0 deletions router-tests/operations/inline_literal_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,76 @@ func TestInlineLiteralValidationWithPersistedOperations(t *testing.T) {
})
}

// TestAllowStringLiteralsForEnums verifies the opt-in spec deviation
// (engine execution config allow_string_literals_for_enums) that accepts a string
// literal where an enum value is expected, as long as the string content matches
// one of the enum's values. Without the flag, the router enforces the strict
// GraphQL spec behavior and rejects such literals at validation.
func TestAllowStringLiteralsForEnums(t *testing.T) {
t.Parallel()

t.Run("disabled by default", func(t *testing.T) {
testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) {
t.Run("rejects string literal for enum argument", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithListOfEnumArg(arg: ["A"]) }`,
})
requireValidationError(t, res.Body, `Enum "EnumType" cannot represent non-enum value: "A".`)
})

t.Run("rejects string literal for enum field of input object", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithInput(arg: {enum: "A"}) }`,
})
requireValidationError(t, res.Body, `Enum "EnumType" cannot represent non-enum value: "A".`)
})
})
})

t.Run("enabled", func(t *testing.T) {
testenv.Run(t, &testenv.Config{
ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) {
cfg.AllowStringLiteralsForEnums = true
},
}, func(t *testing.T, xEnv *testenv.Environment) {
t.Run("accepts string literal matching an enum value for enum argument", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithListOfEnumArg(arg: ["A"]) }`,
})
assert.Equal(t, `{"data":{"rootFieldWithListOfEnumArg":["A"]}}`, res.Body)
})

t.Run("accepts string literal matching an enum value for enum field of input object", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithInput(arg: {enum: "A"}) }`,
})
assert.Equal(t, `{"data":{"rootFieldWithInput":"A"}}`, res.Body)
})

t.Run("rejects string literal not matching an enum value", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithListOfEnumArg(arg: ["NOPE"]) }`,
})
requireValidationError(t, res.Body, `Value "NOPE" does not exist in "EnumType" enum.`)
})

t.Run("still accepts inline enum literal for enum argument", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { rootFieldWithListOfEnumArg(arg: [A]) }`,
})
assert.Equal(t, `{"data":{"rootFieldWithListOfEnumArg":["A"]}}`, res.Body)
})

t.Run("still rejects string literal for a non-enum argument mismatch", func(t *testing.T) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { floatField(arg: "1.5") }`,
})
requireValidationError(t, res.Body, `Float cannot represent non numeric value: "1.5"`)
})
})
})
}

// requireValidationError asserts the GraphQL response body carries exactly one
// error with the given message and no data payload.
//
Expand Down
9 changes: 5 additions & 4 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1599,10 +1599,11 @@ func (s *graphServer) buildGraphMux(
ApolloRouterCompatibilityFlags: s.apolloRouterCompatibilityFlags,
DisableExposingVariablesContentOnValidationError: s.engineExecutionConfiguration.DisableExposingVariablesContentOnValidationError,
RelaxSubgraphOperationFieldSelectionMergingNullability: s.engineExecutionConfiguration.RelaxSubgraphOperationFieldSelectionMergingNullability,
EnableDefer: s.engineExecutionConfiguration.EnableDefer,
ComplexityLimits: s.securityConfiguration.ComplexityLimits,
CostControl: s.securityConfiguration.CostControl,
ValidateInlineArguments: s.engineExecutionConfiguration.ValidateInlineArguments,
AllowStringLiteralsForEnums: s.engineExecutionConfiguration.AllowStringLiteralsForEnums,
EnableDefer: s.engineExecutionConfiguration.EnableDefer,
ComplexityLimits: s.securityConfiguration.ComplexityLimits,
CostControl: s.securityConfiguration.CostControl,
ValidateInlineArguments: s.engineExecutionConfiguration.ValidateInlineArguments,
})

if opts.ReloadPersistentState.inMemoryPlanCacheFallback.IsEnabled() {
Expand Down
6 changes: 6 additions & 0 deletions router/core/operation_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type OperationProcessorOptions struct {
ApolloRouterCompatibilityFlags config.ApolloRouterCompatibilityFlags
DisableExposingVariablesContentOnValidationError bool
RelaxSubgraphOperationFieldSelectionMergingNullability bool
AllowStringLiteralsForEnums bool
ComplexityLimits *config.ComplexityLimits
CostControl *config.CostControl
ParserTokenizerLimits astparser.TokenizerLimits
Expand Down Expand Up @@ -1563,6 +1564,7 @@ type parseKitOptions struct {
apolloRouterCompatibilityFlags config.ApolloRouterCompatibilityFlags
disableExposingVariablesContentOnValidationError bool
relaxSubgraphOperationFieldSelectionMergingNullability bool
allowStringLiteralsForEnums bool
enableDefer bool
validateInlineArguments config.ValidateInlineArguments
}
Expand Down Expand Up @@ -1638,6 +1640,9 @@ func createOperationValidator(options *parseKitOptions) *astvalidation.Operation
if options.relaxSubgraphOperationFieldSelectionMergingNullability {
opts = append(opts, astvalidation.WithRelaxFieldSelectionMergingNullability())
}
if options.allowStringLiteralsForEnums {
opts = append(opts, astvalidation.WithAllowStringLiteralsForEnums())
}
return astvalidation.DefaultOperationValidator(opts...)
}

Expand All @@ -1662,6 +1667,7 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor {
apolloRouterCompatibilityFlags: opts.ApolloRouterCompatibilityFlags,
disableExposingVariablesContentOnValidationError: opts.DisableExposingVariablesContentOnValidationError,
relaxSubgraphOperationFieldSelectionMergingNullability: opts.RelaxSubgraphOperationFieldSelectionMergingNullability,
allowStringLiteralsForEnums: opts.AllowStringLiteralsForEnums,
validateInlineArguments: opts.ValidateInlineArguments,
},
}
Expand Down
2 changes: 2 additions & 0 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ type EngineExecutionConfiguration struct {

RelaxSubgraphOperationFieldSelectionMergingNullability bool `envDefault:"false" env:"ENGINE_RELAX_SUBGRAPH_OPERATION_FIELD_SELECTION_MERGING_NULLABILITY" yaml:"relax_subgraph_operation_field_selection_merging_nullability"`

AllowStringLiteralsForEnums bool `envDefault:"false" env:"ENGINE_ALLOW_STRING_LITERALS_FOR_ENUMS" yaml:"allow_string_literals_for_enums"`

ValidateInlineArguments ValidateInlineArguments `yaml:"validate_inline_arguments" envPrefix:"ENGINE_VALIDATE_INLINE_ARGUMENTS_"`
}

Expand Down
5 changes: 5 additions & 0 deletions router/pkg/config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4093,6 +4093,11 @@
"type": "boolean",
"default": false,
"description": "Relaxes nullability validation for field selection merging when enclosing types are non-overlapping concrete object types. When enabled, fields with differing nullability (e.g. String! vs String) in inline fragments on different union member types will not cause validation errors."
},
"allow_string_literals_for_enums": {
"type": "boolean",
"default": false,
"description": "Enables a deliberate deviation from the GraphQL specification that accepts a string literal where an enum value is expected, as long as the string content matches one of the enum's values (e.g. f(arg: \"VALUE1\") for enum SomeEnum { VALUE1 }). When disabled, only enum literals are valid inline values for enum types."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@
"WebSocketClientAckTimeout": 30000000000,
"ValidateRequiredExternalFields": false,
"RelaxSubgraphOperationFieldSelectionMergingNullability": false,
"AllowStringLiteralsForEnums": false,
"ValidateInlineArguments": {
"Mode": "off",
"EnforceHTTPStatusCode": 400,
Expand Down
1 change: 1 addition & 0 deletions router/pkg/config/testdata/config_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@
"WebSocketClientAckTimeout": 30000000000,
"ValidateRequiredExternalFields": false,
"RelaxSubgraphOperationFieldSelectionMergingNullability": false,
"AllowStringLiteralsForEnums": false,
"ValidateInlineArguments": {
"Mode": "off",
"EnforceHTTPStatusCode": 400,
Expand Down
Loading