Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 53cd63f

Browse files
committed
Merge pull request #18 from graphql/safe-get
Disable GET for non-query operations
2 parents 412470c + 59f6d1f commit 53cd63f

File tree

5 files changed

+241
-32
lines changed

5 files changed

+241
-32
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ If not found in the query-string, it will look in the POST request body.
6767
If a previous middleware has already parsed the POST body, the `request.body`
6868
value will be used. Use [`multer`][] or a similar middleware to add support
6969
for `multipart/form-data` content, which may be useful for GraphQL mutations
70-
involving uploading files. See an [example using multer](https://github.com/graphql/express-graphql/blob/master/src/__tests__/http-test.js#L462).
70+
involving uploading files. See an [example using multer](https://github.com/graphql/express-graphql/blob/master/src/__tests__/http-test.js#L603).
7171

7272
If the POST body has not yet been parsed, graphql-express will interpret it
7373
depending on the provided *Content-Type* header.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"raw-body": "~2.1.2"
5252
},
5353
"peerDependencies": {
54-
"graphql": "~0.4.2"
54+
"graphql": "~0.4.5"
5555
},
5656
"devDependencies": {
5757
"babel": "5.8.21",
@@ -66,7 +66,7 @@
6666
"express": "4.13.3",
6767
"express3": "*",
6868
"flow-bin": "0.14.0",
69-
"graphql": "0.4.2",
69+
"graphql": "0.4.5",
7070
"isparta": "3.0.3",
7171
"mocha": "2.2.5",
7272
"multer": "1.0.3",

src/__tests__/http-test.js

Lines changed: 180 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,33 @@ import {
2828
} from 'graphql';
2929
import graphqlHTTP from '../';
3030

31+
var QueryRootType = new GraphQLObjectType({
32+
name: 'QueryRoot',
33+
fields: {
34+
test: {
35+
type: GraphQLString,
36+
args: {
37+
who: {
38+
type: GraphQLString
39+
}
40+
},
41+
resolve: (root, { who }) => 'Hello ' + (who || 'World')
42+
},
43+
thrower: {
44+
type: new GraphQLNonNull(GraphQLString),
45+
resolve: () => { throw new Error('Throws!'); }
46+
}
47+
}
48+
});
3149

3250
var TestSchema = new GraphQLSchema({
33-
query: new GraphQLObjectType({
34-
name: 'Root',
51+
query: QueryRootType,
52+
mutation: new GraphQLObjectType({
53+
name: 'MutationRoot',
3554
fields: {
36-
test: {
37-
type: GraphQLString,
38-
args: {
39-
who: {
40-
type: GraphQLString
41-
}
42-
},
43-
resolve: (root, { who }) => 'Hello ' + (who || 'World')
44-
},
45-
thrower: {
46-
type: new GraphQLNonNull(GraphQLString),
47-
resolve: () => { throw new Error('Throws!'); }
55+
writeTest: {
56+
type: QueryRootType,
57+
resolve: () => ({})
4858
}
4959
}
5060
})
@@ -168,7 +178,7 @@ describe('test harness', () => {
168178
query helloYou { test(who: "You"), ...shared }
169179
query helloWorld { test(who: "World"), ...shared }
170180
query helloDolly { test(who: "Dolly"), ...shared }
171-
fragment shared on Root {
181+
fragment shared on QueryRoot {
172182
shared: test(who: "Everyone")
173183
}
174184
`,
@@ -183,6 +193,122 @@ describe('test harness', () => {
183193
});
184194
});
185195

196+
it('Reports validation errors', async () => {
197+
var app = express();
198+
199+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
200+
201+
var error = await catchError(
202+
request(app)
203+
.get(urlString({
204+
query: `{ test, unknownOne, unknownTwo }`
205+
}))
206+
);
207+
208+
expect(error.response.status).to.equal(400);
209+
expect(JSON.parse(error.response.text)).to.deep.equal({
210+
errors: [
211+
{
212+
message: 'Cannot query field "unknownOne" on "QueryRoot".',
213+
locations: [ { line: 1, column: 9 } ]
214+
},
215+
{
216+
message: 'Cannot query field "unknownTwo" on "QueryRoot".',
217+
locations: [ { line: 1, column: 21 } ]
218+
}
219+
]
220+
});
221+
});
222+
223+
it('Errors when missing operation name', async () => {
224+
var app = express();
225+
226+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
227+
228+
var error = await catchError(
229+
request(app)
230+
.get(urlString({
231+
query: `
232+
query TestQuery { test }
233+
mutation TestMutation { writeTest { test } }
234+
`
235+
}))
236+
);
237+
238+
expect(error.response.status).to.equal(400);
239+
expect(JSON.parse(error.response.text)).to.deep.equal({
240+
errors: [
241+
{ message: 'Must provide operation name if query contains multiple operations.' }
242+
]
243+
});
244+
});
245+
246+
it('Errors when sending a mutation via GET', async () => {
247+
var app = express();
248+
249+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
250+
251+
var error = await catchError(
252+
request(app)
253+
.get(urlString({
254+
query: 'mutation TestMutation { writeTest { test } }'
255+
}))
256+
);
257+
258+
expect(error.response.status).to.equal(405);
259+
expect(JSON.parse(error.response.text)).to.deep.equal({
260+
errors: [
261+
{ message: 'Can only perform a mutation operation from a POST request.' }
262+
]
263+
});
264+
});
265+
266+
it('Errors when selecting a mutation within a GET', async () => {
267+
var app = express();
268+
269+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
270+
271+
var error = await catchError(
272+
request(app)
273+
.get(urlString({
274+
operationName: 'TestMutation',
275+
query: `
276+
query TestQuery { test }
277+
mutation TestMutation { writeTest { test } }
278+
`
279+
}))
280+
);
281+
282+
expect(error.response.status).to.equal(405);
283+
expect(JSON.parse(error.response.text)).to.deep.equal({
284+
errors: [
285+
{ message: 'Can only perform a mutation operation from a POST request.' }
286+
]
287+
});
288+
});
289+
290+
it('Allows a mutation to exist within a GET', async () => {
291+
var app = express();
292+
293+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
294+
295+
var response = await request(app)
296+
.get(urlString({
297+
operationName: 'TestQuery',
298+
query: `
299+
mutation TestMutation { writeTest { test } }
300+
query TestQuery { test }
301+
`
302+
}));
303+
304+
expect(response.status).to.equal(200);
305+
expect(JSON.parse(response.text)).to.deep.equal({
306+
data: {
307+
test: 'Hello World'
308+
}
309+
});
310+
});
311+
186312
});
187313

188314
describe('POST functionality', () => {
@@ -201,6 +327,21 @@ describe('test harness', () => {
201327
);
202328
});
203329

330+
it('Allows sending a mutation via POST', async () => {
331+
var app = express();
332+
333+
app.use(urlString(), graphqlHTTP({ schema: TestSchema }));
334+
335+
var response = await request(app)
336+
.post(urlString())
337+
.send({ query: 'mutation TestMutation { writeTest { test } }' });
338+
339+
expect(response.status).to.equal(200);
340+
expect(response.text).to.equal(
341+
'{"data":{"writeTest":{"test":"Hello World"}}}'
342+
);
343+
});
344+
204345
it('allows POST with url encoding', async () => {
205346
var app = express();
206347

@@ -345,7 +486,7 @@ describe('test harness', () => {
345486
query helloYou { test(who: "You"), ...shared }
346487
query helloWorld { test(who: "World"), ...shared }
347488
query helloDolly { test(who: "Dolly"), ...shared }
348-
fragment shared on Root {
489+
fragment shared on QueryRoot {
349490
shared: test(who: "Everyone")
350491
}
351492
`,
@@ -376,7 +517,7 @@ describe('test harness', () => {
376517
query helloYou { test(who: "You"), ...shared }
377518
query helloWorld { test(who: "World"), ...shared }
378519
query helloDolly { test(who: "Dolly"), ...shared }
379-
fragment shared on Root {
520+
fragment shared on QueryRoot {
380521
shared: test(who: "Everyone")
381522
}
382523
`);
@@ -978,6 +1119,28 @@ describe('test harness', () => {
9781119
expect(response.text).to.include('response: null');
9791120
});
9801121

1122+
it('GraphiQL accepts a mutation query - does not execute it', async () => {
1123+
var app = express();
1124+
1125+
app.use(urlString(), graphqlHTTP({
1126+
schema: TestSchema,
1127+
graphiql: true
1128+
}));
1129+
1130+
var response = await request(app)
1131+
.get(urlString({
1132+
query: 'mutation TestMutation { writeTest { test } }'
1133+
}))
1134+
.set('Accept', 'text/html');
1135+
1136+
expect(response.status).to.equal(200);
1137+
expect(response.type).to.equal('text/html');
1138+
expect(response.text).to.include(
1139+
'query: "mutation TestMutation { writeTest { test } }"'
1140+
);
1141+
expect(response.text).to.include('response: null');
1142+
});
1143+
9811144
it('returns HTML if preferred', async () => {
9821145
var app = express();
9831146

src/index.js

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
*/
1010

1111
import httpError from 'http-errors';
12-
import { graphql } from 'graphql';
1312
import { formatError } from 'graphql/error';
13+
import { execute } from 'graphql/execution';
14+
import { parse, Source } from 'graphql/language';
15+
import { validate } from 'graphql/validation';
16+
import { getOperationAST } from 'graphql/utilities/getOperationAST';
1417
import { parseBody } from './parseBody';
1518
import { renderGraphiQL } from './renderGraphiQL';
1619
import type { Request, Response } from 'express';
@@ -68,11 +71,11 @@ export default function graphqlHTTP(options: Options): Middleware {
6871
}
6972

7073
// Parse the Request body.
71-
parseBody(request, (error, data = {}) => {
74+
parseBody(request, (parseError, data = {}) => {
7275

7376
// Format any request errors the same as GraphQL errors.
74-
if (error) {
75-
return sendError(response, error, pretty);
77+
if (parseError) {
78+
return sendError(response, parseError, pretty);
7679
}
7780

7881
// Get GraphQL params from the request and POST body data.
@@ -90,13 +93,56 @@ export default function graphqlHTTP(options: Options): Middleware {
9093
}
9194

9295
// Run GraphQL query.
93-
graphql(
94-
schema,
95-
query,
96-
rootValue,
97-
variables,
98-
operationName
99-
).then(result => {
96+
new Promise(resolve => {
97+
var source = new Source(query, 'GraphQL request');
98+
var documentAST = parse(source);
99+
var validationErrors = validate(schema, documentAST);
100+
if (validationErrors.length > 0) {
101+
resolve({ errors: validationErrors });
102+
} else {
103+
104+
// Only query operations are allowed on GET requests.
105+
if (request.method === 'GET') {
106+
// Determine if this GET request will perform a non-query.
107+
var operationAST = getOperationAST(documentAST, operationName);
108+
if (operationAST && operationAST.operation !== 'query') {
109+
// If GraphiQL can be shown, do not perform this query, but
110+
// provide it to GraphiQL so that the requester may perform it
111+
// themselves if desired.
112+
if (graphiql && canDisplayGraphiQL(request, data)) {
113+
return response
114+
.set('Content-Type', 'text/html')
115+
.send(renderGraphiQL({ query, variables }));
116+
}
117+
118+
// Otherwise, report a 405 Method Not Allowed error.
119+
response.set('Allow', 'POST');
120+
return sendError(
121+
response,
122+
httpError(
123+
405,
124+
`Can only perform a ${operationAST.operation} operation ` +
125+
`from a POST request.`
126+
),
127+
pretty
128+
);
129+
}
130+
}
131+
132+
// Perform the execution.
133+
resolve(
134+
execute(
135+
schema,
136+
documentAST,
137+
rootValue,
138+
variables,
139+
operationName
140+
)
141+
);
142+
}
143+
}).catch(error => {
144+
return { errors: [ error ] };
145+
}).then(result => {
100146

101147
// Format any encountered errors.
102148
if (result.errors) {

src/renderGraphiQL.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11-
type GraphiQLData = { query: ?string, variables: ?Object, result: Object };
11+
type GraphiQLData = { query: ?string, variables: ?Object, result?: Object };
1212

1313
// Current latest version of GraphiQL.
1414
var GRAPHIQL_VERSION = '0.2.4';

0 commit comments

Comments
 (0)