You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Description
I'd like to enable only the graphql endpoint, and completely disable REST. Leaving it open is a potential security issue, as it means maintaining two different protocols.
#2796 raised this problem, but it was closed years ago, so I'm opening a new issue. It describes overriding ApiLoader, which still works in version 4.3.3 (just return new RouteCollection(); in load()) but means patching code in vendor.
I'm happy to send a PR if I get a guideline of the implementation.
Hi @brunobg, you don't actually need a new flag for this — the routing system already lets you opt into just the GraphQL endpoints. Here's how.
The api_platform route type (the ApiLoader) is what generates the REST routes from your resource metadata. If you don't import that loader in your routing, no REST routes will be registered. The GraphQL routes live in standalone files that you can import directly:
# config/routes.yaml# Do NOT include this — it's what creates REST routes:# api_platform:# resource: .# type: api_platformapi_platform_graphql:
resource: '@ApiPlatformBundle/Resources/config/routing/graphql/graphql.php'# Optional, only if you want the GraphiQL playground:api_platform_graphiql:
resource: '@ApiPlatformBundle/Resources/config/routing/graphql/graphiql.php'
Then declare your resources with graphQlOperations only:
#[ApiResource(
graphQlOperations: [
newQuery(),
newQueryCollection(),
newMutation(name: 'create'),
newMutation(name: 'update'),
newDeleteMutation(name: 'delete'),
],
)]
class Book {}
If you'd rather keep #[ApiResource] minimal but ensure no REST operations are generated by default for any resource, you can also override the defaults:
api_platform:
defaults:
operations: [] # no default HTTP operations
This combination (no api_platform route loader + GraphQL-only operations) gives you a strictly GraphQL API with zero REST surface area. No vendor patching, no ApiLoader override needed.
Closing this — feel free to reopen if you hit a case this doesn't cover.
/edit: generated by claude, supervised and reviewed by myself. Do you think we should add an entry to the documentation?
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"status": 500,
"detail": "You must define a \"path\" for the route \"api_platform\" in file \"api/config/routes/api_platform.yaml\" in api/config/routes/api_platform.yaml (which is being imported from \"api/src/Kernel.php\").",
"class": "Symfony\\Component\\Config\\Exception\\LoaderLoadException",
if I fully remove api_platform, I get a different error when fetching graphql queries:
{
"errors": [
{
"message": "Internal server error",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"regions"
],
"extensions": {
"debugMessage": "Unable to generate an IRI for the item of type \"App\\Entity\\Region\"",
"file": "api/vendor/api-platform/symfony/Routing/IriConverter.php",
"line": 205,
Damn this is bad indeed, as we don't declare URIs for the resources, routes don't exist anymore. As we're using IRIs for many things (and also in the graphql subsystem) this isn't ideal... I need to think about this.
It adds a new api_platform_iris route loader type registers only the auto-generated NotExposed operations — no REST controllers, no docs/entrypoint routes — so a graphql-only API works without exposing REST while keeping IRIs (Relay global IDs, subscription keys, nested IRI inputs) functional through api_platform.symfony.iri_converter.
@soyuka sorry to reopen this, but I'm still seeing the same error. I updated all api-platform packages to 4.3.4. Perhaps I'm still missing something?
Here's my app_platform.yaml:
api_platform_iris:
resource: .type: api_platform_irisapi_platform_graphql:
resource: "@ApiPlatformBundle/Resources/config/routing/graphql/graphql.php"# Optional, only if you want the GraphiQL playground:api_platform_graphiql:
resource: "@ApiPlatformBundle/Resources/config/routing/graphql/graphiql.php"
@soyuka sorry for the long silence, and thanks for #7946. I tested it on 4.3.21 with api_platform_iris + the GraphQL routes and operations: [] on every resource: IRI generation works now, our whole GraphQL suite passes, and the router holds nothing but /graphql, GraphiQL and the not-exposed item routes.
One thing left: an error on one of those not-exposed routes is now a 500 instead of a 404. GET /bottles/999999999 goes through the error path, and the JSON-LD error normalizer generates @context from api_jsonld_context, which api_platform_iris doesn't load:
RouteNotFoundException: Unable to generate a URL for the named route "api_jsonld_context" as such route does not exist.
api-platform/symfony/Routing/Router.php:95
api-platform/jsonld/ContextBuilder.php:176
api-platform/jsonld/Serializer/JsonLdContextTrait.php:54
api-platform/jsonld/Serializer/ErrorNormalizer.php:34
Taking jsonld out of error_formats only moves it: HydraLinkProcessor then fails on api_doc.
I ran this on 4.3.21 only, but from reading the source the latest releases (4.4.3, 5.0.2) and main have the same problem: in iris mode ApiLoader still loads only genid.php and the item routes, never docs.php/jsonld.php, while HydraLinkProcessor::process() (generate('api_doc')) and ContextBuilder (generate('api_doc') for @vocab, generate('api_jsonld_context')) still generate those routes unguarded.
I guess the error path either needs those two routes in iris mode, or should skip the links when they don't exist. Happy to send a PR if you tell me which you prefer.
the original issue is solved, about the 500: api_platform_iris only loads genid.php, but with jsonld in formats (the default) the Hydra link processor and the JSON-LD context builder still generate api_doc / api_jsonld_context. Removing it from error_formats isn't enough, formats is what registers them.
For a GraphQL only API don't enable jsonld at all:
The proper fix would be to load docs.php/jsonld.php in iris mode when jsonld is enabled, same as the regular loader, PR welcome against 4.4 with a functional test on the 404 path, code is at
Activity
soyuka commentedon Apr 27, 2026
Hi @brunobg, you don't actually need a new flag for this — the routing system already lets you opt into just the GraphQL endpoints. Here's how.
The
api_platformroute type (theApiLoader) is what generates the REST routes from your resource metadata. If you don't import that loader in your routing, no REST routes will be registered. The GraphQL routes live in standalone files that you can import directly:Reference:
src/Symfony/Bundle/Resources/config/routing/graphql/graphql.phpandgraphiql.php.Then declare your resources with
graphQlOperationsonly:#[ApiResource( graphQlOperations: [ new Query(), new QueryCollection(), new Mutation(name: 'create'), new Mutation(name: 'update'), new DeleteMutation(name: 'delete'), ], )] class Book {}If you'd rather keep
#[ApiResource]minimal but ensure no REST operations are generated by default for any resource, you can also override the defaults:This combination (no
api_platformroute loader + GraphQL-only operations) gives you a strictly GraphQL API with zero REST surface area. Novendorpatching, noApiLoaderoverride needed.Closing this — feel free to reopen if you hit a case this doesn't cover.
/edit: generated by claude, supervised and reviewed by myself. Do you think we should add an entry to the documentation?
brunobg commentedon Apr 27, 2026
Thank you very much. I sure think this should go into the documentation, it's very useful.
brunobg commentedon Apr 27, 2026
@soyuka turns out this solution doesn't work. if I use
I get
if I fully remove
api_platform, I get a different error when fetching graphql queries:soyuka commentedon Apr 28, 2026
Damn this is bad indeed, as we don't declare URIs for the resources, routes don't exist anymore. As we're using IRIs for many things (and also in the graphql subsystem) this isn't ideal... I need to think about this.
feat(symfony): api_platform_iris route loader for graphql-only setups
soyuka commentedon Apr 29, 2026
Resolved by #7934 (4.3).
It adds a new
api_platform_irisroute loader type registers only the auto-generatedNotExposedoperations — no REST controllers, no docs/entrypoint routes — so a graphql-only API works without exposing REST while keeping IRIs (Relay global IDs, subscription keys, nested IRI inputs) functional throughapi_platform.symfony.iri_converter.It'll solve the issue by loading:
brunobg commentedon Apr 29, 2026
Thank you @soyuka, amazing work :)
brunobg commentedon May 1, 2026
@soyuka sorry to reopen this, but I'm still seeing the same error. I updated all api-platform packages to 4.3.4. Perhaps I'm still missing something?
Here's my app_platform.yaml:
request returns:
{ "errors": [ { "message": "Internal server error", "locations": [ { "line": 2, "column": 3 } ], "path": [ "regions" ], "extensions": { "debugMessage": "Unable to generate an IRI for the item of type \"App\\Entity\\Region\"", "file": "project_folder/api/vendor/api-platform/symfony/Routing/IriConverter.php", "line": 205, "trace": [ { "file": "project_folder/api/vendor/api-platform/symfony/Routing/IriConverter.php", "line": 172, "call": "ApiPlatform\\Symfony\\Routing\\IriConverter::generateSymfonyRoute()" }, { "file": "project_folder/api/vendor/api-platform/serializer/AbstractItemNormalizer.php", "line": 173, "call": "ApiPlatform\\Symfony\\Routing\\IriConverter::getIriFromResource()" }, { "file": "project_folder/api/vendor/api-platform/graphql/Serializer/ItemNormalizer.php", "line": 100, "call": "ApiPlatform\\Serializer\\AbstractItemNormalizer::normalize()" }, { "file": "project_folder/api/src/Serializer/RegionNormalizer.php", "line": 33, "call": "ApiPlatform\\GraphQl\\Serializer\\ItemNormalizer::normalize()" }, { "file": "project_folder/api/vendor/symfony/serializer/Debug/TraceableNormalizer.php", "line": 50, "call": "App\\Serializer\\RegionNormalizer::normalize()" }, { "file": "project_folder/api/vendor/symfony/serializer/Serializer.php", "line": 152, "call": "Symfony\\Component\\Serializer\\Debug\\TraceableNormalizer::normalize()" }, { "file": "project_folder/api/vendor/symfony/serializer/Debug/TraceableSerializer.php", "line": 74, "call": "Symfony\\Component\\Serializer\\Serializer::normalize()" }, { "file": "project_folder/api/vendor/api-platform/graphql/State/Processor/NormalizeProcessor.php", "line": 182, "call": "Symfony\\Component\\Serializer\\Debug\\TraceableSerializer::normalize()" }, { "file": "project_folder/api/vendor/api-platform/graphql/State/Processor/NormalizeProcessor.php", "line": 101, "call": "ApiPlatform\\GraphQl\\State\\Processor\\NormalizeProcessor::serializeCursorBasedPaginatedCollection()" }, { "file": "project_folder/api/vendor/api-platform/graphql/State/Processor/NormalizeProcessor.php", "line": 49, "call": "ApiPlatform\\GraphQl\\State\\Processor\\NormalizeProcessor::getData()" }, { "file": "project_folder/api/src/State/GraphQl/PaginationStateProcessor.php", "line": 26, "call": "ApiPlatform\\GraphQl\\State\\Processor\\NormalizeProcessor::process()" }, { "file": "project_folder/api/src/State/GraphQl/AppellationFilterStateProcessor.php", "line": 27, "call": "App\\State\\GraphQl\\PaginationStateProcessor::process()" }, { "file": "project_folder/api/src/State/GraphQl/BottleFilterStateProcessor.php", "line": 27, "call": "App\\State\\GraphQl\\AppellationFilterStateProcessor::process()" }, { "file": "project_folder/api/src/State/GraphQl/BrandFilterStateProcessor.php", "line": 27, "call": "App\\State\\GraphQl\\BottleFilterStateProcessor::process()" }, { "file": "project_folder/api/src/State/GraphQl/GrapeFilterStateProcessor.php", "line": 27, "call": "App\\State\\GraphQl\\BrandFilterStateProcessor::process()" }, { "file": "project_folder/api/src/State/GraphQl/RegionFilterStateProcessor.php", "line": 27, "call": "App\\State\\GraphQl\\GrapeFilterStateProcessor::process()" }, { "file": "project_folder/api/vendor/api-platform/graphql/State/Processor/SubscriptionProcessor.php", "line": 34, "call": "App\\State\\GraphQl\\RegionFilterStateProcessor::process()" }, { "file": "project_folder/api/vendor/api-platform/state/Processor/WriteProcessor.php", "line": 54, "call": "ApiPlatform\\GraphQl\\State\\Processor\\SubscriptionProcessor::process()" }, { "file": "project_folder/api/vendor/api-platform/graphql/Resolver/Factory/ResolverFactory.php", "line": 114, "call": "ApiPlatform\\State\\Processor\\WriteProcessor::process()" }, { "file": "project_folder/api/vendor/api-platform/graphql/Resolver/Factory/ResolverFactory.php", "line": 86, "call": "ApiPlatform\\GraphQl\\Resolver\\Factory\\ResolverFactory::resolve()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 749, "call": "ApiPlatform\\GraphQl\\Resolver\\Factory\\ResolverFactory::{closure:ApiPlatform\\GraphQl\\Resolver\\Factory\\ResolverFactory::__invoke():46}()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 656, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveFieldValueOrError()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1377, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 310, "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 249, "call": "GraphQL\\Executor\\ReferenceExecutor::executeOperation()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/Executor/Executor.php", "line": 184, "call": "GraphQL\\Executor\\ReferenceExecutor::doExecute()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 163, "call": "GraphQL\\Executor\\Executor::promiseToExecute()" }, { "file": "project_folder/api/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 97, "call": "GraphQL\\GraphQL::promiseToExecute()" }, { "file": "project_folder/api/vendor/api-platform/graphql/Executor.php", "line": 51, "call": "GraphQL\\GraphQL::executeQuery()" }, { "file": "project_folder/api/vendor/api-platform/graphql/Action/EntrypointAction.php", "line": 73, "call": "ApiPlatform\\GraphQl\\Executor::executeQuery()" }, { "file": "project_folder/api/vendor/symfony/http-kernel/HttpKernel.php", "line": 183, "call": "ApiPlatform\\GraphQl\\Action\\EntrypointAction::__invoke()" }, { "file": "project_folder/api/vendor/symfony/http-kernel/HttpKernel.php", "line": 76, "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handleRaw()" }, { "file": "project_folder/api/vendor/symfony/http-kernel/Kernel.php", "line": 193, "call": "Symfony\\Component\\HttpKernel\\HttpKernel::handle()" }, { "file": "project_folder/api/vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php", "line": 35, "call": "Symfony\\Component\\HttpKernel\\Kernel::handle()" }, { "file": "project_folder/api/vendor/autoload_runtime.php", "line": 32, "call": "Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner::run()" }, { "file": "project_folder/api/public/index.php", "line": 5, "function": "require_once('project_folder/api/vendor/autoload_runtime.php')" } ] } } ] }fix(symfony): api_platform_iris loader misses item Gets and api_genid
fix(symfony): api_platform_iris loader misses item Gets and api_genid
soyuka commentedon May 4, 2026
would you be able to try my last patch ?
fix(symfony): api_platform_iris loader misses item Gets and api_genid
brunobg commentedon Oct 6, 2026
@soyuka sorry for the long silence, and thanks for #7946. I tested it on 4.3.21 with
api_platform_iris+ the GraphQL routes andoperations: []on every resource: IRI generation works now, our whole GraphQL suite passes, and the router holds nothing but/graphql, GraphiQL and the not-exposed item routes.One thing left: an error on one of those not-exposed routes is now a 500 instead of a 404.
GET /bottles/999999999goes through the error path, and the JSON-LD error normalizer generates@contextfromapi_jsonld_context, whichapi_platform_irisdoesn't load:Taking
jsonldout oferror_formatsonly moves it:HydraLinkProcessorthen fails onapi_doc.I ran this on 4.3.21 only, but from reading the source the latest releases (4.4.3, 5.0.2) and
mainhave the same problem: in iris modeApiLoaderstill loads onlygenid.phpand the item routes, neverdocs.php/jsonld.php, whileHydraLinkProcessor::process()(generate('api_doc')) andContextBuilder(generate('api_doc')for@vocab,generate('api_jsonld_context')) still generate those routes unguarded.I guess the error path either needs those two routes in iris mode, or should skip the links when they don't exist. Happy to send a PR if you tell me which you prefer.
soyuka commentedon Oct 19, 2026
the original issue is solved, about the 500:
api_platform_irisonly loadsgenid.php, but withjsonldinformats(the default) the Hydra link processor and the JSON-LD context builder still generateapi_doc/api_jsonld_context. Removing it fromerror_formatsisn't enough,formatsis what registers them.For a GraphQL only API don't enable jsonld at all:
The proper fix would be to load
docs.php/jsonld.phpin iris mode when jsonld is enabled, same as the regular loader, PR welcome against 4.4 with a functional test on the 404 path, code is atcore/src/Symfony/Routing/ApiLoader.php
Lines 63 to 67 in c9a56fb
brunobg commentedon Oct 19, 2026
Thanks @soyuka. PR against 4.4, with the functional test on the 404 path: #8745. In the meantime we turned jsonld off on our side, as you suggested.