Search code examples
javaspringxssgraphqlgraphql-java

Why does my Spring controller not handle an OPTIONS request sent by GraphiQL?


I'm trying to make the GraphQL Java server work with GraphiQL server.

Using the GraphiQL running locally I submit a query with the following parameters:

GraphiQL query and its results

My Spring controller (copied from here) looks like this:

@Controller
@EnableAutoConfiguration
public class MavenController {
    private final MavenSchema schema = new MavenSchema();
    private final GraphQL graphql = new GraphQL(schema.getSchema());
    private static final Logger log = LoggerFactory.getLogger(MavenController.class);

    @RequestMapping(value = "/graphql", method = RequestMethod.OPTIONS, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Object executeOperation(@RequestBody Map body) {
        log.error("body: " + body);
        final String query = (String) body.get("query");
        final Map<String, Object> variables = (Map<String, Object>) body.get("variables");
        final ExecutionResult executionResult = graphql.execute(query, (Object) null, variables);
        final Map<String, Object> result = new LinkedHashMap<>();
        if (executionResult.getErrors().size() > 0) {
            result.put("errors", executionResult.getErrors());
            log.error("Errors: {}", executionResult.getErrors());
        }
        log.error("data: " + executionResult.getData());
        result.put("data", executionResult.getData());
        return result;
    }
}

In theory, executeOperation should be invoked, when I submit the query in GraphiQL. It's not, I don't see the log statement in the console output.

What am I doing wron? How can I make sure that the MavenController.executeOperation is called, when a query is submitted in GraphiQL?

Update 1 (13.01.2017 13:35 MSK): Here is a tutorial on how to reproduce the error. My goal is to create a Java-based GraphQL server, with which I can interact using GraphiQL. If possible, this should work locally.

I’ve read that in order to do that, following steps are necessary:

  1. Set up a GraphQL server. An example of this can be found here https://github.com/graphql-java/todomvc-relay-java. That example uses Spring Boot, but you can use whatever HTTP server you like to get that going.
  2. Set up the GraphiQL server. That is a bit beyond the scope of this project, but basically you need to get GraphiQL talking to the server in step 1 above. It will use introspection to load the schema.

I checked out the project todomvc-relay-java, modified it according to my needs and put it into directory E:\graphiql-java\graphql-server. You can download an archive with that directory here.

Step 1: Install Node.JS

Step 2

Go to E:\graphiql-java\graphql-server\app and run npm install there.

Step 3

Run npm start from the same directory.

Step 4

Go to E:\graphiql-java\graphql-server and run gradlew start there.

Step 5

Run docker run -p 8888:8080 -d -e GRAPHQL_SERVER=http://localhost:8080/graphql merapar/graphql-browser-docker.

Docker sources: graphql-browser-docker

Step 6

Launch Chrome with disabled XSS check, e. g. "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --args --disable-xss-auditor. Some sources claim you have to kill all other Chrome instances in order for these arguments to have any effect.

Step 7

Open http://localhost:8888/ in that browser.

Step 8

Try to run query

{ allArtifacts(group: "com.graphql-java", name: "graphql-java") { group name version } }

Actual results:

1) Error in the Console tab of Chrome: Fetch API cannot load http://localhost:8080/graphql. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8888' is therefore not allowed access. The response had HTTP status code 403. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled..

Errors in the "Console" tab

2) Errors in the Network tab of Chrome:

Errors in the "Network" tab

Update 2 (14.01.2017 13:51): Currently, CORS is configured in the Java appliction in the following way.

Main class:

@SpringBootApplication
public class Main {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry
                        .addMapping("/**")
                        .allowedMethods("OPTIONS")
                        .allowedOrigins("*")
                        .allowedHeaders(
                                "Access-Control-Request-Headers",
                                "Access-Control-Request-Method",
                                "Host",
                                "Connection",
                                "Origin",
                                "User-Agent",
                                "Accept",
                                "Referer",
                                "Accept-Encoding",
                                "Accept-Language",
                                "Access-Control-Allow-Origin"
                        )
                        .allowCredentials(true)
                        ;
            }
        };
    }
}

WebSecurityConfigurerAdapter subclass:

@Configuration
@EnableWebSecurity
public class SpringWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        System.out.println("SpringWebSecurityConfiguration");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        source.registerCorsConfiguration("/**", config);
        http
                .addFilterBefore(new CorsFilter(source), ChannelProcessingFilter.class)
                .httpBasic()
                .disable()
                .authorizeRequests()
                    .anyRequest()
                    .permitAll()
                .and()
                .csrf()
                    .disable();
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
    }
}

The controller:

@Controller
@EnableAutoConfiguration
public class MavenController {
    private final MavenSchema schema = new MavenSchema();
    private final GraphQL graphql = new GraphQL(schema.getSchema());
    private static final Logger log = LoggerFactory.getLogger(MavenController.class);

    @CrossOrigin(
            origins = {"http://localhost:8888", "*"},
            methods = {RequestMethod.OPTIONS},
            allowedHeaders = {"Access-Control-Request-Headers",
            "Access-Control-Request-Method",
            "Host",
            "Connection",
            "Origin",
            "User-Agent",
            "Accept",
            "Referer",
            "Accept-Encoding",
            "Accept-Language",
             "Access-Control-Allow-Origin"},
            exposedHeaders = "Access-Control-Allow-Origin"
             )
    @RequestMapping(value = "/graphql", method = RequestMethod.OPTIONS, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Object executeOperation(@RequestBody Map body) {
        [...]
    }
}

MavenController.executeOperation is the method that's supposed to be called, when I issue a query request in GraphiQL.

Update 3 (15.01.2017 22:05): Tried to use CORSFilter, new source code is here. No result, I'm still getting "Invalid CORS response" error.


Solution

  • The problem is the with the usage of .allowedMethods("OPTIONS")configuration in your application.

    Pre-flight requests by design sent with OPTIONS request method.

    Access-Control-Request-Method is added by the browser automatically and allowedMethods property actually controls what request method are allowed for the actual request.

    From the docs,

    The Access-Control-Request-Method header notifies the server as part of a preflight request that when the actual request is sent, it will be sent with a POST request method.

    So it is POST method and and the request fails as you're only allowing OPTIONS when the validation is done.

    So changing allowedMethods to * will match browser set POST request method for actual request.

    After you do above change you'll get 405 as your controller only allows OPTIONS for your POST request.

    So, you'll need to update the controller request mapping to allow POST for the actual request to succeed after pre-flight request.

    Sample Response:

    {
        "timestamp": 1484606833696,
        "status": 500,
        "error": "Internal Server Error",
        "exception": "graphql.AssertException",
        "message": "arguments can't be null",
        "path": "/graphql"
    }
    

    I'm not sure looks like you've CORS configuration set at too many places. I just have to change the .allowedMethods in the spring security configuration to make it work the way I described. So you may want to look into that.