diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f002de77e4..0dc47c3557b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ [PR #131](https://github.com/apollostack/apollo-server/pull/131) * Improve logging function. Issue #79. ([@nnance](https://github.com/nnance)) in [PR #136](https://github.com/apollostack/apollo-server/pull/136) +* Output stack trace for errors in debug mode. Issue #111. ([@nnance](https://github.com/nnance)) in +[PR #137](https://github.com/apollostack/apollo-server/pull/137) ### v0.2.6 * Expose the OperationStore as part of the public API. ([@nnance](https://github.com/nnance)) diff --git a/package.json b/package.json index 674c6a269d2..ef01fbe8d8d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "mocha": "^3.0.0", "multer": "^1.1.0", "remap-istanbul": "^0.6.4", + "sinon": "^1.17.5", "supertest": "^2.0.0", "supertest-as-promised": "^4.0.0", "tslint": "^3.13.0", diff --git a/src/core/runQuery.test.ts b/src/core/runQuery.test.ts index 5b57586c8a1..da1620d949f 100644 --- a/src/core/runQuery.test.ts +++ b/src/core/runQuery.test.ts @@ -1,6 +1,5 @@ -import { - expect, -} from 'chai'; +import { expect } from 'chai'; +import { stub } from 'sinon'; import { GraphQLSchema, @@ -62,6 +61,12 @@ const QueryType = new GraphQLObjectType({ return 'it ' + (Promise).await('works'); }, }, + testError: { + type: GraphQLString, + resolve() { + throw new Error('Secret error message'); + }, + }, }, }); @@ -88,18 +93,46 @@ describe('runQuery', () => { }); }); - it('returns a syntax error if the query string contains one', () => { - const query = `query { test`; - const expected = /Syntax Error GraphQL/; - return runQuery({ - schema: Schema, - query: query, - variables: { base: 1 }, - }).then((res) => { - expect(res.data).to.be.undefined; - expect(res.errors.length).to.equal(1); - return expect(res.errors[0].message).to.match(expected); - }); + it('returns a syntax error if the query string contains one', () => { + const query = `query { test `; + const expected = /Syntax Error GraphQL/; + return runQuery({ + schema: Schema, + query: query, + variables: { base: 1 }, + }).then((res) => { + expect(res.data).to.be.undefined; + expect(res.errors.length).to.equal(1); + return expect(res.errors[0].message).to.match(expected); + }); + }); + + it('sends stack trace to error if in an error occurs and debug mode is set', () => { + const query = `query { testError }`; + const expected = /at resolveOrError/; + const logStub = stub(console, 'error'); + return runQuery({ + schema: Schema, + query: query, + debug: true, + }).then((res) => { + logStub.restore(); + expect(logStub.callCount).to.equal(1); + return expect(logStub.getCall(0).args[0]).to.match(expected); + }); + }); + + it('does not send stack trace if in an error occurs and not in debug mode', () => { + const query = `query { testError }`; + const logStub = stub(console, 'error'); + return runQuery({ + schema: Schema, + query: query, + debug: false, + }).then((res) => { + logStub.restore(); + return expect(logStub.callCount).to.equal(0); + }); }); it('returns a validation error if the query string does not pass validation', () => { diff --git a/src/core/runQuery.ts b/src/core/runQuery.ts index f16de924a92..ef5314d24c9 100644 --- a/src/core/runQuery.ts +++ b/src/core/runQuery.ts @@ -49,6 +49,7 @@ export interface QueryOptions { formatError?: Function; formatResponse?: Function; + debug?: boolean; } const resolvedPromise = Promise.resolve(); @@ -62,6 +63,8 @@ function doRunQuery(options: QueryOptions): Promise { let documentAST: Document; const logFunction = options.logFunction || function(){ return null; }; + const debugDefault = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'; + const debug = typeof options.debug !== 'undefined' ? options.debug : debugDefault; logFunction({action: LogAction.request, step: LogStep.start}); @@ -73,6 +76,10 @@ function doRunQuery(options: QueryOptions): Promise { return errors.map(options.formatError || formatError as any) as Array; } + function printStackTrace(error: Error) { + console.error(error.stack); + } + const qry = typeof options.query === 'string' ? options.query : print(options.query); logFunction({action: LogAction.request, step: LogStep.status, key: 'query', data: qry}); logFunction({action: LogAction.request, step: LogStep.status, key: 'variables', data: options.variables}); @@ -123,6 +130,9 @@ function doRunQuery(options: QueryOptions): Promise { }; if (gqlResponse.errors) { response['errors'] = format(gqlResponse.errors); + if (debug) { + gqlResponse.errors.map(printStackTrace); + } } if (options.formatResponse) { response = options.formatResponse(response, options); diff --git a/src/integrations/apolloOptions.ts b/src/integrations/apolloOptions.ts index 5400a0e27ef..f51643603a2 100644 --- a/src/integrations/apolloOptions.ts +++ b/src/integrations/apolloOptions.ts @@ -12,6 +12,7 @@ import { LogFunction } from '../core/runQuery'; * - (optional) formatParams: a function applied to the parameters of every invocation of runQuery * - (optional) validationRules: extra validation rules applied to requests * - (optional) formatResponse: a function applied to each graphQL execution result + * - (optional) debug: a boolean that will print additional debug logging if execution errors occur * */ interface ApolloOptions { @@ -23,6 +24,7 @@ interface ApolloOptions { formatParams?: Function; validationRules?: Array; formatResponse?: Function; + debug?: boolean; } export default ApolloOptions; diff --git a/src/integrations/expressApollo.ts b/src/integrations/expressApollo.ts index 1cef167ce26..3bbb5a04317 100644 --- a/src/integrations/expressApollo.ts +++ b/src/integrations/expressApollo.ts @@ -98,6 +98,7 @@ export function apolloExpress(options: ApolloOptions | ExpressApolloOptionsFunct validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 9e661dc530e..f822151ff28 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -141,6 +141,7 @@ async function processQuery(graphqlParams, optionsObject: ApolloOptions, reply) validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/src/integrations/integrations.test.ts b/src/integrations/integrations.test.ts index 33a47b85cc9..863a5627d4f 100644 --- a/src/integrations/integrations.test.ts +++ b/src/integrations/integrations.test.ts @@ -1,6 +1,5 @@ -import { - expect, -} from 'chai'; +import { expect } from 'chai'; +import { stub } from 'sinon'; import { GraphQLSchema, @@ -425,6 +424,45 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); }); + it('sends stack trace to error if debug mode is set', () => { + const expected = /at resolveOrError/; + const stackTrace = []; + const origError = console.error; + console.error = (...args) => stackTrace.push(args); + app = createApp({apolloOptions: { + schema: Schema, + debug: true, + }}); + const req = request(app) + .post('/graphql') + .send({ + query: 'query test{ testError }', + }); + return req.then((res) => { + console.error = origError; + return expect(stackTrace[0][0]).to.match(expected); + }); + }); + + it('sends stack trace to error log if debug mode is set', () => { + const logStub = stub(console, 'error'); + const expected = /at resolveOrError/; + app = createApp({apolloOptions: { + schema: Schema, + debug: true, + }}); + const req = request(app) + .post('/graphql') + .send({ + query: 'query test{ testError }', + }); + return req.then((res) => { + logStub.restore(); + expect(logStub.callCount).to.equal(1); + return expect(logStub.getCall(0).args[0]).to.match(expected); + }); + }); + it('applies additional validationRules', () => { const expected = 'AlwaysInvalidRule was really invalid!'; const AlwaysInvalidRule = function (context) { diff --git a/src/integrations/koaApollo.ts b/src/integrations/koaApollo.ts index d2825413868..386e83c5200 100644 --- a/src/integrations/koaApollo.ts +++ b/src/integrations/koaApollo.ts @@ -75,6 +75,7 @@ export function apolloKoa(options: ApolloOptions | KoaApolloOptionsFunction): Ko validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/typings.json b/typings.json index 780cd73f541..6e486b1fb3a 100644 --- a/typings.json +++ b/typings.json @@ -2,7 +2,8 @@ "dependencies": { "chai": "registry:npm/chai#3.5.0+20160723033700", "graphql": "github:nitintutlani/typed-graphql#ffe7e46e2249cc8f3824a5d15a44938f4354afe9", - "http-errors": "registry:npm/http-errors#1.4.0+20160723033700" + "http-errors": "registry:npm/http-errors#1.4.0+20160723033700", + "sinon": "registry:npm/sinon#1.16.0+20160723033700" }, "globalDependencies": { "body-parser": "registry:dt/body-parser#0.0.0+20160619023215",