1
- import { createServer , IncomingMessage , Server , ServerResponse , STATUS_CODES } from 'node:http' ;
1
+ import { createServer , IncomingMessage , ServerResponse , STATUS_CODES } from 'node:http' ;
2
2
import { AddressInfo } from 'node:net' ;
3
3
import { fetch } from '@whatwg-node/fetch' ;
4
- import { createGraphQLError , createSchema , createYoga } from '../src/index.js' ;
4
+ import { createGraphQLError , createSchema , createYoga , Plugin } from '../src/index.js' ;
5
5
6
6
describe ( 'node-http' , ( ) => {
7
- let server : Server ;
8
- let port : number ;
9
-
10
- beforeAll ( async ( ) => {
7
+ it ( 'should expose Node req and res objects in the context' , async ( ) => {
11
8
const yoga = createYoga < {
12
9
req : IncomingMessage ;
13
10
res : ServerResponse ;
@@ -16,12 +13,43 @@ describe('node-http', () => {
16
13
typeDefs : /* GraphQL */ `
17
14
type Query {
18
15
isNode: Boolean!
19
- throw(status: Int): Int!
20
16
}
21
17
` ,
22
18
resolvers : {
23
19
Query : {
24
20
isNode : ( _ , __ , { req, res } ) => ! ! ( req && res ) ,
21
+ } ,
22
+ } ,
23
+ } ) ,
24
+ } ) ;
25
+ const server = createServer ( yoga ) ;
26
+ await new Promise < void > ( resolve => server . listen ( 0 , resolve ) ) ;
27
+ const port = ( server . address ( ) as AddressInfo ) . port ;
28
+
29
+ try {
30
+ const response = await fetch ( `http://localhost:${ port } /graphql?query=query{isNode}` ) ;
31
+ expect ( response . status ) . toBe ( 200 ) ;
32
+ const body = await response . json ( ) ;
33
+ expect ( body . errors ) . toBeUndefined ( ) ;
34
+ expect ( body . data . isNode ) . toBe ( true ) ;
35
+ } finally {
36
+ await new Promise < void > ( resolve => server . close ( ( ) => resolve ( ) ) ) ;
37
+ }
38
+ } ) ;
39
+
40
+ it ( 'should set status text by status code' , async ( ) => {
41
+ const yoga = createYoga < {
42
+ req : IncomingMessage ;
43
+ res : ServerResponse ;
44
+ } > ( {
45
+ schema : createSchema ( {
46
+ typeDefs : /* GraphQL */ `
47
+ type Query {
48
+ throw(status: Int): Int!
49
+ }
50
+ ` ,
51
+ resolvers : {
52
+ Query : {
25
53
throw ( _ , { status } ) {
26
54
throw createGraphQLError ( 'Test' , {
27
55
extensions : {
@@ -36,26 +64,10 @@ describe('node-http', () => {
36
64
} ) ,
37
65
logging : false ,
38
66
} ) ;
39
- server = createServer ( yoga ) ;
67
+ const server = createServer ( yoga ) ;
40
68
await new Promise < void > ( resolve => server . listen ( 0 , resolve ) ) ;
41
- port = ( server . address ( ) as AddressInfo ) . port ;
42
- } ) ;
43
-
44
- afterAll ( async ( ) => {
45
- if ( server ) {
46
- await new Promise < void > ( resolve => server . close ( ( ) => resolve ( ) ) ) ;
47
- }
48
- } ) ;
49
-
50
- it ( 'should expose Node req and res objects in the context' , async ( ) => {
51
- const response = await fetch ( `http://localhost:${ port } /graphql?query=query{isNode}` ) ;
52
- expect ( response . status ) . toBe ( 200 ) ;
53
- const body = await response . json ( ) ;
54
- expect ( body . errors ) . toBeUndefined ( ) ;
55
- expect ( body . data . isNode ) . toBe ( true ) ;
56
- } ) ;
69
+ const port = ( server . address ( ) as AddressInfo ) . port ;
57
70
58
- it ( 'should set status text by status code' , async ( ) => {
59
71
for ( const statusCodeStr in STATUS_CODES ) {
60
72
const status = Number ( statusCodeStr ) ;
61
73
if ( status < 200 ) continue ;
@@ -77,4 +89,146 @@ describe('node-http', () => {
77
89
expect ( response . statusText ) . toBe ( STATUS_CODES [ status ] ) ;
78
90
}
79
91
} ) ;
92
+
93
+ it ( 'request cancellation causes signal passed to executor to be aborted' , async ( ) => {
94
+ const d = createDeferred ( ) ;
95
+ const didAbortD = createDeferred ( ) ;
96
+
97
+ const plugin : Plugin = {
98
+ onExecute ( ctx ) {
99
+ ctx . setExecuteFn ( async function execute ( params ) {
100
+ d . resolve ( ) ;
101
+
102
+ return new Promise ( ( _ , rej ) => {
103
+ // @ts -expect-error Signal is not documented yet...
104
+ params . signal . addEventListener ( 'abort' , ( ) => {
105
+ didAbortD . resolve ( ) ;
106
+ rej ( new DOMException ( 'The operation was aborted' , 'AbortError' ) ) ;
107
+ } ) ;
108
+ } ) ;
109
+ } ) ;
110
+ } ,
111
+ } ;
112
+ const yoga = createYoga ( {
113
+ schema : createSchema ( {
114
+ typeDefs : /* GraphQL */ `
115
+ type Query {
116
+ hi: String!
117
+ }
118
+ ` ,
119
+ } ) ,
120
+ plugins : [ plugin ] ,
121
+ } ) ;
122
+ const server = createServer ( yoga ) ;
123
+ await new Promise < void > ( resolve => server . listen ( 0 , resolve ) ) ;
124
+ const port = ( server . address ( ) as AddressInfo ) . port ;
125
+ try {
126
+ const controller = new AbortController ( ) ;
127
+ const response$ = fetch ( `http://localhost:${ port } /graphql` , {
128
+ method : 'POST' ,
129
+ headers : {
130
+ 'content-type' : 'application/json' ,
131
+ } ,
132
+ body : JSON . stringify ( {
133
+ query : /* GraphQL */ `
134
+ query {
135
+ hi
136
+ }
137
+ ` ,
138
+ } ) ,
139
+ signal : controller . signal ,
140
+ } ) ;
141
+ await d . promise ;
142
+ controller . abort ( ) ;
143
+ await expect ( response$ ) . rejects . toThrow ( 'The operation was aborted' ) ;
144
+ await didAbortD . promise ;
145
+ } finally {
146
+ await new Promise < void > ( resolve => server . close ( ( ) => resolve ( ) ) ) ;
147
+ }
148
+ } ) ;
149
+
150
+ it ( 'request cancellation causes no more resolvers being invoked' , async ( ) => {
151
+ const didInvokeSlowResolverD = createDeferred ( ) ;
152
+
153
+ const didCancelD = createDeferred ( ) ;
154
+
155
+ let didInvokedNestedField = false ;
156
+ const yoga = createYoga ( {
157
+ schema : createSchema ( {
158
+ typeDefs : /* GraphQL */ `
159
+ type Query {
160
+ slow: Nested!
161
+ }
162
+
163
+ type Nested {
164
+ field: String!
165
+ }
166
+ ` ,
167
+ resolvers : {
168
+ Query : {
169
+ async slow ( ) {
170
+ didInvokeSlowResolverD . resolve ( ) ;
171
+ await didCancelD . promise ;
172
+ return { } ;
173
+ } ,
174
+ } ,
175
+ Nested : {
176
+ field ( ) {
177
+ didInvokedNestedField = true ;
178
+ return 'test' ;
179
+ } ,
180
+ } ,
181
+ } ,
182
+ } ) ,
183
+ } ) ;
184
+ const server = createServer ( yoga ) ;
185
+ await new Promise < void > ( resolve => server . listen ( 0 , resolve ) ) ;
186
+ const port = ( server . address ( ) as AddressInfo ) . port ;
187
+ try {
188
+ const controller = new AbortController ( ) ;
189
+ const response$ = fetch ( `http://localhost:${ port } /graphql` , {
190
+ method : 'POST' ,
191
+ headers : {
192
+ 'content-type' : 'application/json' ,
193
+ } ,
194
+ body : JSON . stringify ( {
195
+ query : /* GraphQL */ `
196
+ query {
197
+ slow {
198
+ field
199
+ }
200
+ }
201
+ ` ,
202
+ } ) ,
203
+ signal : controller . signal ,
204
+ } ) ;
205
+
206
+ await didInvokeSlowResolverD . promise ;
207
+ controller . abort ( ) ;
208
+ await expect ( response$ ) . rejects . toThrow ( 'The operation was aborted' ) ;
209
+ // wait a few milliseconds to ensure server-side cancellation logic runs
210
+ await new Promise < void > ( resolve => setTimeout ( resolve , 10 ) ) ;
211
+ didCancelD . resolve ( ) ;
212
+ // wait a few milliseconds to allow the nested field resolver to run (if cancellation logic is incorrect)
213
+ await new Promise < void > ( resolve => setTimeout ( resolve , 10 ) ) ;
214
+ expect ( didInvokedNestedField ) . toBe ( false ) ;
215
+ } finally {
216
+ await new Promise < void > ( resolve => server . close ( ( ) => resolve ( ) ) ) ;
217
+ }
218
+ } ) ;
80
219
} ) ;
220
+
221
+ type Deferred < T = void > = {
222
+ resolve : ( value : T ) => void ;
223
+ reject : ( value : unknown ) => void ;
224
+ promise : Promise < T > ;
225
+ } ;
226
+
227
+ function createDeferred < T = void > ( ) : Deferred < T > {
228
+ const d = { } as Deferred < T > ;
229
+ d . promise = new Promise < T > ( ( resolve , reject ) => {
230
+ d . resolve = resolve ;
231
+ d . reject = reject ;
232
+ } ) ;
233
+ return d ;
234
+ }
0 commit comments