Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Masking for Yoga hooks & Respect result processor in case of an error #1521

Merged
merged 4 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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