Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
review follow-ups: default LogException, name the field, skip client …
…errors

- LogException gets a default (no-op) interface implementation. Adding a
  member without one breaks every custom ISchemaProvider, which a patch
  release should not do.
- It takes the field name and logs field failures under their own event id and
  message ("Error executing field 'x'", FieldError). Reusing the request-level
  message meant a query with several root fields could not say which one failed,
  and field errors could not be filtered from request errors.
- Document and validation errors are not logged. GenerateErrors returns their
  message to the caller in full, so nothing is being swallowed and the request,
  not the server, is what went wrong - logging every malformed query at Error is
  noise and is trivially remote-triggerable. Ones wrapping a real fault (inner
  exception set) are sanitised on the way out, so they are still logged, as are
  authorization failures (EntityGraphQLFieldException, not EntityGraphQLException).
- Test for that, and the existing one now asserts the field-named message.
- CHANGELOG entry trimmed and says it is a 6.0 regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  • Loading branch information
lukemurray and claude committed Aug 3, 2026
commit 5afd5a9fb1f7fa6fde350b1531f7f27f291cf713
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Fixes

- Field-level execution exceptions that are returned as GraphQL `errors` (partial results) are logged again via the schema `ILogger` with the original `Exception` and stack trace. In 6.x these failures are caught per field instead of bubbling to `SchemaProvider.HandleException`, so the previous `Error executing QueryRequest` log (with stack) was skipped. `ISchemaProvider.LogException(Exception)` is new for custom schema implementations; the built-in provider uses it from both top-level request handling and field catch sites. Custom `ISchemaProvider` implementations need to add it.
- Field-level exceptions are logged again, with the field name and stack trace, to the `ILogger` the schema was built with. A regression in 6.0: partial results turned a field failure into a GraphQL error instead of rethrowing it, so nothing reached the request-level log and the only way to see why a field failed was development mode or `AllowedExceptions` - both of which return the detail to the caller. Responses are unchanged. Document and validation errors, whose message the caller already gets in full, are not logged.

# 6.1.4

