GraphQL Batching
Insgestamt ist GraphQL für mich meist schwierig verständlich. Schon alleine über Design von REST-Schnittstellen lässt sich lange streiten. Bei GraphQL scheint der grösste Wildwuchs zu herrschen.
Für ein Experiment, bei dem ich den fxa-content-server anbinden wollte, wurde ich mit einer sehr speziellen Art GraphQL Schnittstelle konfrontiert:
[
{
"operationName": "GetInitialMetricsState",
"variables": {
},
"query": "query GetInitialMetricsState {\n account {\n uid\n recoveryKey {\n exists\n estimatedSyncDeviceCount\n __typename\n }\n metricsEnabled\n emails {\n email\n isPrimary\n verified\n __typename\n }\n totp {\n exists\n verified\n __typename\n }\n __typename\n }\n}"
},
{
"operationName": "GetSessionIsValid",
"variables": {
"sessionToken": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
},
"query": "query GetSessionIsValid($sessionToken: String!) {\n isValidToken(sessionToken: $sessionToken)\n}"
}
]
Man beachte, dass die Queries in einem Array gepackt wurden. - Das nennt sich Query Batching. Beschrieben in Apollo GraphQL.
Reverse Proxy Splitter
Die Idee war nun, dass ich dieses Array parse und dann weiterleite als mehrere Requests. Doch das stellt sich als schwieriger dar, als man denken könnte.
Array to String
Also parsen wir die Queries in einen String. Doch auf Generics könnnen keine Annotationen wie @JsonRawValue gepackt werden.
@PostMapping
public void consumeArray(@RequestBody List<String> queries)
Dafür kann man aber eine Wrapper-Klasse machen und Jackson sagen, dass wir den Inhalt als String wollen:
public class WrappedGraphQlRequest {
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public WrappedGraphQlRequest(Object obj) {
try {
request = new ObjectMapper().writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error deserializing", e);
}
}
}
Controller
Danach müssen wir dem Controller noch sagen, dass er alle /graphql-Requests abfangen soll, und mit ProxyExchange von Spring Cloud Gateway verarbeiten
@RestController
public class GraphqlSplitterController {
@PostMapping(path = {"/graphql", "/graphql/**"})
public ResponseEntity<?> reverse(ProxyExchange<byte[]> proxy, HttpServletRequest request, @RequestBody List<WrappedGraphQlRequest> requests) {
var results = requests.stream()
.map(r -> new String(requireNonNull(proxy.body(r.body()).uri("http://localhost:" + request.getServerPort() + "/api/graphql" + proxy.path("/graphql"))
.post().getBody())))
.collect(Collectors.joining(","));
return ResponseEntity.ok(String.format("[%s]", results));
}
}
Achtung, dafür müssen wir aber von “Spring for GraphQL” den Pfad /graphql freimachen:
spring.graphql.path=/api/graphql
So können wir nun alle Batched Requests in mehrere Requests splitten. Einfach nur ein Hack 👷