Skip to content

Improve Scalar result coercion spec compliance #5306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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 lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class << self
autoload :AnalysisError, "graphql/analysis_error"
autoload :CoercionError, "graphql/coercion_error"
autoload :InvalidNameError, "graphql/invalid_name_error"
autoload :ScalarCoercionError, "graphql/scalar_coercion_error"
autoload :IntegerDecodingError, "graphql/integer_decoding_error"
autoload :IntegerEncodingError, "graphql/integer_encoding_error"
autoload :StringEncodingError, "graphql/string_encoding_error"
Expand Down
2 changes: 2 additions & 0 deletions lib/graphql/execution/interpreter/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,8 @@ def continue_field(value, owner_type, field, current_type, ast_node, next_select
when "SCALAR", "ENUM"
r = begin
current_type.coerce_result(value, context)
rescue GraphQL::ExecutionError => ex_err
return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result)
Comment on lines +678 to +679
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any possible issues here? No tests break but this will change error messages, though it will improve and make them more spec compliant.

rescue StandardError => err
query.handle_or_reraise(err)
end
Expand Down
25 changes: 1 addition & 24 deletions lib/graphql/integer_encoding_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,6 @@ module GraphQL
# - `GraphQL::Types::BigInt` for really big integer values
#
# @see GraphQL::Types::Int which raises this error
class IntegerEncodingError < GraphQL::RuntimeTypeError
# The value which couldn't be encoded
attr_reader :integer_value

# @return [GraphQL::Schema::Field] The field that returned a too-big integer
attr_reader :field

# @return [Array<String, Integer>] Where the field appeared in the GraphQL response
attr_reader :path

def initialize(value, context:)
@integer_value = value
@field = context[:current_field]
@path = context[:current_path]
message = "Integer out of bounds: #{value}".dup
if @path
message << " @ #{@path.join(".")}"
end
if @field
message << " (#{@field.path})"
end
message << ". Consider using ID or GraphQL::Types::BigInt instead."
super(message)
end
class IntegerEncodingError < GraphQL::ScalarCoercionError
end
end
23 changes: 23 additions & 0 deletions lib/graphql/scalar_coercion_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module GraphQL
# This error is raised when a scalar type cannot coerce a value to its expected type. It is considered legacy because it's raised as a RuntimeTypeError from `Schema.type_error` when `Schema.spec_compliant_scalar_coercion_errors` is not enabled.
class ScalarCoercionError < GraphQL::RuntimeTypeError
# The value which couldn't be coerced
attr_reader :value

# @return [GraphQL::Schema::Field] The field that returned a type error
attr_reader :field

# @return [Array<String, Integer>] Where the field appeared in the GraphQL response
attr_reader :path

def initialize(message, value:, context:)
@value = value
@field = context[:current_field]
@path = context[:current_path]

super(message)
end
end
end
29 changes: 27 additions & 2 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,18 @@ def error_bubbling(new_error_bubbling = nil)

attr_writer :error_bubbling

def spec_compliant_scalar_coercion_errors(new_spec_compliant_scalar_coercion_errors = NOT_CONFIGURED)
if NOT_CONFIGURED.equal?(new_spec_compliant_scalar_coercion_errors)
if defined?(@spec_compliant_scalar_coercion_errors)
@spec_compliant_scalar_coercion_errors
else
find_inherited_value(:spec_compliant_scalar_coercion_errors, false)
end
else
@spec_compliant_scalar_coercion_errors = new_spec_compliant_scalar_coercion_errors
end
end

attr_writer :max_depth

def max_depth(new_max_depth = nil, count_introspection_fields: true)
Expand Down Expand Up @@ -1314,10 +1326,23 @@ def type_error(type_error, context)
execution_error.path = context[:current_path]

context.errors << execution_error
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
raise type_error
when GraphQL::ScalarCoercionError
if spec_compliant_scalar_coercion_errors == true
execution_error = GraphQL::CoercionError.new(type_error.message)
raise execution_error
else
warn <<~MSG
Scalar coercion errors will return GraphQL execution errors instead of raising Ruby exceptions in a future version.
To opt into this new behavior, set `Schema.spec_compliant_scalar_coercion_errors = true`.
To keep or customize the current behavior, add custom error handling in `Schema.type_error`.
MSG

