Skip to content

Input: Cached type in registry is not the type returned by type mapper #531

Closed

Description

@NormySan

Hey,

I'm having a weird issue with input types where I get the exception in the title "Cached type in the registry is not the type returned by type mapper".

For some reason, the type stored in the cache is not the same as the one from the type mapper even if the input class they both reference is the same.

The mutation that I try to run only fails when I use variables to run the mutation, If I run the mutation without variables and provide the values directly it all works but this is not possible to do in the frontend since everything is automatically generated and requires the type to be provided.

Failing mutation

mutation CreateFacility($input: FacilityInput!) {
  createFacility(input: $input) {
    id
  }
}

Working mutation

mutation CreateFacility {
  createFacility(input: {
    name: "Facility"
    subdomain: "facility"
  }) {
    id
  }
}

Input type

#[Input]
class FacilityInput
{
    #[Field]
    public string $name;

    #[Field]
    public string $subdomain;
}

Controller/Resolver

class FacilityResolver
{
    public function __construct(
        private readonly CommandBus $commandBus,
    ) {
    }

    #[Mutation]
    #[Logged]
    public function createFacility(
        #[InjectUser] Employee $employee,
        FacilityInput $input
    ): Facility {
        /** @var Facility $facility */
        $facility = $this->commandBus->dispatch(new CreateFacilityCommand(
            (string) $employee->organizationId,
            $input->name,
            $input->subdomain,
        ));

        return $facility;
    }
}

The cached type

object(TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType)[782]
  public 'name' => string 'FacilityInput' (length=13)
  public 'description' => null
  public 'astNode' => null
  public 'config' => 
    array (size=3)
      'name' => string 'FacilityInput' (length=13)
      'fields' => 
        object(Closure)[775]
          virtual 'closure' 'TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType::TheCodingMachine\GraphQLite\Types\{closure}'
          public 'static' => 
            array (size=5)
              ...
      'interfaces' => 
        object(Closure)[842]
          virtual 'closure' 'TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType::TheCodingMachine\GraphQLite\Types\{closure}'
          public 'static' => 
            array (size=3)
              ...
  public 'extensionASTNodes' => 
    array (size=0)
      empty
  private 'fields' (GraphQL\Type\Definition\TypeWithFields) => null
  public 'resolveFieldFn' => null
  private 'interfaces' (GraphQL\Type\Definition\ObjectType) => null
  private 'interfaceMap' (GraphQL\Type\Definition\ObjectType) => null
  private ?string 'status' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'frozen' (length=6)
  private array 'fieldsCallables' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => 
    array (size=0)
      empty
  private ?array 'fields' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => null
  private ?string 'className' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'App\API\Dashboard\Input\FacilityInput' (length=37)

The type from the type mapper

object(TheCodingMachine\GraphQLite\Types\InputType)[1057]
  public 'name' => string 'FacilityInput' (length=13)
  public 'description' => null
  public 'astNode' => null
  public 'config' => 
    array (size=3)
      'name' => string 'FacilityInput' (length=13)
      'description' => null
      'fields' => 
        object(Closure)[1063]
          virtual 'closure' '$this->TheCodingMachine\GraphQLite\Types\{closure}'
          public 'static' => 
            array (size=4)
              ...
          public 'this' => 
            &object(TheCodingMachine\GraphQLite\Types\InputType)[1057]
  public 'extensionASTNodes' => null
  private 'fields' (GraphQL\Type\Definition\InputObjectType) => null
  protected string 'status' => string 'pending' (length=7)
  protected array 'fieldsCallables' => 
    array (size=0)
      empty
  protected ?array 'finalFields' => null
  private array 'constructorInputFields' => 
    array (size=0)
      empty
  private array 'inputFields' => 
    array (size=0)
      empty
  private string 'className' => string 'App\API\Dashboard\Input\FacilityInput' (length=37)
  private ?TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface 'inputTypeValidator' => null

