Skip to content

Commit e151191

Browse files
authoredAug 27, 2019
Validate variables at the gateway level (#3213)
Previously, the gateway depended on downstream services to report any errors with graphql execution. In the case of invalid variables, it's not necessary to make any fetches or even build a query plan since we can perform that validation when the request is first received. Validating at the gateway also guarantees that we surface invalid queries to users, independent of how the downstream services are implemented and what kind of validation _they_ perform on incoming requests.
1 parent a0290c2 commit e151191

File tree

2 files changed

+78
-5
lines changed

2 files changed

+78
-5
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import gql from 'graphql-tag';
2+
import { ApolloGateway } from '../../';
3+
4+
import * as accounts from '../__fixtures__/schemas/accounts';
5+
import * as books from '../__fixtures__/schemas/books';
6+
import * as inventory from '../__fixtures__/schemas/inventory';
7+
import * as product from '../__fixtures__/schemas/product';
8+
import * as reviews from '../__fixtures__/schemas/reviews';
9+
10+
describe('ApolloGateway executor', () => {
11+
it('validates requests prior to execution', async () => {
12+
const gateway = new ApolloGateway({
13+
localServiceList: [accounts, books, inventory, product, reviews],
14+
});
15+
16+
const { executor } = await gateway.load();
17+
18+
const { errors } = await executor({
19+
document: gql`
20+
query InvalidVariables($first: Int!) {
21+
topReviews(first: $first) {
22+
body
23+
}
24+
}
25+
`,
26+
request: {
27+
variables: { first: '3' },
28+
},
29+
queryHash: 'hashed',
30+
context: null,
31+
cache: {} as any,
32+
});
33+
34+
expect(errors![0].message).toMatch(
35+
'Variable "$first" got invalid value "3"; Expected type Int.',
36+
);
37+
});
38+
});

‎packages/apollo-gateway/src/index.ts

+40-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isIntrospectionType,
1616
GraphQLSchema,
1717
GraphQLError,
18+
VariableDefinitionNode,
1819
} from 'graphql';
1920
import { GraphQLSchemaValidationError } from 'apollo-graphql';
2021
import { composeAndValidate, ServiceDefinition } from '@apollo/federation';
@@ -38,6 +39,7 @@ import { serializeQueryPlan, QueryPlan, OperationContext } from './QueryPlan';
3839
import { GraphQLDataSource } from './datasources/types';
3940
import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource';
4041
import { HeadersInit } from 'node-fetch';
42+
import { getVariableValues } from 'graphql/execution/values';
4143

4244
export type ServiceEndpointDefinition = Pick<ServiceDefinition, 'name' | 'url'>;
4345

@@ -132,6 +134,11 @@ export type UpdateServiceDefinitions = (
132134

133135
type Await<T> = T extends Promise<infer U> ? U : T;
134136

137+
type RequestContext<TContext> = WithRequired<
138+
GraphQLRequestContext<TContext>,
139+
'document' | 'queryHash'
140+
>;
141+
135142
export class ApolloGateway implements GraphQLService {
136143
public schema?: GraphQLSchema;
137144
protected serviceMap: ServiceMap = Object.create(null);
@@ -439,10 +446,7 @@ export class ApolloGateway implements GraphQLService {
439446
// are unlikely to show up as GraphQLErrors. Do we need to use
440447
// formatApolloErrors or something?
441448
public executor = async <TContext>(
442-
requestContext: WithRequired<
443-
GraphQLRequestContext<TContext>,
444-
'document' | 'operation' | 'queryHash'
445-
>,
449+
requestContext: RequestContext<TContext>,
446450
): Promise<GraphQLExecutionResult> => {
447451
const { request, document, queryHash } = requestContext;
448452
const queryPlanStoreKey = queryHash + (request.operationName || '');
@@ -451,7 +455,19 @@ export class ApolloGateway implements GraphQLService {
451455
document,
452456
request.operationName,
453457
);
454-
let queryPlan;
458+
459+
// No need to build a query plan if we know the request is invalid beforehand
460+
// In the future, this should be controlled by the requestPipeline
461+
const validationErrors = this.validateIncomingRequest(
462+
requestContext,
463+
operationContext,
464+
);
465+
466+
if (validationErrors.length > 0) {
467+
return { errors: validationErrors };
468+
}
469+
470+
let queryPlan: QueryPlan | undefined;
455471
if (this.queryPlanStore) {
456472
queryPlan = await this.queryPlanStore.get(queryPlanStoreKey);
457473
}
@@ -510,6 +526,25 @@ export class ApolloGateway implements GraphQLService {
510526
return response;
511527
};
512528

529+
protected validateIncomingRequest<TContext>(
530+
requestContext: RequestContext<TContext>,
531+
operationContext: OperationContext,
532+
) {
533+
// casting out of `readonly`
534+
const variableDefinitions = operationContext.operation
535+
.variableDefinitions as VariableDefinitionNode[] | undefined;
536+
537+
if (!variableDefinitions) return [];
538+
539+
const { errors } = getVariableValues(
540+
operationContext.schema,
541+
variableDefinitions,
542+
requestContext.request.variables!,
543+
);
544+
545+
return errors || [];
546+
}
547+
513548
private initializeQueryPlanStore(): void {
514549
this.queryPlanStore = new InMemoryLRUCache<QueryPlan>({
515550
// Create ~about~ a 30MiB InMemoryLRUCache. This is less than precise

0 commit comments

Comments
 (0)
Please sign in to comment.