Skip to content

FieldExtension#after_resolve returns nil for falsy field values (regression in 2.5.24) #5609

@fliebe92

Description

@fliebe92

Describe the bug

FieldExtension#after_resolve uses || to choose between value and values, which treats falsy-but-valid return values (e.g. false) as absent. When a mutation field is declared null: false and resolves to false, the extension returns nil instead, triggering null propagation.

Versions

graphql version: 2.5.24 (regression from 2.5.23)
rails (or other framework): 8.0.5

GraphQL schema

class MyFieldExtension < GraphQL::Schema::FieldExtension
  def resolve(object:, arguments:, context:, **_rest)
    yield(object, arguments)
  end
  # after_resolve NOT overridden — inherits base class default
end

class BaseMutation < GraphQL::Schema::Mutation
  field_class BaseField  # BaseField pushes MyFieldExtension onto every field
end

class Mutations::DoSomething < BaseMutation
  field :success, Boolean, null: false

  def resolve
    { success: false }  # error path
  end
end

GraphQL query

mutation {
  doSomething {
    success
  }
}

Expected response:

{ "data": { "doSomething": { "success": false } } }

Actual response (2.5.24):

{ "data": null, "errors": [{ "message": "Cannot return null for non-nullable field DoSomethingPayload.success" }] }

Steps to reproduce

  • Create a mutation with a Boolean, null: false field named success.
  • Attach any FieldExtension subclass that does not override after_resolve (inheriting the base class default).
  • Execute the mutation through a code path that returns { success: false }.
  • Observe the null propagation error. The success: true path is unaffected.

Expected behavior

false is a valid non-null Boolean. A field extension that does not override after_resolve should pass the resolved value through unchanged, as it did in 2.5.23.

Actual behavior

The default after_resolve in 2.5.24 changed from returning value to returning value || values. When value is false, false || nil evaluates to nil, which violates the null: false constraint and nullifies the parent object.

The regression is in this diff between 2.5.23 and 2.5.24:

# 2.5.23

def after_resolve(object:, arguments:, context:, value:, memo:)
  value
end

# 2.5.24

def after_resolve(object: nil, objects: nil, arguments:, context:, values: nil, value: nil, memo:)
  value || values  # ← breaks when value is false
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions