From 97c40698f76b7f995cd404fbae72529419a4a9d6 Mon Sep 17 00:00:00 2001 From: Michael Fedora Date: Mon, 24 Jan 2022 12:06:42 -0500 Subject: [PATCH 1/4] better http handling - handle different accept/content types - handle graphql GET ?query requests --- http.ts | 84 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/http.ts b/http.ts index 01bdac9..61700b9 100644 --- a/http.ts +++ b/http.ts @@ -1,4 +1,4 @@ -import { runHttpQuery, GQLOptions } from './common.ts' +import { runHttpQuery, GQLOptions, GraphQLParams } from './common.ts' import type { GQLRequest } from './types.ts' /** @@ -18,8 +18,31 @@ export function GraphQLHTTP) { return async (request: Req) => { - if (options.graphiql && request.method === 'GET') { - if (request.headers.get('Accept')?.includes('text/html')) { + const accept = request.headers.get('Accept') || ''; + + const typeList = [ + 'text/html', + 'text/plain', + 'application/json', + '*/*' + ].map(contentType => ({ contentType, index: accept.indexOf(contentType) })) + .filter(({ index }) => index >= 0) + .sort((a, b) => a.index - b.index) + .map(({ contentType }) => contentType) + + if (accept && !typeList.length) { + return new Response('Not Acceptable', { status: 406, headers: new Headers(headers) }) + } else if (!['GET', 'PUT', 'POST', 'PATCH'].includes(request.method)) { + return new Response('Method Not Allowed', { status: 405, headers: new Headers(headers) }) + } + + let params: Promise + + if (request.method === 'GET') { + const urlQuery = request.url.substring(request.url.indexOf('?')) + const queryParams = new URLSearchParams(urlQuery) + + if (options.graphiql && typeList[0] === 'text/html' && !queryParams.has('raw')) { const { renderPlaygroundPage } = await import('./graphiql/render.ts') const playground = renderPlaygroundPage({ ...playgroundOptions, endpoint: '/graphql' }) @@ -29,35 +52,38 @@ export function GraphQLHTTP(await request.json(), options, { request }) - - return new Response(JSON.stringify(result, null, 2), { - status: 200, - headers: new Headers({ - 'Content-Type': 'application/json', - ...headers - }) - }) - } catch (e) { - console.error(e) - return new Response('Malformed request body', { - status: 400, - headers: new Headers(headers) - }) - } - } + params = request.json() + } + + try { + const result = await runHttpQuery(await params, options, { request }) + + let contentType = 'text/plain' + + if(!typeList.length || typeList.includes('application/json') || typeList.includes('*/*')) + contentType = 'application/json'; + + return new Response(JSON.stringify(result, null, 2), { + status: 200, + headers: new Headers({ + 'Content-Type': contentType, + ...headers + }) + }) + } catch (e) { + console.error(e) + return new Response('Malformed Request ' + (request.method === 'GET' ? 'Query' : 'Body'), { + status: 400, + headers: new Headers(headers) + }) } } } From 5221db6da90e6736db05dc071e4657ea68f8dcde Mon Sep 17 00:00:00 2001 From: Michael Fedora Date: Mon, 24 Jan 2022 12:12:04 -0500 Subject: [PATCH 2/4] fix vanilla --- examples/vanilla.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/vanilla.ts b/examples/vanilla.ts index fc757bd..62dfc77 100644 --- a/examples/vanilla.ts +++ b/examples/vanilla.ts @@ -28,7 +28,9 @@ const s = new Server({ })(req) : new Response('Not Found', { status: 404 }) }, - addr: ':3000' + port: 3000 }) s.listenAndServe() + +console.log(`☁ Started on http://localhost:3000`) From fbe1590744df8d61fcbd46d5d20368afd66bea65 Mon Sep 17 00:00:00 2001 From: Michael Fedora Date: Mon, 24 Jan 2022 12:17:37 -0500 Subject: [PATCH 3/4] remove semi-colons --- http.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http.ts b/http.ts index 61700b9..cfb30be 100644 --- a/http.ts +++ b/http.ts @@ -18,7 +18,7 @@ export function GraphQLHTTP) { return async (request: Req) => { - const accept = request.headers.get('Accept') || ''; + const accept = request.headers.get('Accept') || '' const typeList = [ 'text/html', @@ -69,7 +69,7 @@ export function GraphQLHTTP Date: Tue, 1 Feb 2022 10:54:01 -0500 Subject: [PATCH 4/4] fix/add tests & add 406 on GET --- http.ts | 2 ++ mod_test.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/http.ts b/http.ts index cfb30be..81ebfce 100644 --- a/http.ts +++ b/http.ts @@ -52,6 +52,8 @@ export function GraphQLHTTP { - it('should send 405 on GET', async () => { + it('should send 400 on malformed request query', async () => { const request = superdeno(app) - await request.get('/').expect(405) + await request.get('/').expect(400, 'Malformed Request Query') }) it('should send 400 on malformed request body', async () => { const request = superdeno(app) - await request.post('/').expect(400, 'Malformed request body') + await request.post('/').expect(400, 'Malformed Request Body') }) - it('should send resolved GraphQL query', async () => { + it('should send resolved POST GraphQL query', async () => { const request = superdeno(app) await request @@ -35,6 +35,57 @@ describe('GraphQLHTTP({ schema, rootValue })', () => { .send('{ "query": "{ hello }" }') .expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') }) + it('should send resolved GET GraphQL query', async () => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + }) + it('should send resolved GET GraphQL query when Accept is application/json', async () => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .set('Accept', 'application/json') + .expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + .expect('Content-Type', 'application/json') + }) + it('should send resolved GET GraphQL query when Accept is */*', async() => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .set('Accept', '*/*') + .expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + .expect('Content-Type', 'application/json') + }) + + it('should send resolved GET GraphQL query when Accept is text/plain', async() => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .set('Accept', 'text/plain') + .expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + .expect('Content-Type', 'text/plain') + }) + it('should send 406 not acceptable when Accept is other (text/html)', async () => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .set('Accept', 'text/html') + .expect(406, 'Not Acceptable') + }); + it('should send 406 not acceptable when Accept is other (text/css)', async () => { + const request = superdeno(app) + + await request + .get('/?query={hello}') + .set('Accept', 'text/css') + .expect(406, 'Not Acceptable') + }) it('should pass req obj to server context', async () => { type Context = { request: Request } @@ -57,21 +108,35 @@ describe('GraphQLHTTP({ schema, rootValue })', () => { }) describe('graphiql', () => { - it('should forbid GET requests when set to false', async () => { + it('should allow query GET requests when set to false', async () => { + const app = GraphQLHTTP({ graphiql: false, schema, rootValue }) + + const request = superdeno(app) + + await request.get('/?query={hello}').expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + }) + it('should allow query GET requests when set to true', async () => { + const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) + + const request = superdeno(app) + + await request.get('/?query={hello}').expect(200, '{\n "data": {\n "hello": "Hello World!"\n }\n}') + }) + it('should send 406 when Accept is only text/html when set to false', async () => { const app = GraphQLHTTP({ graphiql: false, schema, rootValue }) const request = superdeno(app) - await request.get('/').expect(405) + await request.get('/').set('Accept', 'text/html').expect(406, 'Not Acceptable') }) - it('should send 400 when Accept does not include text/html when set to true', async () => { + it('should render a playground when Accept does include text/html when set to true', async () => { const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) const request = superdeno(app) - await request.get('/').expect(400, '"Accept" header value must include text/html') + await request.get('/?query={hello}').set('Accept', 'text/html;*/*').expect(200).expect('Content-Type', 'text/html') }) - it('should render a playground if graphql is set to true', async () => { + it('should render a playground if graphiql is set to true', async () => { const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) const request = superdeno(app)