Skip to content

feat: complete the GraphQL server HTTP binding (G1) - #1590

Open
jlouvel wants to merge 3 commits into
feat/graphql-g0-spikefrom
feat/graphql-g1-server
Open

jlouvel wants to merge 3 commits into
feat/graphql-g0-spikefrom
feat/graphql-g1-server

Conversation

@jlouvel

@jlouvel jlouvel commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #1583. Part of #1581.

Stacked on #1589 (G0). Merge #1589 first; this PR's base then moves to 3.0.

Summary

Completes the GraphQL over HTTP binding of GraphqlServerAdapter.

GraphqlServerAdapter graphql = new GraphqlServerAdapter(getContext());
graphql.setGraphQL(GraphQL.newGraphQL(schema).build());
graphql.setGetEnabled(true);                                   // off by default
graphql.setMaxRequestBytes(256 * 1024);                        // 1 MiB by default
graphql.setParserOptions(ParserOptions.newParserOptions().maxTokens(5_000).build());
graphql.setExecutionInputCustomizer((request, input) -> input.dataLoaderRegistry(loaders()));
Aspect Behavior
Methods POST for all operations. GET for queries only when enabled; other operations over GET get 405 with Allow: POST
Request media type application/json; others get 415. A JSON array (batch) gets 400, never executed
Response media type application/graphql-response+json when named by the client, otherwise application/json; 406 if neither
Status codes 400 for malformed requests. Parse/validation errors and missing or unknown operations: 400 with graphql-response+json, 200 with application/json. 200 once execution starts, even with field errors
Limits 413 above maxRequestBytes, checked on the declared size and while reading. Parser limits from the adapter's ParserOptions, passed per execution in the GraphQLContext, never through graphql-java's process-wide static defaults
Request context RestletGraphql.REQUEST and RestletGraphql.CLIENT_INFO in the GraphQLContext, set after the customizer so neither it nor caller extensions can replace them
Subscriptions Rejected without executing the subscription field until SSE support (G3)

Policies stay native: depth and complexity limits (MaxQueryDepthInstrumentation, MaxQueryComplexityInstrumentation), introspection and exception rendering (DataFetcherExceptionHandler) are configured on the GraphQL instance.

The document is parsed once by the adapter (to select the operation and enforce the GET and subscription rules before anything executes) and again by graphql-java. The adapter does not install a PreparsedDocumentProvider, which remains the application's choice.

Validation

./mvnw -B verify -P default,test -pl org.restlet.ext.graphql: 25 tests, 0 failures; 95.8% instruction coverage (gate 80%); Spotless, SpotBugs, Javadoc, -Werror and dependency convergence pass.

New GraphqlServerBindingTestCase (12 tests): GET disabled by default, GET queries with variables and operationName, GET mutations rejected, 415, malformed parameters, status per media type for five kinds of document errors, field errors, subscriptions rejected without creating their publisher, 413 with declared and chunked bodies, per-adapter parser limits with the process-wide defaults unchanged, request and DataLoader through the customizer (which can't spoof the request key), the user of a ChallengeAuthenticator reaching a data fetcher, frozen configuration.

The community graphql-http audit suite (JavaScript) is not wired in; the cases above follow its server rules.

🤖 Generated with Claude Code

@jlouvel

jlouvel commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Added a test-only commit: the chunked oversized-body case now accepts either the 413 or a client-side communication error. The server answers before reading the whole body and closes the connection, so the client may fail while still writing (seen once while running G3).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The customizer can change the selected operation after transport validation, allowing mutations over GET and subscriptions to bypass execution restrictions.

4 open issues
What changed in this PR

Completes the GraphQL server HTTP binding on top of the G0 extension, adding request controls and Restlet context integration for G1.

Changes:

  • Adds opt-in GET queries, operation restrictions, parameter validation, and response-media-specific status codes.
  • Introduces request-size limits, per-adapter parser options, and execution customization with protected request context keys.
  • Adds binding tests and shared schema fixtures, and updates the changelog.
File Description
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​TickPublisher.java Adds a publisher fixture to detect subscription execution.
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​GraphqlTestSchemas.java Adds context, DataLoader, and subscription test fields.
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​GraphqlServerBindingTestCase.java Tests HTTP rules, limits, authentication context, and configuration freezing.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​RestletGraphql.java Defines public request and client-info context keys.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​GraphqlServerAdapter.java Implements the expanded HTTP binding and execution controls.
changes.md Documents the new server-binding capabilities.

Review effort: Balanced

jlouvel and others added 3 commits October 30, 2026 15:38
- GET for queries only, when enabled (off by default); other operations
  over GET get 405 with Allow: POST.
- 415 for non-JSON POST bodies; 400 for batches and malformed
  parameters; 413 above maxRequestBytes (1 MiB), checked on the
  declared size and while reading.
- Documents are parsed with the adapter's ParserOptions before
  execution to select the operation. Parse errors, validation errors
  and missing or unknown operations get 400 with
  application/graphql-response+json and 200 with application/json.
  Execution results are 200, even with field errors.
- ParserOptions are passed per execution through the GraphQLContext,
  never through graphql-java's process-wide defaults.
- RestletGraphql.REQUEST and CLIENT_INFO context keys, set after the
  execution input customizer so they can't be replaced.
- Subscriptions are rejected without being executed until G3.

Refs #1583

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
The server answers 413 before reading the whole chunked body and closes
the connection, so the client may fail while still writing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
Select the operation once the execution input is customized, so that a
customizer can't run a mutation over GET or a subscription. Treat an
empty operationName as omitted, advertise GET in Allow when enabled and
add Vary: Accept to negotiated responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jlouvel
jlouvel force-pushed the feat/graphql-g1-server branch from 06249a5 to c4b92ab Compare October 30, 2026 19:46
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants