11package graphql.execution.instrumentation
22
3+ import graphql.ErrorType
34import graphql.ExecutionInput
45import graphql.ExecutionResult
56import graphql.GraphQL
7+ import graphql.GraphqlErrorBuilder
8+ import graphql.GraphqlErrorBuilderTest
69import graphql.StarWarsSchema
710import graphql.TestUtil
811import graphql.execution.AsyncExecutionStrategy
12+ import graphql.execution.DataFetcherExceptionHandlerResult
13+ import graphql.execution.DataFetcherResult
914import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters
1015import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters
1116import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters
@@ -23,6 +28,8 @@ import spock.lang.Specification
2328import java.util.concurrent.CompletableFuture
2429import java.util.concurrent.TimeUnit
2530import java.util.concurrent.atomic.AtomicBoolean
31+ import java.util.concurrent.atomic.AtomicInteger
32+ import java.util.concurrent.atomic.AtomicLong
2633
2734class InstrumentationTest extends Specification {
2835
@@ -152,6 +159,165 @@ class InstrumentationTest extends Specification {
152159 instrumentation. throwableList[0 ]. getMessage() == " DF BANG!"
153160 }
154161
162+ def " field fetch will instrument exceptions correctly - includes exception handling with onExceptionHandled" () {
163+
164+ given :
165+
166+ def query = """
167+ {
168+ hero {
169+ id
170+ }
171+ }
172+ """
173+
174+ def instrumentation = new LegacyTestingInstrumentation () {
175+ def onHandledCalled = false
176+ def onCompletedCalled = false
177+ def onDispatchedCalled = false
178+
179+ @Override
180+ DataFetcher<?> instrumentDataFetcher (DataFetcher<?> dataFetcher , InstrumentationFieldFetchParameters parameters , InstrumentationState state ) {
181+ return new DataFetcher<Object > () {
182+ @Override
183+ Object get (DataFetchingEnvironment environment ) {
184+ throw new RuntimeException (" DF BANG!" )
185+ }
186+ }
187+ }
188+
189+ @Override
190+ FieldFetchingInstrumentationContext beginFieldFetching (InstrumentationFieldFetchParameters parameters , InstrumentationState state ) {
191+ return new FieldFetchingInstrumentationContext () {
192+ @Override
193+ void onDispatched () {
194+ onDispatchedCalled = true
195+ }
196+
197+ @Override
198+ void onCompleted (Object result , Throwable t ) {
199+ onCompletedCalled = true
200+ }
201+
202+ @Override
203+ void onExceptionHandled (DataFetcherResult<Object > dataFetcherResult ) {
204+ onHandledCalled = true
205+ }
206+ }
207+ }
208+ }
209+
210+ def graphQL = GraphQL
211+ .newGraphQL(StarWarsSchema . starWarsSchema)
212+ .defaultDataFetcherExceptionHandler { it ->
213+ // catch all exceptions and transform to graphql error with a prefixed message
214+ return CompletableFuture . completedFuture(
215+ DataFetcherExceptionHandlerResult . newResult(GraphqlErrorBuilder . newError()
216+ .errorType(ErrorType.DataFetchingException )
217+ .message(" Handled " + it. exception. message)
218+ .path(it. path)
219+ .build())
220+ .build())
221+ }
222+ .instrumentation(instrumentation)
223+ .build()
224+
225+ when :
226+ def resp = graphQL. execute(query)
227+
228+ then : " exception handler turned the exception into a graphql error and message prefixed with Handled"
229+ resp. errors. size() == 1
230+ resp. errors[0 ]. message == " Handled DF BANG!"
231+
232+ and : " all instrumentation methods were called"
233+ instrumentation. onDispatchedCalled == true
234+ instrumentation. onCompletedCalled == true
235+ instrumentation. onHandledCalled == true
236+ }
237+
238+
239+ def " field fetch verify order and call of all methods" () {
240+
241+ given :
242+
243+ def query = """
244+ {
245+ hero {
246+ id
247+ }
248+ }
249+ """
250+
251+ def metric = []
252+ def instrumentation = new SimplePerformantInstrumentation () {
253+ def timeElapsed = new AtomicInteger ()
254+
255+ @Override
256+ DataFetcher<?> instrumentDataFetcher (DataFetcher<?> dataFetcher , InstrumentationFieldFetchParameters parameters , InstrumentationState state ) {
257+ return new DataFetcher<Object > () {
258+ @Override
259+ Object get (DataFetchingEnvironment environment ) {
260+ // simulate latency
261+ timeElapsed. addAndGet(50 )
262+ throw new RuntimeException (" DF BANG!" )
263+ }
264+ }
265+ }
266+
267+ @Override
268+ FieldFetchingInstrumentationContext beginFieldFetching (InstrumentationFieldFetchParameters parameters , InstrumentationState state ) {
269+ return new FieldFetchingInstrumentationContext () {
270+ def start = 0
271+ def duration = 0
272+ def hasError = false
273+
274+
275+ @Override
276+ void onDispatched () {
277+ start = 1
278+ }
279+
280+ @Override
281+ void onCompleted (Object result , Throwable t ) {
282+ duration = timeElapsed. get() - start
283+ metric = [duration, hasError]
284+ }
285+
286+ @Override
287+ void onExceptionHandled (DataFetcherResult<Object > dataFetcherResult ) {
288+ hasError = dataFetcherResult. errors != null && ! dataFetcherResult. errors. isEmpty()
289+ && dataFetcherResult. errors. any { it. message. contains(" Handled" ) }
290+ }
291+ }
292+ }
293+ }
294+
295+ def graphQL = GraphQL
296+ .newGraphQL(StarWarsSchema . starWarsSchema)
297+ .defaultDataFetcherExceptionHandler { it ->
298+ // catch all exceptions and transform to graphql error with a prefixed message
299+ return CompletableFuture . completedFuture(
300+ DataFetcherExceptionHandlerResult . newResult(GraphqlErrorBuilder . newError()
301+ .errorType(ErrorType.DataFetchingException )
302+ .message(" Handled " + it. exception. message)
303+ .path(it. path)
304+ .build())
305+ .build())
306+ }
307+ .instrumentation(instrumentation)
308+ .build()
309+
310+ when :
311+ def resp = graphQL. execute(query)
312+
313+ then : " exception handler turned the exception into a graphql error and prefixed its message with 'Handled'"
314+ resp. errors. size() == 1
315+ resp. errors[0 ]. message == " Handled DF BANG!"
316+
317+ and : " metric was captured i.e all instrumentation methods were called in the right order"
318+ metric == [49 , true ]
319+ }
320+
155321 /**
156322 * This uses a stop and go pattern and multiple threads. Each time
157323 * the execution strategy is invoked, the data fetchers are held
0 commit comments