Skip to content

Commit 96d622b

Browse files
authored
Merge pull request #2 from ef-eng/fix-express-graphql-pretty-print-bug
fix: fixing bug where responses don't get rewritten with pretty-printing
2 parents 842552b + 61056cc commit 96d622b

File tree

3 files changed

+148
-5
lines changed

3 files changed

+148
-5
lines changed

src/index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ interface RewriterMiddlewareOpts {
1010
const rewriteResJson = (res: Response) => {
1111
const originalJsonFunc = res.json.bind(res);
1212
res.json = function(body: any) {
13-
if (!this.req || !this.req._rewriteHandler) return originalJsonFunc(body);
13+
if (!this.req || !this.req._rewriteHandler || this._isRewritten) return originalJsonFunc(body);
14+
this._isRewritten = true;
1415
const rewriteHandler = this.req._rewriteHandler;
1516
if (typeof body === 'object' && !(body instanceof Buffer) && body.data) {
1617
const newResponseData = rewriteHandler.rewriteResponse(body.data);
@@ -21,6 +22,43 @@ const rewriteResJson = (res: Response) => {
2122
};
2223
};
2324

25+
const rewriteResRaw = (res: Response) => {
26+
const originalEndFunc = res.end.bind(res);
27+
res.end = function(body: any) {
28+
if (!this.req || !this.req._rewriteHandler || this._isRewritten || this.headersSent) {
29+
return originalEndFunc(body);
30+
}
31+
this._isRewritten = true;
32+
const existingHeaders = this.getHeaders();
33+
const isJsonContent = existingHeaders['content-type'] === 'application/json; charset=utf-8';
34+
const rewriteHandler = this.req._rewriteHandler;
35+
if (isJsonContent && body instanceof Buffer) {
36+
try {
37+
const bodyJson = JSON.parse(body.toString('utf8'));
38+
if (bodyJson && bodyJson.data) {
39+
const newResponseData = rewriteHandler.rewriteResponse(bodyJson.data);
40+
const newResBodyJson = { ...bodyJson, data: newResponseData };
41+
// assume this was pretty-printed if we're here and not in the res.json handler
42+
const newResBodyStr = JSON.stringify(newResBodyJson, null, 2);
43+
const newResChunk = Buffer.from(newResBodyStr, 'utf8');
44+
this.setHeader('Content-Length', String(newResChunk.length));
45+
return originalEndFunc(newResChunk);
46+
}
47+
} catch (err) {
48+
// if we can't decode the response as json, just forward it along
49+
return originalEndFunc(body);
50+
}
51+
}
52+
return originalEndFunc(body);
53+
};
54+
};
55+
56+
const rewriteRes = (res: Response) => {
57+
rewriteResJson(res);
58+
// if res.json isn't available, or pretty-printing is enabled, express-graphql uses raw res.end()
59+
rewriteResRaw(res);
60+
};
61+
2462
const graphqlRewriterMiddleware = ({
2563
rewriters,
2664
ignoreParsingErrors = true
@@ -46,7 +84,7 @@ const graphqlRewriterMiddleware = ({
4684
req.body = newBody;
4785
}
4886
req._rewriteHandler = rewriteHandler;
49-
rewriteResJson(res);
87+
rewriteRes(res);
5088
} catch (err) {
5189
if (!ignoreParsingErrors) return next(err);
5290
}

src/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ declare namespace Express {
33
interface Request {
44
_rewriteHandler?: import('graphql-query-rewriter').RewriteHandler;
55
}
6+
7+
interface Response {
8+
_isRewritten?: boolean;
9+
}
610
}

test/index.test.ts

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('middleware test', () => {
166166
});
167167
});
168168

169-
const setupMutationApp = () => {
169+
const setupMutationApp = (extraExpressGraphqlOpts: any = {}) => {
170170
const app = express();
171171

172172
app.use(
@@ -190,7 +190,8 @@ describe('middleware test', () => {
190190
'/graphql',
191191
graphqlHTTP({
192192
schema,
193-
rootValue
193+
rootValue,
194+
...extraExpressGraphqlOpts
194195
})
195196
);
196197
return app;
@@ -362,7 +363,7 @@ describe('middleware test', () => {
362363
expect(invalidQueryRes.body.data).toEqual({ random: true });
363364
});
364365

365-
it('ignores invalid json responses', async () => {
366+
it('ignores invalid json responses sent via response.json()', async () => {
366367
const app = express();
367368

368369
app.use(
@@ -401,6 +402,106 @@ describe('middleware test', () => {
401402
expect(invalidQueryRes.body).toEqual('jimmy');
402403
});
403404

405+
it('ignores invalid json responses sent via response.end()', async () => {
406+
const app = express();
407+
408+
app.use(
409+
'/graphql',
410+
graphqlRewriterMiddleware({
411+
rewriters: [
412+
new FieldArgsToInputTypeRewriter({
413+
fieldName: 'makePokemon',
414+
argNames: ['name']
415+
}),
416+
new NestFieldOutputsRewriter({
417+
fieldName: 'makePokemon',
418+
newOutputName: 'pokemon',
419+
outputsToNest: ['id', 'name']
420+
})
421+
]
422+
})
423+
);
424+
425+
app.use('/graphql', (req, res) => {
426+
const messedUpRes = Buffer.from('jisdhfiods{{{{', 'utf8');
427+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
428+
res.setHeader('Content-Length', String(messedUpRes.length));
429+
res.end(messedUpRes);
430+
});
431+
432+
const deprecatedQuery = `
433+
mutation {
434+
makePokemon(name: "Squirtle") {
435+
id
436+
name
437+
}
438+
}
439+
`;
440+
441+
const invalidQueryRes = await request(app)
442+
.post('/graphql')
443+
.send({ query: deprecatedQuery })
444+
// disable supertest json parsing
445+
.buffer(true)
446+
.parse((res, cb) => {
447+
let data = Buffer.from('');
448+
res.on('data', chunk => {
449+
data = Buffer.concat([data, chunk]);
450+
});
451+
res.on('end', () => {
452+
cb(null, data.toString());
453+
});
454+
});
455+
456+
expect(invalidQueryRes.body).toEqual('jisdhfiods{{{{');
457+
});
458+
459+
it('is able to rewriter responses with pretty printing enabled on express-graphql', async () => {
460+
const app = setupMutationApp({ pretty: true });
461+
// in the past, we didn't use input or output types correctly
462+
// so we need to rewrite the query to this old query will work still
463+
const deprecatedQuery = `
464+
mutation makePokemonWithWrongType($name: String!) {
465+
makePokemon(name: $name) {
466+
id
467+
name
468+
}
469+
}
470+
`;
471+
472+
const deprecatedRes = await request(app)
473+
.post('/graphql')
474+
.send({ query: deprecatedQuery, variables: { name: 'Squirtle' } });
475+
expect(deprecatedRes.body.errors).toBe(undefined);
476+
expect(deprecatedRes.body.data.makePokemon).toEqual({
477+
id: '17',
478+
name: 'Squirtle'
479+
});
480+
481+
// the new version of the query should still work with no problem though
482+
const newQuery = `
483+
mutation makePokemon($input: MakePokemonInput!) {
484+
makePokemon(input: $input) {
485+
pokemon {
486+
id
487+
name
488+
}
489+
}
490+
}
491+
`;
492+
493+
const newRes = await request(app)
494+
.post('/graphql')
495+
.send({ query: newQuery, variables: { input: { name: 'Squirtle' } } });
496+
expect(newRes.body.errors).toBe(undefined);
497+
expect(newRes.body.data.makePokemon).toEqual({
498+
pokemon: {
499+
id: '17',
500+
name: 'Squirtle'
501+
}
502+
});
503+
});
504+
404505
it('throws on invalid graphql if ignoreParsingErrors === false', async () => {
405506
const app = express();
406507

0 commit comments

Comments
 (0)