Expand Down
19 changes: 13 additions & 6 deletions src/EntityGraphQL/Compiler/GqlNodes/ExecutableGraphQLStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,24 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
}
catch (EntityGraphQLFieldException fe)
{
Schema.LogException(fe);
Schema.LogException(fe, expandedField.Name);
allErrors.Add(new GraphQLError(Schema.AllowedExceptionMessage(fe), expandedField.BuildPath(), null));
}
catch (EntityGraphQLException ve)
{
Schema.LogException(ve);
// A document or validation error's message is returned to the caller in full, so
// nothing is being swallowed and the request, not the server, is what went wrong -
// logging every malformed query as a server error is noise. One that wraps a real
// fault (it has an inner exception) is sanitised on the way out, so it is logged.
if (ve.InnerException != null)
Schema.LogException(ve, expandedField.Name);
allErrors.AddRange(Schema.GenerateErrors(ve));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
}
catch (TargetInvocationException tie) when (tie.InnerException != null)
{
Schema.LogException(tie.InnerException);
Schema.LogException(tie.InnerException, expandedField.Name);
allErrors.AddRange(Schema.GenerateErrors(tie.InnerException, expandedField.Name));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
Expand All @@ -218,7 +223,7 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
// than wrapped in a TargetInvocationException - flatten it to distinct errors the same way
catch (AggregateException ae)
{
Schema.LogException(ae);
Schema.LogException(ae, expandedField.Name);
allErrors.AddRange(Schema.GenerateErrors(ae, expandedField.Name));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
Expand All @@ -227,7 +232,7 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
{
// Log the original exception so the stack trace points at the resolver failure,
// not the EntityGraphQLException wrapper we build for the GraphQL error payload.
Schema.LogException(ex);
Schema.LogException(ex, expandedField.Name);
allErrors.AddRange(
Schema.GenerateErrors(
new EntityGraphQLException(
Expand All @@ -253,7 +258,9 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
// building the variables could cause this
catch (EntityGraphQLException ce)
{
Schema.LogException(ce);
// as above - only worth logging when it wraps a fault the caller does not get told about
if (ce.InnerException != null)
Schema.LogException(ce);
allErrors.AddRange(Schema.GenerateErrors(ce));
}

Expand Down
8 changes: 5 additions & 3 deletions src/EntityGraphQL/Schema/ISchemaProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ void AddMutationsFrom<TType>(SchemaBuilderOptions? options = null)

/// <summary>
/// Logs an exception raised while executing a GraphQL request. Field-level failures that are returned as
/// GraphQL errors (partial results) should still call this so the Exception and stack trace are available
/// to the configured <c>ILogger</c>, matching top-level request failure logging.
/// GraphQL errors (partial results) call this too, so the exception and its stack trace reach the
/// configured <c>ILogger</c> even though the caller only gets a sanitised message. Pass the field name
/// when the failure belongs to one field. Does nothing by default so implementations of this interface do
/// not have to change.
/// </summary>
void LogException(Exception exception);
void LogException(Exception exception, string? fieldName = null) { }
}
13 changes: 11 additions & 2 deletions src/EntityGraphQL/Schema/SchemaProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public class SchemaProvider<TContextType> : ISchemaProvider, IDisposable
// map some types to scalar types
private readonly Dictionary<Type, GqlTypeInfo> customTypeMappings;
private static readonly Action<ILogger, Exception> logErrorMessage = LoggerMessage.Define(LogLevel.Error, new EventId(1, "QueryRequest"), "Error executing QueryRequest");
private static readonly Action<ILogger, string, Exception> logFieldErrorMessage = LoggerMessage.Define<string>(
LogLevel.Error,
new EventId(2, "FieldError"),
"Error executing field '{Field}'"
);

public SchemaProvider()
: this(null, null) { }
Expand Down Expand Up @@ -595,9 +600,13 @@ private QueryResult HandleException(Exception exception)
}

/// <inheritdoc />
public void LogException(Exception exception)
public void LogException(Exception exception, string? fieldName = null)
{
if (logger != null)
if (logger == null)
return;
if (fieldName != null)
logFieldErrorMessage(logger, fieldName, exception);
else
logErrorMessage(logger, exception);
}

Expand Down
16 changes: 15 additions & 1 deletion src/tests/EntityGraphQL.Tests/ErrorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,21 @@ public void FieldExecutionException_IsLoggedWithException()
Assert.Contains(errorLogs, e => e.Exception!.ToString().Contains("You should not see this message outside of Development"));
var fieldFailLog = errorLogs.First(e => e.Exception!.ToString().Contains("You should not see this message outside of Development"));
Assert.False(string.IsNullOrEmpty(fieldFailLog.Exception!.GetBaseException().StackTrace));
Assert.Equal("Error executing QueryRequest", fieldFailLog.Message);
Assert.Equal("Error executing field 'people'", fieldFailLog.Message);
}

[Fact]
public void ClientError_IsNotLogged()
{
// The caller gets this message in full - nothing is swallowed and it is the request that is wrong, so
// it is not logged as a server error (every malformed query would otherwise log at Error).
var logger = new CapturingLogger<SchemaProvider<TestDataContext>>();
var schemaProvider = SchemaBuilder.FromObject<TestDataContext>(logger: logger);

var results = schemaProvider.ExecuteRequestWithContext(new QueryRequest { Query = "{ people { x: id x: name } }" }, new TestDataContext(), null, null);

Assert.Contains(results.Errors!, e => e.Message.Contains("must be the same field with identical arguments"));
Assert.DoesNotContain(logger.Entries, e => e.Level == LogLevel.Error);
}

private sealed class CapturingLogger<T> : ILogger<T>
Expand Down