raise type_error
end
when GraphQL::IntegerDecodingError
nil
when GraphQL::UnresolvedTypeError
raise type_error
end
end

Expand Down
17 changes: 1 addition & 16 deletions lib/graphql/string_encoding_error.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
# frozen_string_literal: true
module GraphQL
class StringEncodingError < GraphQL::RuntimeTypeError
attr_reader :string, :field, :path
def initialize(str, context:)
@string = str
@field = context[:current_field]
@path = context[:current_path]
message = "String #{str.inspect} was encoded as #{str.encoding}".dup
if @path
message << " @ #{@path.join(".")}"
end
if @field
message << " (#{@field.path})"
end
message << ". GraphQL requires an encoding compatible with UTF-8."
super(message)
end
class StringEncodingError < GraphQL::ScalarCoercionError
end
end
15 changes: 13 additions & 2 deletions lib/graphql/types/float.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ def self.coerce_input(value, _ctx)
value.is_a?(Numeric) ? value.to_f : nil
end

def self.coerce_result(value, _ctx)
value.to_f
def self.coerce_result(value, ctx)
coerced_value = Float(value, exception: false)

if coerced_value.nil? || !coerced_value.finite?
error = GraphQL::ScalarCoercionError.new(
"Float cannot represent non numeric value: #{value.inspect}",
value: value,
context: ctx
)
ctx.schema.type_error(error, ctx)
else
coerced_value
end
end

default_scalar true
Expand Down
11 changes: 8 additions & 3 deletions lib/graphql/types/int.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ def self.coerce_input(value, ctx)
end

def self.coerce_result(value, ctx)
value = value.to_i
if value >= MIN && value <= MAX
value = Integer(value, exception: false)

if value && (value >= MIN && value <= MAX)
value
else
err = GraphQL::IntegerEncodingError.new(value, context: ctx)
err = GraphQL::IntegerEncodingError.new(
"Int cannot represent non 32-bit signed integer value: #{value.inspect}",
value: value,
context: ctx
)
ctx.schema.type_error(err, ctx)
end
end
Expand Down
8 changes: 6 additions & 2 deletions lib/graphql/types/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ def self.coerce_result(value, ctx)
str.encode!(Encoding::UTF_8)
end
rescue EncodingError
err = GraphQL::StringEncodingError.new(str, context: ctx)
ctx.schema.type_error(err, ctx)
error = GraphQL::StringEncodingError.new(
"String cannot represent value: #{value.inspect}",
value: value,
context: ctx
)
ctx.schema.type_error(error, ctx)
end

def self.coerce_input(value, _ctx)
Expand Down
84 changes: 84 additions & 0 deletions spec/graphql/types/float_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,88 @@
assert_nil GraphQL::Types::Float.coerce_isolated_input(enum)
end
end

describe "coerce_result" do
it "coercess ints and floats" do
err_ctx = GraphQL::Query.new(Dummy::Schema, "{ __typename }").context

assert_equal 1.0, GraphQL::Types::Float.coerce_result(1, err_ctx)
assert_equal 1.0, GraphQL::Types::Float.coerce_result("1", err_ctx)
assert_equal 1.0, GraphQL::Types::Float.coerce_result("1.0", err_ctx)
assert_equal 6.1, GraphQL::Types::Float.coerce_result(6.1, err_ctx)
end

it "rejects other types" do
err_ctx = GraphQL::Query.new(Dummy::Schema, "{ __typename }").context

assert_raises(GraphQL::ScalarCoercionError) do
GraphQL::Types::Float.coerce_result("foo", err_ctx)
end

assert_raises(GraphQL::ScalarCoercionError) do
GraphQL::Types::Float.coerce_result(1.0 / 0, err_ctx)
end
end

describe "with Schema.spec_compliant_scalar_coercion_errors" do
class FloatScalarSchema < GraphQL::Schema
class Query < GraphQL::Schema::Object
field :float, GraphQL::Types::Float do
argument :value, GraphQL::Types::Float
end

field :bad_float, GraphQL::Types::Float

def float(value:)
value
end

def bad_float
Float::INFINITY
end
end

query(Query)
end

