graphql-java / graphql-java-spring Goto Github PK
View Code? Open in Web Editor NEWGraphQL Java Spring and Spring Boot integration
License: MIT License
GraphQL Java Spring and Spring Boot integration
License: MIT License
I'm using MockMvc
to test GraphQL API, but tests always failure because the response is empty. The full code can be found at Spring Boot in Practice.
package net.jaggerwang.sbip.api;
...
@SpringBootTest()
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Sql({"/db/init-db-test.sql"})
@Sql(scripts = {"/db/clean-db-test.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@EnabledIfSystemProperty(named = "test.api.enabled", matches = "true")
public class UserGraphQLApiTests {
@Autowired
private MockMvc mvc;
@Value("${graphql.url}")
private String graphqlUrl;
@Autowired
private ObjectMapper objectMapper;
@Test
void login() throws Exception {
var userEntity = UserEntity.builder().username("jaggerwang").password("123456").build();
var content = new ObjectMapper().createObjectNode();
content.put("query", "mutation($user: UserInput!) { authLogin(user: $user) { id username } }");
content.putObject("variables").putObject("user").put("username", userEntity.getUsername())
.put("password", userEntity.getPassword());
mvc.perform(post(graphqlUrl).contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(content))).andExpect(status().isOk())
.andExpect(jsonPath("$.errors").doesNotExist())
.andExpect(jsonPath("$.data.authLogin.username").value(userEntity.getUsername()));
}
...
}
Test output:
...
MockHttpServletRequest:
HTTP Method = POST
Request URI = /graphql
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"149"]
Body = {"query":"mutation($user: UserInput!) { authLogin(user: $user) { id username } }","variables":{"user":{"username":"jaggerwang","password":"123456"}}}
Session Attrs = {}
Handler:
Type = graphql.spring.web.servlet.components.GraphQLController
Method = graphql.spring.web.servlet.components.GraphQLController#graphqlPOST(String, String, String, String, String, WebRequest)
Async:
Async started = true
Async result = {data={authLogin={id=1, username=jaggerwang}}}
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Set-Cookie:"SESSION=OTFiNzRmNDgtYTIzNy00N2ZlLTk4MWQtNmMxNzMwYzU1ZDVk; Path=/; HttpOnly; SameSite=Lax"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = [[MockCookie@7d9c448f name = 'SESSION', value = 'OTFiNzRmNDgtYTIzNy00N2ZlLTk4MWQtNmMxNzMwYzU1ZDVk', comment = [null], domain = [null], maxAge = -1, path = '/', secure = false, version = 0, httpOnly = true]]
...
[ERROR] login Time elapsed: 0.187 s <<< FAILURE!
java.lang.AssertionError: No value at JSON path "$.data.authLogin.username"
at net.jaggerwang.sbip.api.UserGraphQLApiTests.login(UserGraphQLApiTests.java:45)
Caused by: java.lang.IllegalArgumentException: json can not be null or empty
at net.jaggerwang.sbip.api.UserGraphQLApiTests.login(UserGraphQLApiTests.java:45)
...
Can we implement some sort of integration with spring security? I think it might look like special GraphQL mutation, witch will be handled by library. The problem is that I cannot configure app using existing '..ConfigurerAdapter's since they rely on filters and with GraphQL we always have one endpoint.
I'd like to avoid manual authentication on home grown mutation.
I'm using graphql in spring cloud gateway, so it is a webflux environment. I've already successfully run a graphql endpoint, the data fetchers need to get data from backend services using webclient, so I need to pass through the Authorization
header. I wrote a webclient filter to do this automatically. The full source code can be found at Spring Cloud in Practice.
package net.jaggerwang.scip.common.api.filter;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Set;
public class HeadersRelayFilter implements ExchangeFilterFunction {
private Set<String> headers;
public HeadersRelayFilter(Set<String> headers) {
this.headers = headers;
}
@Override
public Mono<ClientResponse> filter(ClientRequest clientRequest,
ExchangeFunction exchangeFunction) {
return Mono.subscriberContext()
.flatMap(ctx -> {
var upstreamExchange = ctx.getOrEmpty(ServerWebExchange.class);
if (upstreamExchange.isPresent()) {
var upstreamHeaders = ((ServerWebExchange) upstreamExchange.get())
.getRequest().getHeaders();
for (var header: headers) {
clientRequest.headers().addAll(header, upstreamHeaders.get(header));
}
}
return exchangeFunction.exchange(clientRequest);
});
}
}
But the ctx
is always empty. I can get current exchange object in normal webflux handler, so it seems the problem of GraphQLController
. I'm using spring boot 2.2.2.RELEASE
and spring cloud Hoxton.SR1
.
<?xml version="1.0" ?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice-gateway</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>spring-cloud-in-practice-gateway</name>
<description>Spring cloud in practice gateway</description>
<dependencies>
<dependency>
<groupId>net.jaggerwang</groupId>
<artifactId>spring-cloud-in-practice-common</artifactId>
<version>${scip-common.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-spring-boot-starter-webflux</artifactId>
<version>${graphql-starter.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
<version>${graphql-extended-scalars.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
When I implement the interface 'GraphQLQueryResolver' to handle query request, the 'DataFetchingEnvironment' is not provided. Thus I must configure a spring 'GraphQL' bean to register a data fetcher to get the enviornment. Has there any shortcuts please?
As stated in https://graphql.org/learn/serving-over-http/ besides the 'normal' GET
and POST
requests the following is recommended:
- If the "query" query string parameter is present (as in the GET example above), it should be parsed and handled in the same way as the HTTP GET case.
- If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.
If interested I can create a PR for this. Let me know.
Note: in the Micronaut integration lib we are doing the same: https://github.com/micronaut-projects/micronaut-graphql/blob/master/graphql/src/main/java/io/micronaut/graphql/GraphQLController.java#L116-L178
Hello,
You've merged the pull request from marceloverdijk.
This allows to use DataLoader with Spring Boot. Very interesting !
Please publish a release, so we can use it for our builds.
Etienne
I am trying to introduce graphql from my project ,
now , who can tell can me ,how to implement mapping my function with graphql
just like resultful
Code :
public CustomerEntity getCustomer(String account) {
CustomerEntity customerEntity = this.customerMapper.selectByAccount(account);
if (null == customerEntity.getAccount()) {
return null;
}
if (customerEntity.getCustomerStatus() > 9) {
return null;
}
return customerEntity;
}
I post a request with header "Content-Type: application/json; charset=utf-8", and it returns 422 Http Code.
Now the controller only accept Content-Type strictly equals to "application/json" Can it support more dynamic Content-Type header?
Same to the title;
in my application.properties
graphql.url = /other
but still only request to the graphql
, and when request to /other
,response with status 404
I have given a try the code introduced by #12
I used version 1.0 and created a CustomGraphQLInvocation
class based on https://github.com/graphql-java/graphql-java-spring/pull/12/files#diff-cb93f026b7b911f1cdfa731e56caa39f
The comment in this PR says:
I think it's overkill to create a ExecutionInputCustomizer for this, simply defining a (request-scoped) @bean should be sufficient.
I've create that bean like this:
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public DataLoaderRegistry dataLoaderRegistry() {...}
And it fails with:
{
"timestamp": "2019-09-29T11:07:42.456+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error creating bean with name 'scopedTarget.dataLoaderRegistry': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.",
"path": "/graphql"
}
If I change the scope to WebApplicationContext.SCOPE_APPLICATION
then it doesn't throw an error. How should this be approached to have a request scoped DataLoaderRegistry
?
A working workaround is to change the DefaultGraphQLInvocation
and remove:
@Autowired(required = false)
DataLoaderRegistry dataLoaderRegistry;
and replace:
if (dataLoaderRegistry != null) {
executionInputBuilder.dataLoaderRegistry(dataLoaderRegistry);
}
with:
DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry();
// Loader specific configuration
DataLoader<String, CountryTO> countryLoader = DataLoader.newDataLoader(dataFetchers.countryBatchLoader());
dataLoaderRegistry.register("countries", countryLoader);
// Loader specific configuration end
executionInputBuilder.dataLoaderRegistry(dataLoaderRegistry);
executionInputBuilder.context(dataLoaderRegistry); // add it to the context to use it later in fetchers
Right now, graphql java webflux only support CompletableFuture
return type, the mono returned need to be converted to CompletableFuture
by calling toFuture()
method.
package net.jaggerwang.scip.gateway.adapter.graphql.datafetcher.query;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import net.jaggerwang.scip.gateway.adapter.graphql.datafetcher.AbstractDataFetcher;
import org.springframework.stereotype.Component;
@Component
public class QueryUserInfoDataFetcher extends AbstractDataFetcher implements DataFetcher {
@Override
public Object get(DataFetchingEnvironment env) {
var id = Long.valueOf((Integer) env.getArgument("id"));
return userAsyncService.info(id)
.subscriberContext(context -> env.getContext())
.toFuture();
}
}
But if I want to use spring security to protect method by @EnableReactiveMethodSecurity
, spring security will require the method to return Mono
or Flux
. The two are conflicted.
@Component
public class QueryUserInfoDataFetcher extends AbstractDataFetcher implements DataFetcher {
@Override
@PreAuthorize("isAuthenticated()")
public Object get(DataFetchingEnvironment env) {
var id = Long.valueOf((Integer) env.getArgument("id"));
return monoWithContext(userAsyncService.info(id), env);
}
}
How can I solve this problem?
Hello,
this is acually a question, not an issue. Can somebody please explain the reasons/benefits, why one should use this project instead of
https://github.com/graphql-java-kickstart/graphql-spring-boot
There seems also to be more activity on the other project.
Thanks and best,
Michael
In graphql-java-spring-webmvc
, GraphQLController.java
, when request has content type of application/json
and contains query parameter, it is not handled, as following if
statement is trying to parse request body (which is null in this case):
Is this correct behavior? According to this article it is not said that application/json
cannot be used with query parameter.
The project can autoconfigure graphqlController, provides an rest mapping of /graphql be default. It is great for my project, save me lot of code.
Recently, I want to write a subscription to monitor changes from server. I checked out the source code and related artifacts about spring-boot-starter of graphql.
I founded com.graphql-java:graphql-spring-boot-starter:5.0.2 can do that, but that uses an old graphql-java core dependency version released around 2018. Which have not more newer version, and have unofficial package name in source code, so I think it is likely be DEPRECATED? or retired?
And graphql-java-spring likely the official "starter" project, but have no subscriptions support, any other friends meet the similar problems?
Hi, is there any way to gzip the response when the request headers contains a Accept-Encoding: gzip
.
Thanks in advance.
My Graphql-Java server is working fine and queries work fine through Insomnia. However I am trying to use React/Relay as the client and I am getting the following error message from my React App
Access to fetch at 'http://localhost:8080/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Do you know how I can set-up my GraphQL server to respond correctly?
Thanks, Martin
Spring webflux uses Reactor for its async logic. Its made of a chain of Mono<T>
's. That chain can carry a context that can be used anywhere in your code within the scope of that single request. The SecurityContext
resides within that chain context.
That chain is broken by the underlying implementation of DataFetcher
which uses CompletableFuture
for its own async code. As a result the User is no longer authenticated at any point after the logic is delegated to GraphQL-Java.
One solution to this is to add the SecurityContext
to the ExecutionInput
's context, so that we are at least able to access it (even if its not the normal way)
Some example code written in kotlin
@Component
@Internal
class DefaultGraphQLInvocation(private val graphQL: GraphQL) : GraphQLInvocation {
override fun invoke(invocationData: GraphQLInvocationData, webRequest: ServerWebExchange): Mono<ExecutionResult> {
return ReactiveSecurityContextHolder.getContext().flatMap { securityContext ->
val executionInput = ExecutionInput.newExecutionInput()
.query(invocationData.query)
.operationName(invocationData.operationName)
.variables(invocationData.variables)
.context(securityContext)
.build()
Mono.fromCompletionStage(graphQL.executeAsync(executionInput))
}
}
}
Currently the default Spring ObjectMapper is injected.
But in many applications it is already widely used and cannot be customized for graphql.
In my case a legacy app uses ObjectMapper with setSerializationInclusion(Include.NON_NULL)
. This will break lot's of graphql clients. But I can't change it, because a lot of stuff within the legacy app relies on that behavior.
I used this depenendy in my app and everything worked just fine, until I decided to add uploading files feature. I have read that this feature is already implemented in
graphql-java-kickstart/graphql-java-servlet#102
so I declared the new Scalar (in schema and in RuntimeWiring
configuration .scalar(ApolloScalars.Upload)
). Sadly when I try to POST with the file
curl http://localhost:8080/graphql \ -F operations='{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id } }", "variables": { "file": null } }' \ -F map='{ "0": ["variables.file"] }' \ -F [email protected]
I get
{"timestamp":"2019-05-19T16:31:05.151+0000","status":415,"error":"Unsupported Media Type","message":"Content type 'multipart/form-data;boundary=------------------------07182a3f39beb5b0;charset=UTF-8' not supported","path":"/graphql"}
I figured out that it was because @GraphQLController
from this repository only consumes JSON and not multipart/form-data. Do you have any suggestions how to solve it?
I'm trying to run the graphql-java-spring-webmvc web app locally, which works fine with graphql-java 14.1
, but breaks with 15.0
:
2020-06-04 21:55:15.014 ERROR 45400 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.NoSuchMethodError: graphql.Assert.assertNotNull(Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;] with root cause
java.lang.NoSuchMethodError: graphql.Assert.assertNotNull(Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;
at graphql.spring.web.servlet.GraphQLInvocationData.<init>(GraphQLInvocationData.java:17) ~[graphql-java-spring-webmvc-1.0.jar!/:na]
at graphql.spring.web.servlet.components.GraphQLController.graphqlPOST(GraphQLController.java:47) ~[graphql-java-spring-webmvc-1.0.jar!/:na]
...
The problem is that graphql-java version 15 made changes to the graphql.Assert
class that aren't backwards-compatible. As a result, the call from the GraphQLInvocationData constructor to Assert.assertNotNull()
fails to find a method matching the expected signature.
I don't know if there are other changes in v15 that need to be addressed here as well, but I wanted to bring this one to your attention. I'll stay on v14.1 for now, as a result of this problem.
Now spring boot 2.2.0 has turned GA, spring-fu released related 0.2.1. Can you take a look at #18, it would allow graphql-java-spring to be used in spring-fu project.
Since a GraphQL query can contain mutations, allowing the GET HTTP method opens the door for CSRF attacks in certain configurations.
When GET is allowed for APIs that use HTTP Cookies, a malicious actor can trick another user into clicking a link containing a GraphQL query that mutates state. Of course itβs important to note that disallowing GET is not sufficient to prevent all CSRF attacks.
See the Get Scenario on OWASP's CSRF article for more details.
For an API to be susceptible to this type of attack:
1 and 2 are very common for APIs. 3 is less common (and a bad idea) but still used.
Since GraphQL clients tend to prefer the POST method, disabling the GET method is possible with little (no?) fallout.
Are the project maintainers open to this change? If so I am willing to do the work.
Hello,
Are there any plans to upgrade to the latest spring boot version ?
Existing version: 2.1.5.RELEASE (May 2019)
I know spring is moving very fast, but would be good to keep up to date with them - looks like we are +/- 50 releases behind.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.