spring-projects / spring-projects/spring-graphql
GraphQlRequestPredicates rejects requests to non-GraphQL paths with 415 when the Content-Type is unparseable (path is matched last)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.6k
- Forks
- 336
- PR merge metrics
- No merged PRs in 30d
Description
Since #1145 (1.3.5), GraphQlRequestPredicates$GraphQlHttpRequestPredicate.contentTypeMatch throws UnsupportedMediaTypeStatusException when the request's Content-Type header cannot be parsed. The predicate evaluates its conditions in this order:
public boolean test(ServerRequest request) {
return httpMethodMatch(request, HttpMethod.POST)
&& contentTypeMatch(request, this.contentTypes)
&& acceptMatch(request, this.acceptedMediaTypes)
&& pathMatch(request, this.pattern); // path is checked LAST
}
Because RouterFunctionMapping evaluates the predicate during handler lookup for every request, any POST anywhere in the application with a malformed Content-Type is now answered 415 — even when it does not target the GraphQL endpoint at all. The route predicate effectively acts as an application-wide Content-Type validator, and the exception escapes getHandler(...) before other mappings (or a servlet forward) get a chance to handle the request.
Notably, Spring MVC itself tolerates the same header: ConsumesRequestCondition catches InvalidMediaTypeException and simply reports "no match", so in an application without spring-graphql the identical request proceeds normally.
Real-world impact
Vaadin Flow's client sends every heartbeat request with the literal header Content-Type: null — a long-standing client quirk (reported as vaadin/flow#25104). In any application combining Vaadin with spring-graphql's WebMVC transport, every heartbeat POST to / is rejected 415 during handler mapping, the Vaadin client interprets the failed heartbeats as a connection problem, and users see a permanent "connection lost / trying to reconnect" overlay on a healthy connection. That combination is how we found this; it took a debugger on Response.sendError to trace the 415 to the GraphQL route predicate, since the failing request has nothing to do with GraphQL.
Minimal reproduction
Spring Boot app with spring-boot-starter-web + spring-boot-starter-graphql (any controller or none):
curl -i -X POST http://localhost:8080/this-is-not-graphql -H 'Content-Type: foo'
→ 415 UnsupportedMediaTypeStatusException: Could not parse Content-Type [foo] — expected: whatever the application's handler chain would normally produce for that path (404, a controller response, a forward, …).
Observed with spring-graphql 2.0.4 / Spring Boot 4.1.0 / Framework 7.0.8; the code on main is unchanged.
Suggested change
Evaluate pathMatch first (it is cheap and decisive), so requests that do not target the GraphQL path are never affected by Content-Type parsing. The deliberate 415 from #1145 would remain fully intact for requests genuinely aimed at the GraphQL endpoint. Alternatively, contentTypeMatch could return false instead of throwing when the path does not match.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with GraphQlRequestPredicates$GraphQlHttpRequestPredicate.test and its pathMatch/contentTypeMatch calls, then run the listed curl reproduction against a Spring Boot app. Done means POST requests outside the GraphQL path no longer receive 415 for an unparseable Content-Type, while malformed Content-Type requests aimed at the GraphQL endpoint retain the deliberate 415 behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- graphql, java, spring
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100