class FloatSpecCompliantErrors < FloatScalarSchema
spec_compliant_scalar_coercion_errors true
end

class FloatNonSpecComplaintErrors < FloatScalarSchema
spec_compliant_scalar_coercion_errors false
end

it "returns GraphQL execution errors with spec_compliant_scalar_coercion_errors enabled" do
query = "{ badFloat }"
result = FloatSpecCompliantErrors.execute(query)

assert_equal(
{
"errors" => [
{
"message" => "Float cannot represent non numeric value: Infinity",
"locations" => [{ "line" => 1, "column" => 3 }],
"path" => ["badFloat"],
},
],
"data" => {
"badFloat" => nil,
}
},
result.to_h
)
end

it "raises Ruby exceptions with spec_compliant_scalar_coercion_errors disabled" do
query = "{ badFloat }"

error = assert_raises(GraphQL::ScalarCoercionError) do
FloatNonSpecComplaintErrors.execute(query)
end

assert_equal("Float cannot represent non numeric value: Infinity", error.message)
end
end
end
end
70 changes: 63 additions & 7 deletions spec/graphql/types/int_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,77 @@
assert_equal(-(2**31), GraphQL::Types::Int.coerce_result(-(2**31), context))
end

it "replaces values, if configured to do so" do
assert_equal Dummy::Schema::MAGIC_INT_COERCE_VALUE, GraphQL::Types::Int.coerce_result(99**99, context)
end
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to be removed? It seems like the override in the example schema could be retained, but a method call would have to be changed.... or is this behavior not possible anymore?

(Maybe the old override could be retained as-is if an alias accessor was added to the new IntegerEncodingError)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it doesn't have to be I don't think. I'll check


it "raises on values out of bounds" do
err_ctx = GraphQL::Query.new(Dummy::Schema, "{ __typename }").context
assert_raises(GraphQL::IntegerEncodingError) { GraphQL::Types::Int.coerce_result(2**31, err_ctx) }
err = assert_raises(GraphQL::IntegerEncodingError) { GraphQL::Types::Int.coerce_result(-(2**31 + 1), err_ctx) }
assert_equal "Integer out of bounds: -2147483649. Consider using ID or GraphQL::Types::BigInt instead.", err.message
assert_equal "Int cannot represent non 32-bit signed integer value: -2147483649", err.message

err = assert_raises GraphQL::IntegerEncodingError do
Dummy::Schema.execute("{ hugeInteger }")
end
expected_err = "Integer out of bounds: 2147483648 @ hugeInteger (Query.hugeInteger). Consider using ID or GraphQL::Types::BigInt instead."
assert_equal expected_err, err.message
assert_equal "Int cannot represent non 32-bit signed integer value: 2147483648", err.message
end

describe "with Schema.spec_compliant_scalar_coercion_errors" do
class IntScalarSchema < GraphQL::Schema
class Query < GraphQL::Schema::Object
field :int, GraphQL::Types::Int do
argument :value, GraphQL::Types::Int
end

field :bad_int, GraphQL::Types::Int

def int(value:)
value
end

def bad_int
2**31 # Out of range
end
end

query(Query)
end

class IntSpecCompliantErrors < IntScalarSchema
spec_compliant_scalar_coercion_errors true
end

class IntNonSpecComplaintErrors < IntScalarSchema
spec_compliant_scalar_coercion_errors false
end

it "returns GraphQL execution errors with spec_compliant_scalar_coercion_errors enabled" do
query = "{ badInt }"
result = IntSpecCompliantErrors.execute(query)

assert_equal(
{
"errors" => [
{
"message" => "Int cannot represent non 32-bit signed integer value: 2147483648",
"locations" => [{ "line" => 1, "column" => 3 }],
"path" => ["badInt"],
},
],
"data" => {
"badInt" => nil,
}
},
result.to_h
)
end

it "raises Ruby exceptions with spec_compliant_scalar_coercion_errors disabled" do
query = "{ badInt }"

error = assert_raises(GraphQL::IntegerEncodingError) do
IntNonSpecComplaintErrors.execute(query)
end

assert_equal("Int cannot represent non 32-bit signed integer value: 2147483648", error.message)
end
end
end
end
Expand Down
Loading