Skip to content

Commit

Permalink
Error Masking for Yoga hooks & Respect result processor in case of an…
Browse files Browse the repository at this point in the history
… error (#1521)

* Respect result processors in case of an error

* Prettier Hello?

* :))

* CI :))
  • Loading branch information
ardatan committed Aug 1, 2022
1 parent c4166e8 commit e021c6f
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 155 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/algolia-integrity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
SITE_URL: https://www.graphql-yoga.com/

- name: Format
run: yarn format
run: yarn prettier

- name: Compare
run: git diff origin/${{ github.base_ref }}.. -- website/algolia-lockfile.json
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/algolia-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
SITE_URL: https://www.graphql-yoga.com/

- name: Format
run: yarn format
run: yarn prettier

- name: Compare
run: git diff website/algolia-lockfile.json
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,4 @@ jobs:
run: yarn bob check

- name: Prettier
run: yarn prettier-check
run: yarn prettier:check
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn pretty-quick
yarn lint-staged
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"**/node_modules": true,
"**/dist": true
},
"prettier.prettierPath": ".yarn/sdks/prettier/index.js",
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"prettier.prettierPath": "node_modules/prettier/index.js",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.exclude": {
"**/.git": true,
Expand Down
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"name": "graphql-yoga-monorepo",
"private": true,
"scripts": {
"format": "prettier . --write",
"pretty-quick": "pretty-quick --staged",
"prettier-check": "prettier --check .",
"prebuild": "rimraf packages/*/dist",
"check": "yarn workspaces run check",
"build": "yarn workspace @graphql-yoga/graphiql run build && yarn workspace @graphql-yoga/render-graphiql run build && yarn workspace graphql-yoga run generate-graphiql-html && bob build",
Expand All @@ -14,8 +11,19 @@
"start:docs": "yarn workspace website dev",
"postinstall": "patch-package && husky install",
"fix-bin": "node scripts/fix-bin.js",
"lint": "eslint --ignore-path .gitignore --ext ts,js,tsx,jsx .",
"prettier": "prettier --ignore-path .prettierignore --write --list-different .",
"prettier:check": "prettier --ignore-path .prettierignore --check .",
"build-website": "yarn build && cd website && yarn build"
},
"lint-staged": {
"packages/**/src/**/*.{ts,tsx}": [
"eslint --fix"
],
"**/*.{ts,tsx,graphql,yml,md,mdx,json}": [
"prettier --write"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/dotansimha/graphql-yoga.git"
Expand Down Expand Up @@ -66,12 +74,12 @@
"babel-plugin-transform-typescript-metadata": "0.3.2",
"bob-the-bundler": "3.0.5",
"eslint": "^8.15.0",
"graphql": "^16.1.0",
"graphql": "^16.5.0",
"husky": "^8.0.0",
"jest": "^28.0.0",
"patch-package": "^6.4.7",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.2",
"lint-staged": "^13.0.3",
"rimraf": "^3.0.2",
"supertest": "^6.1.6",
"ts-jest": "^28.0.0",
Expand Down
141 changes: 137 additions & 4 deletions packages/graphql-yoga/__tests__/node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
File,
FormData,
} from '@whatwg-node/fetch'
import { ExecutionResult } from '@graphql-tools/utils'
import { createGraphQLError, ExecutionResult } from '@graphql-tools/utils'
import { ServerResponse, Server } from 'http'

describe('Disable Introspection with plugin', () => {
Expand Down Expand Up @@ -54,7 +54,6 @@ describe('Disable Introspection with plugin', () => {
expect(response.body.data).toBeNull()
expect(response.body.errors![0]).toMatchInlineSnapshot(`
Object {
"extensions": Object {},
"locations": Array [
Object {
"column": 7,
Expand Down Expand Up @@ -198,6 +197,28 @@ describe('Masked Error Option', () => {
},
})
})
it('should mask errors from onRequestParse(HTTP hook) with 500', async () => {
const yoga = createYoga({
schema,
plugins: [
{
onRequestParse() {
throw new Error('Some random error!')
},
},
],
logging: false,
})

const response = await request(yoga).post('/graphql').send({
query: '{ hi hello }',
})

expect(response.statusCode).toBe(500)

const body = JSON.parse(response.text)
expect(body.errors?.[0]?.message).toBe('Unexpected error.')
})
})

describe('Context error', () => {
Expand Down Expand Up @@ -305,6 +326,120 @@ describe('Context error', () => {
})
})

describe('HTTP Error Extensions', () => {
it('should respect the status code and headers given with the thrown error during the execution', async () => {
const yoga = createYoga({
schema: {
typeDefs: /* GraphQL */ `
type Query {
hello: String
}
`,
resolvers: {
Query: {
hello() {
throw new GraphQLError('Some random error!', {
extensions: {
http: {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer',
},
},
},
})
},
},
},
},
logging: false,
})

const response = await request(yoga).post('/graphql').send({
query: '{ hello }',
})

expect(response.statusCode).toBe(401)
expect(response.headers['www-authenticate']).toBe('Bearer')
})
it('should respect the highest status code if there are many errors thrown with different HTTP status codes', async () => {
const yoga = createYoga({
schema: {
typeDefs: /* GraphQL */ `
type Query {
secret: String
inaccessibleOnDb: String
}
`,
resolvers: {
Query: {
secret: () => {
throw createGraphQLError('You cannot access this secret', {
extensions: {
http: {
status: 401,
},
},
})
},
inaccessibleOnDb: () => {
throw createGraphQLError('DB is not available', {
extensions: {
http: {
status: 503,
},
},
})
},
},
},
},
})

const response = await request(yoga).post('/graphql').send({
query: '{ secret inaccessibleOnDb }',
})

expect(response.statusCode).toBe(503)

const body = JSON.parse(response.text)
expect(body).toMatchInlineSnapshot(`
Object {
"data": Object {
"inaccessibleOnDb": null,
"secret": null,
},
"errors": Array [
Object {
"locations": Array [
Object {
"column": 3,
"line": 1,
},
],
"message": "You cannot access this secret",
"path": Array [
"secret",
],
},
Object {
"locations": Array [
Object {
"column": 10,
"line": 1,
},
],
"message": "DB is not available",
"path": Array [
"inaccessibleOnDb",
],
},
],
}
`)
})
})

it('parse error is sent to clients', async () => {
const yoga = createYoga({
logging: false,
Expand All @@ -328,7 +463,6 @@ it('parse error is sent to clients', async () => {
"data": null,
"errors": Array [
Object {
"extensions": Object {},
"locations": Array [
Object {
"column": 10,
Expand Down Expand Up @@ -368,7 +502,6 @@ it('validation error is sent to clients', async () => {
"data": null,
"errors": Array [
Object {
"extensions": Object {},
"locations": Array [
Object {
"column": 2,
Expand Down
69 changes: 58 additions & 11 deletions packages/graphql-yoga/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { UseMaskedErrorsOpts } from '@envelop/core'
import { createGraphQLError } from '@graphql-tools/utils'
import { GraphQLError } from 'graphql'
import { GraphQLError, GraphQLHTTPErrorExtensions } from 'graphql'
import { ResultProcessorInput } from './plugins/types'
import { YogaMaskedErrorOpts } from './types'

declare module 'graphql' {
interface GraphQLHTTPErrorExtensions {
Expand All @@ -21,22 +24,66 @@ function hasToString(obj: any): obj is { toString(): string } {

export function handleError(
error: unknown,
maskedErrorsOpts: YogaMaskedErrorOpts | null,
errors: GraphQLError[] = [],
): GraphQLError[] {
if (isAggregateError(error)) {
for (const singleError of error.errors) {
errors.push(...handleError(singleError))
errors.push(...handleError(singleError, maskedErrorsOpts))
}
} else if (error instanceof GraphQLError) {
errors.push(error)
} else if (error instanceof Error) {
errors.push(createGraphQLError(error.message))
} else if (typeof error === 'string') {
errors.push(createGraphQLError(error))
} else if (hasToString(error)) {
errors.push(createGraphQLError(error.toString()))
} else if (maskedErrorsOpts) {
const maskedError = maskedErrorsOpts.formatError(
error,
maskedErrorsOpts.errorMessage,
maskedErrorsOpts.isDev,
)
errors.push(maskedError)
} else {
errors.push(createGraphQLError('Unexpected error!'))
if (error instanceof GraphQLError) {
errors.push(error)
}
if (error instanceof Error) {
errors.push(createGraphQLError(error.message))
} else if (typeof error === 'string') {
errors.push(createGraphQLError(error))
} else if (hasToString(error)) {
errors.push(createGraphQLError(error.toString()))
} else {
errors.push(createGraphQLError('Unexpected error!'))
}
}
return errors
}

export function getResponseInitByRespectingErrors(
result: ResultProcessorInput,
headers: Record<string, string> = {},
) {
let status: number | undefined

if ('errors' in result && result.errors?.length) {
for (const error of result.errors) {
if (error.extensions?.http) {
if (
error.extensions.http.status &&
(!status || error.extensions.http.status > status)
) {
status = error.extensions.http.status
}
if (error.extensions.http.headers) {
Object.assign(headers, error.extensions.http.headers)
}
// Remove http extensions from the final response
delete error.extensions.http
//TODO: avoid slow "delete"
}
}
} else {
status = 200
}

return {
status,
headers,
}
}
8 changes: 1 addition & 7 deletions packages/graphql-yoga/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,7 @@ export interface YogaLogger {
error: (...args: any[]) => void
}

const isDebug = () =>
typeof process === 'object'
? process.env.DEBUG
: // @ts-expect-error
typeof DEBUG !== 'undefined'
? true
: false
const isDebug = () => !!globalThis.process?.env?.DEBUG

const prefix = [LEVEL_COLOR.title, `🧘 Yoga -`, LEVEL_COLOR.reset]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isAsyncIterable } from '@envelop/core'
import { ExecutionResult } from 'graphql'
import { getResponseInitByRespectingErrors } from '../../error.js'
import { FetchAPI } from '../../types.js'
import { ResultProcessorInput } from '../types.js'

Expand All @@ -12,15 +13,13 @@ export function processMultipartResult(
result: ResultProcessorInput,
fetchAPI: FetchAPI,
): Response {
const headersInit: HeadersInit = {
const headersInit = {
Connection: 'keep-alive',
'Content-Type': 'multipart/mixed; boundary="-"',
'Transfer-Encoding': 'chunked',
}
const responseInit: ResponseInit = {
headers: headersInit,
status: 200,
}

const responseInit = getResponseInitByRespectingErrors(result, headersInit)

let iterator: AsyncIterator<ExecutionResult<any>>

Expand Down
Loading

0 comments on commit e021c6f

Please sign in to comment.