Activity

  1. oojacoboo commented on Nov 26, 2022

    @oojacoboo
    Collaborator

    @NormySan what cache adapter are you using? I'm not familiar with this error. It looks like you're doing things correctly.

  2. NormySan commented on Nov 26, 2022

    @NormySan
    Author

    @oojacoboo I'm using a cache pool from Symfony. Only inputs causing this problem so far, all other types seems to be working properly.

  3. oojacoboo commented on Nov 26, 2022

    @oojacoboo
    Collaborator

    From the cached type:

    private ?string 'className' (TheCodingMachine\GraphQLite\Types\MutableObjectType) => string 'App\API\Dashboard\Input\FacilityInput' (length=37)

    From the type mapper:

    private string 'className' => string 'App\API\Dashboard\Input\FacilityInput' (length=37)

    What's the stack-trace look like? How is the comparison being made?

  4. NormySan commented on Nov 27, 2022

    @NormySan
    Author

    @oojacoboo This is the stack trace:

    https://gist.github.com/NormySan/d505993a1817843f7c23aa2f5a39acc0

    The exception is thrown here in the recursive type mapper:

    throw new RuntimeException('Cached type in registry is not the type returned by type mapper.');

  5. oojacoboo commented on Nov 28, 2022

    @oojacoboo
    Collaborator

    @NormySan do you use this same input type in multiple mutations? Can you confirm that the cached type and the type from the type mapper are effectively the same object? I think the issue here is that it's using !== instead of !=. Can you test with != and see if that resolves the issue for you?

  6. NormySan commented on Nov 28, 2022

    @NormySan
    Author

    @oojacoboo Yeah, I'm using the input type in multiple mutations, one is for updating and one is for creating. I tried changing the comparison to just using != and I'm getting the same exception.

  7. oojacoboo commented on Nov 28, 2022

    @oojacoboo
    Collaborator

    @NormySan if you're certain in your testing that it was changed and executed (not cached), and that comparison is failing, can you identify what's different about the two objects and what's causing it to fail that comparison?

  8. NormySan commented on Nov 28, 2022

    @NormySan
    Author

    @oojacoboo I think it's failing because they are entirely different objects, I'm going to set up a basic reproduction of my codebase, maybe that will help with identifying the underlying issue.

  9. NormySan commented on Nov 28, 2022

    @NormySan
    Author

    Here is a simple reproduction of the exception in a simple Symfony application.

    https://github.com/NormySan/graphqlite-input-error

    It does not use the Symfony bundle but instead uses a custom implementation. The GraphQL request is handled by the App\GraphQL\RequestHandler class, this class is created through the App\GraphQL\RequestHandlerFactory.

  10. NormySan commented on Nov 28, 2022

    @NormySan
    Author

    So far I've narrowed it down to the following:

    1. An attempt is made to load the FacilityInput type.
    2. All type mappers are called where it ends up in the RecursiveTypeMapper in the mapNameToType method.
    3. This method then calls the mapNameToType method on the type mapper.
    4. This results in a TypeAnnotatedObjectType being returned and registered in the type registry.
    5. Later on a call is made to the RecursiveTypeMapper to the mapClassToInputType method to get the input type.
    6. This results in an InputType object being created instead by the type mapper which is not the same type of object that was cached earlier on.
    7. Because the objects are not the same it crashes with the exception "Cached type in the registry is not the type returned by type mapper".

    I'm not sure what the expected type in the cache should be but I'm guessing that it should be the InputType object and not the TypeAnnotatedObjectType object?

  11. oojacoboo commented on Nov 29, 2022

    @oojacoboo
    Collaborator

    @NormySan so, I think the issue here is that you're using the same Input type for multiple operations. I'm not going to assume this is an incorrect design decision on your part, although it very well could be. It's mostly recommended to have a unique Input type per operation. I know this is how our API is designed and likely the reason we haven't run into this error - same for most others.

    That said, looking at the code, it seems clear that it's doing a direct comparison of a cached version of the Input type and the one currently being processed by the type mapper.

    I don't have enough insight or feedback on the overall use of this. But, it seems like a fresh look at this caching logic needs to be had.

    • What is cached and how is that used?
    • How much value are we getting out of the cache?
    • If the cache can't be used, do we need to throw an Exception, or can the non-cached version be returned?
    • What possible performance implications might be had from the design decision?

    I don't have any answers to these questions, but it seems this is the path forward. Then we can assess a refactor of the design and PR.

  12. NormySan commented on Nov 29, 2022

    @NormySan
    Author

    Tried using two different input types and I'm still getting the same error so must be something else. I did try to set up a very minimal test based on the no-framework instructions and having the same name for the input type worked well in this case so it must be something else in Symfony or my setup causing the problem.

    If I can locate the underlying cause and if it's in this package I will try to make a PR.

  13. pvlg commented on Nov 29, 2022

    @pvlg
    Contributor

    Similar issue in EnumTypeMapper.php. Need to add $enumClass = ltrim($enumClass, '\\'); in mapByClassName(). Because of the slash does not find the class in the cache.

  14. Lappihuan commented on Nov 29, 2022

    @Lappihuan
    Contributor

    @NormySan did you try ommiting the Input suffix on the PHP Class?
    Its automatically added and i do remember having issues if i added it by accident.

  15. NormySan commented on Nov 29, 2022

    @NormySan
    Author

    @Lappihuan That solved it, I would really like my inputs to be named like that though since it creates a lot of ambiguity if the input is named the same as the entities. Going to have a look at what part of the library that might be causing that issue.

  16. 14 remaining items

  17. oojacoboo commented on Dec 18, 2022

    @oojacoboo
    Collaborator

    #532 has been merged. Please confirm that this issue is now resolved.

  18. apuckey commented on Jun 15, 2026

    @apuckey

    apologies for resurrecting an old thread, but i am seeing this on the latest version, only on the very first request, subsequent requests work, i have tried using both filesystem adapter and cache adapter from synfony ( swoole application thats long lived )

    when i print this to the screen from RecursiveTypeMapper::mapClassToType where this exception is thrown 'Cached type in registry is not the type returned by type mapper.':

    var_dump($cachedType->name() . ' -> ' . $type->name());
    var_dump(get_class($cachedType) . ' -> ' . get_class($type));
    var_dump(spl_object_id($cachedType) . ' -> ' . spl_object_id($type));
    

    i get this result:

    string(14) "Order -> Order"
    string(118) "TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType -> TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType"
    string(12) "7154 -> 7263"
    

    I have zero #[Inputs]

    Every time a new version of the application starts, the first request always fails.

  19. oojacoboo commented on Jun 15, 2026

    @oojacoboo
    Collaborator

    @apuckey that's strange - so it's the same object, but it's creating two of them in memory on first load. Presumably it's not an issue b/c on subsequent loads, the cache is picked up from whichever one is stored.

  20. apuckey commented on Jun 15, 2026

    @apuckey

    Yes it works fine after the first request, but the first request is always an error.

    I'll trace it when i have more time later in the day and let you know my findings :)

  21. apuckey commented on Jun 16, 2026

    @apuckey

    apologies this might of been incorrect information. I am just debugging exactly whats going on now.

  22. oojacoboo commented on Jun 16, 2026

    @oojacoboo
    Collaborator

    @apuckey okay, let me know, if you could, in the PR. Thanks

  23. apuckey commented on Jun 16, 2026

    @apuckey

    @oojacoboo Good afternoon,

    I think this may be cause by a race condition outside of this library so i apologise for that.

    What i was seeing was from local development of a report generator in strict mode so it obviously did 2 introspection queries instantly, these 2 coroutines, in the process of generating types, do file io so swoole would automatically yield to the other coroutine that was also generating the types at the same time.

    So in my library now when I am generating the schema on first request I am creating a lock so it only does it once.

    Instead of just returning the Schema I am forcing it to build the typemap at the same time the schema gets created.

    	protected Lock $lock;
    
    	public function __construct(protected ContainerInterface $container)
    	{
    		$this->config = $container->get(ConfigInterface::class)->get('graphql', []);
    		$this->lock = new Lock();
    	}
    
    	public function get(string $server = 'webhttp'): Schema
    	{
    		if (isset($this->schemas[$server]) && $this->schemas[$server] instanceof Schema) {
    			return $this->schemas[$server];
    		}
    
    		$this->lock->lock();
    		defer(function () {
    			$this->lock->unlock();
    		});
    
    		// Simple re-check as another coroutine might of just created it due to locking and concurrent requests
    		if (isset($this->schemas[$server]) && $this->schemas[$server] instanceof Schema) {
    			return $this->schemas[$server];
    		}
    ...
    
    		$schema = $factory->createSchema();
    
    		$schema->getTypeMap();
    
    		return $schema;
    

    This seems to have solved both issues so this may not actually be required. I will do some more testing now that I am locking the schema / type creation

  24. oojacoboo commented on Jun 16, 2026

    @oojacoboo
    Collaborator

    @apuckey yes, it's a race for sure. But the cache should be the source of truth. If, at the point of type creation, the cache is checked, it should prevent duplicate types. That's what the PR was attempting to resolve. Did you test it with this in mind?

  25. apuckey commented on Jun 16, 2026

    @apuckey

    I have tested using this PR and without after locking the schema type creation and it works as expected in both scenarios.

    If I lock the schema generation including getting the typemap as above it will block any other requests whilst its creating Cache on the first request, Or I can avoid locks altogether and hook into my OnWorkerStart event to generate it when the worker starts before any requests are served.

    Of course using fpm and getting 2 requests at the same time and using file cache might be a bit different, however I think it will still work and the second one should overwrite the cache as the code is not shared between requests unlike swoole.

    So I think ultimately it was my fault for not locking and generating the typemap as there must be I/O operations somewhere in the typemap generation that was causing the context switching between coroutines for me.

    Maybe its just a documentation thing that we can add to schema creation that in a concurrent environment we need to block whilst it generates the typemap to not cause any issues.

  26. oojacoboo commented on Jun 17, 2026

    @oojacoboo
    Collaborator

    @apuckey thanks. I've updated the documentation around this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions