Repository: Azure/data-api-builder
Pull request #2681: Handle unexpected exception due to column type change in datasource
Author: login: vadeveka
Created: 2025-05-12T23:33:47Z | Merged: 2025-05-14T16:46:45Z

## Why make this change?
When an API owner starts up DAB, the GraphQL data types for the fields are automatically derived from the SQL column types. However, if the API owner later changes the column type without restarting DAB, the requests will fail at runtime due to failure with parsing the JSON result in the leaf node resolver.
Due to the unhandled exception, this will result in an error message `One of the identified items was in an invalid format` or `"The requested operation requires an element of type 'Number', but the target element has type 'String'.` 

## What is this change?
1. Wrap the field value parsing to catch specific exception types and throw a clearer exception
2. Ensure location and path in errors are retained for DataApiBuilderException for 4xx status codes

## How was this tested?

- [x] Manual test
- [] Integration Tests
- [] Unit Tests

## Sample Request(s)

Step 1: Create sample table in datasource
```
-- Create the table
CREATE TABLE Employees (
    EmployeeID INT PRIMARY KEY,
    Age INT NOT NULL,
    Salary INT NOT NULL
);

-- Insert sample data
INSERT INTO Employees (EmployeeID, Age, Salary)
VALUES 
    (1, 25, 50000),
    (2, 30, 60000),
    (3, 35, 70000),
    (4, 40, 80000),
    (5, 45, 90000);
```

Step 2:
Configure dab config with following entity
```
    "Employees": {
      "source": {
        "object": "dbo.Employees",
        "type": "table"
      },
      "permissions": [
        {
          "role": "anonymous",
          "actions": [
            {
              "action": "*"
            }
          ]
        }
      ]
    }, 
```

Verify employees query runs fine with Salary field
```
{
    employees {
        items {
            EmployeeID
            Salary
        }
    }
}
```

Step 3: Update Salary column type
```
ALTER TABLE Employees
ALTER COLUMN Salary NVARCHAR(100);
```

Run employees query again, response returned
```
{
  "errors": [
    {
      "message": "The value could not be parsed for configured GraphQL data type Int",
      "locations": [
        {
          "line": 5,
          "column": 13
        }
      ],
      "path": [
        "employees",
        "items",
        0,
        "Salary"
      ],
      "extensions": {
        "code": "GraphQLMapping"
      }
    }, 
    ...
  ]
}
```


Diff:
diff --git a/src/Config/Utilities/HttpStatusCodeExtensions.cs b/src/Config/Utilities/HttpStatusCodeExtensions.cs
new file mode 100644
index 0000000000..45a53c70c6
--- /dev/null
+++ b/src/Config/Utilities/HttpStatusCodeExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+
+namespace Azure.DataApiBuilder.Config.Utilities;
+
+public static class HttpStatusCodeExtensions
+{
+    /// <summary>
+    /// Check for status code within 4xx range
+    /// </summary>
+    /// <param name="statusCode"></param>
+    /// <returns></returns>
+    public static bool IsClientError(this HttpStatusCode statusCode)
+    {
+        int code = (int)statusCode;
+        return code >= 400 && code < 500;
+    }
+}
diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs
index b90873538b..ad9c69b05e 100644
--- a/src/Core/Services/ExecutionHelper.cs
+++ b/src/Core/Services/ExecutionHelper.cs
@@ -2,12 +2,14 @@
 // Licensed under the MIT License.
 
 using System.Globalization;
+using System.Net;
 using System.Text.Json;
 using Azure.DataApiBuilder.Config.ObjectModel;
 using Azure.DataApiBuilder.Core.Configurations;
 using Azure.DataApiBuilder.Core.Models;
 using Azure.DataApiBuilder.Core.Resolvers;
 using Azure.DataApiBuilder.Core.Resolvers.Factories;
+using Azure.DataApiBuilder.Service.Exceptions;
 using Azure.DataApiBuilder.Service.GraphQLBuilder;
 using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
 using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes;
@@ -155,27 +157,41 @@ fieldValue.ValueKind is not (JsonValueKind.Undefined or JsonValueKind.Null))
                 // transform it into the runtime type.
                 // We also want to ensure here that we do not unnecessarily convert values to
                 // strings and then force the conversion to parse them.
-                return namedType switch
+                try
                 {
-                    StringType => fieldValue.GetString(), // spec
-                    ByteType => fieldValue.GetByte(),
-                    ShortType => fieldValue.GetInt16(),
-                    IntType => fieldValue.GetInt32(), // spec
-                    LongType => fieldValue.GetInt64(),
-                    FloatType => fieldValue.GetDouble(), // spec
-                    SingleType => fieldValue.GetSingle(),
-                    DecimalType => fieldValue.GetDecimal(),
-                    DateTimeType => DateTimeOffset.TryParse(fieldValue.GetString()!, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out DateTimeOffset date) ? date : null, // for DW when datetime is null it will be in "" (double quotes) due to stringagg parsing and hence we need to ensure parsing is correct.
-                    DateType => DateTimeOffset.TryParse(fieldValue.GetString()!, out DateTimeOffset date) ? date : null,
-                    LocalTimeType => fieldValue.GetString()!.Equals("null", StringComparison.OrdinalIgnoreCase) ? null : LocalTimePattern.ExtendedIso.Parse(fieldValue.GetString()!).Value,
-                    ByteArrayType => fieldValue.GetBytesFromBase64(),
-                    BooleanType => fieldValue.GetBoolean(), // spec
-                    UrlType => new Uri(fieldValue.GetString()!),
-                    UuidType => fieldValue.GetGuid(),
-                    TimeSpanType => TimeSpan.Parse(fieldValue.GetString()!),
-                    AnyType => fieldValue.ToString(),
-                    _ => fieldValue.GetString()
-                };
+                    return namedType switch
+                    {
+                        StringType => fieldValue.GetString(), // spec
+                        ByteType => fieldValue.GetByte(),
+                        ShortType => fieldValue.GetInt16(),
+                        IntType => fieldValue.GetInt32(), // spec
+                        LongType => fieldValue.GetInt64(),
+                        FloatType => fieldValue.GetDouble(), // spec
+                        SingleType => fieldValue.GetSingle(),
+                        DecimalType => fieldValue.GetDecimal(),
+                        DateTimeType => DateTimeOffset.TryParse(fieldValue.GetString()!, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out DateTimeOffset date) ? date : null, // for DW when datetime is null it will be in "" (double quotes) due to stringagg parsing and hence we need to ensure parsing is correct.
+                        DateType => DateTimeOffset.TryParse(fieldValue.GetString()!, out DateTimeOffset date) ? date : null,
+                        LocalTimeType => fieldValue.GetString()!.Equals("null", StringComparison.OrdinalIgnoreCase) ? null : LocalTimePattern.ExtendedIso.Parse(fieldValue.GetString()!).Value,
+                        ByteArrayType => fieldValue.GetBytesFromBase64(),
+                        BooleanType => fieldValue.GetBoolean(), // spec
+                        UrlType => new Uri(fieldValue.GetString()!),
+                        UuidType => fieldValue.GetGuid(),
+                        TimeSpanType => TimeSpan.Parse(fieldValue.GetString()!),
+                        AnyType => fieldValue.ToString(),
+                        _ => fieldValue.GetString()
+                    };
+                }
+                catch (Exception ex) when (ex is InvalidOperationException or FormatException)
+                {
+                    // this usually means that database column type was changed since generating the GraphQL schema
+                    // for e.g. System.FormatException - One of the identified items was in an invalid format
+                    // System.InvalidOperationException - The requested operation requires an element of type 'Number', but the target element has type 'String'.
+                    throw new DataApiBuilderException(
+                        message: $"The {context.Selection.Field.Name} value could not be parsed for configured GraphQL data type {namedType.Name}",
+                        statusCode: HttpStatusCode.Conflict,
+                        subStatusCode: DataApiBuilderException.SubStatusCodes.GraphQLMapping,
+                        ex);
+                }
             }
 
             return null;
diff --git a/src/Service.Tests/Unittests/ConfigFileWatcherUnitTests.cs b/src/Service.Tests/Unittests/ConfigFileWatcherUnitTests.cs
index aa1d5cf886..00af635489 100644
--- a/src/Service.Tests/Unittests/ConfigFileWatcherUnitTests.cs
+++ b/src/Service.Tests/Unittests/ConfigFileWatcherUnitTests.cs
@@ -11,7 +11,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests;
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests;
 
 [TestClass]
 public class ConfigFileWatcherUnitTests
diff --git a/src/Service.Tests/Unittests/MultiSourceQueryExecutionUnitTests.cs b/src/Service.Tests/Unittests/MultiSourceQueryExecutionUnitTests.cs
index d0721a76c7..b792d0d74c 100644
--- a/src/Service.Tests/Unittests/MultiSourceQueryExecutionUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultiSourceQueryExecutionUnitTests.cs
@@ -34,7 +34,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     [TestClass]
     public class MultiSourceQueryExecutionUnitTests
diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs
index aaf3fbbd4b..5bdaa5f83d 100644
--- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MsSqlMultipleCreateOrderHelperUnitTests.cs
@@ -4,7 +4,7 @@
 using System.Threading.Tasks;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     [TestClass, TestCategory(TestCategory.MSSQL)]
     public class MsSqlMultipleCreateOrderHelperUnitTests : MultipleCreateOrderHelperUnitTests
diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
index 87e3882eed..672f5d1989 100644
--- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
@@ -12,7 +12,7 @@
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     [TestClass]
     public abstract class MultipleCreateOrderHelperUnitTests : SqlTestBase
diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs
index 4df72676e7..2563364e28 100644
--- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MySqlMultipleCreateOrderHelperUnitTests.cs
@@ -4,7 +4,7 @@
 using System.Threading.Tasks;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     /// <summary>
     /// Currently, we don't support multiple-create for MySql but the order determination logic for insertions is valid for MySql as well.
diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs
index c9b3fed32d..ce1f94d502 100644
--- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/PgSqlMultipleCreateOrderHelperUnitTests.cs
@@ -4,7 +4,7 @@
 using System.Threading.Tasks;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     /// <summary>
     /// Currently, we don't support multiple-create for PostgreSql but the order determination logic for insertions is valid for PostgreSql as well.
diff --git a/src/Service.Tests/Unittests/SerializationDeserializationTests.cs b/src/Service.Tests/Unittests/SerializationDeserializationTests.cs
index 61a7c931da..2b5e5bf3ba 100644
--- a/src/Service.Tests/Unittests/SerializationDeserializationTests.cs
+++ b/src/Service.Tests/Unittests/SerializationDeserializationTests.cs
@@ -13,7 +13,7 @@
 using Azure.DataApiBuilder.Core.Services.MetadataProviders.Converters;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 
-namespace Azure.DataApiBuilder.Service.Tests.Unittests
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
 {
     [TestClass]
     [TestCategory("Serialization and Deserialization using SqlMetadataProvider converters")]
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index a0d3ef3393..3d70f60e30 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -9,6 +9,7 @@
 using Azure.DataApiBuilder.Config;
 using Azure.DataApiBuilder.Config.Converters;
 using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Config.Utilities;
 using Azure.DataApiBuilder.Core.AuthenticationHelpers;
 using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
 using Azure.DataApiBuilder.Core.Authorization;
@@ -377,11 +378,16 @@ private void AddGraphQLService(IServiceCollection services, GraphQLRuntimeOption
                 {
                     if (error.Exception is DataApiBuilderException thrownException)
                     {
-                        return error.RemoveException()
-                                .RemoveLocations()
-                                .RemovePath()
+                        error = error.RemoveException()
                                 .WithMessage(thrownException.Message)
                                 .WithCode($"{thrownException.SubStatusCode}");
+
+                        // If user error i.e. validation error or conflict error with datasource, then retain location/path
+                        if (!thrownException.StatusCode.IsClientError())
+                        {
+                            error = error.RemoveLocations()
+                                .RemovePath();
+                        }
                     }
 
                     return error;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Improves error handling when column types change by converting JSON parsing errors into clearer GraphQL errors and preserving location info for client-side failures
- Wrap JSON-to-CLR conversions in `ExecutionHelper` to catch parsing issues and throw `DataApiBuilderException` with a clear message and 409 status
- Update GraphQL middleware in `Startup.cs` to set custom message/code and only strip locations/path for non-4xx errors
- Add `System.Net` import to use `HttpStatusCode` for clearer status code handling

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File                                 | Description                                                                                             |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| src/Service/Startup.cs               | Enhanced error mapping logic to retain `locations`/`path` on 4xx `DataApiBuilderException` and set code |
| src/Core/Services/ExecutionHelper.cs | Wrapped field value parsing in try/catch to throw a `DataApiBuilderException` on format or operation errors |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/Core/Services/ExecutionHelper.cs:184**
* Add unit tests to cover this new exception mapping branch, verifying that parsing errors throw a `DataApiBuilderException` with the correct status and subStatusCode.
```
catch (Exception ex) when (ex is InvalidOperationException or FormatException)
```
**src/Core/Services/ExecutionHelper.cs:190**
* [nitpick] Consider including the field name or JSON path in this error message to help consumers pinpoint which property failed to parse.
```
message: $"The value could not be parsed for configured GraphQL data type {namedType.Name}"
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T23:35:12Z
user: login: Alekhya-Polavarapu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:54:24Z
user: login: Alekhya-Polavarapu
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:54:31Z
user: login: vadeveka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T20:04:24Z
user: login: rusamant
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T20:24:33Z
user: login: Aniruddh25
body: LGTM, thanks for this contribution.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T01:24:46Z

Review comments:
diff_hunk: @@ -322,11 +323,17 @@ private void AddGraphQLService(IServiceCollection services, GraphQLRuntimeOption
                 {
                     if (error.Exception is DataApiBuilderException thrownException)
                     {
-                        return error.RemoveException()
-                                .RemoveLocations()
-                                .RemovePath()
+                        error = error.RemoveException()
                                 .WithMessage(thrownException.Message)
                                 .WithCode($"{thrownException.SubStatusCode}");
+
+                        // If user error i.e. validation error or conflict error with datasource, then retain location/path
+                        int httpStatusCode = (int)thrownException.StatusCode;
+                        if (httpStatusCode < 400 || httpStatusCode >= 500)
path: src/Service/Startup.cs
user: login: Copilot
body: Replace these hard-coded status code bounds with `HttpStatusCode` enum values or a helper method (e.g., `IsClientError`) to clarify that you’re targeting the 4xx range.
```suggestion
                        if (!IsClientError(httpStatusCode))
```
created_at: 2025-05-12T23:35:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+
+namespace Azure.DataApiBuilder.Config.Utilities;
+
+public static class HttpStatusCodeExtensions
+{
+    /// <summary>
+    /// Check for status code within 4xx range
+    /// </summary>
+    /// <param name="statusCode"></param>
+    /// <returns></returns>
+    public static bool IsClientError(this HttpStatusCode statusCode)
path: src/Config/Utilities/HttpStatusCodeExtensions.cs
user: login: Alekhya-Polavarapu
body: Am i missing or is this not being used?
created_at: 2025-05-13T19:54:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+
+namespace Azure.DataApiBuilder.Config.Utilities;
+
+public static class HttpStatusCodeExtensions
+{
+    /// <summary>
+    /// Check for status code within 4xx range
+    /// </summary>
+    /// <param name="statusCode"></param>
+    /// <returns></returns>
+    public static bool IsClientError(this HttpStatusCode statusCode)
path: src/Config/Utilities/HttpStatusCodeExtensions.cs
user: login: vadeveka
body: used in Startup
created_at: 2025-05-13T20:04:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: vadeveka
created_at: 2025-05-13T00:12:06Z
body: /azp run
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-05-13T00:12:28Z
body: <samp>
Azure Pipelines successfully started running 6 pipeline(s).<br>

</samp>
author_association: NONE
user: login: vadeveka
created_at: 2025-05-13T00:13:55Z
body: /azp run
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2025-05-13T00:14:15Z
body: <samp>
Azure Pipelines successfully started running 6 pipeline(s).<br>

</samp>
author_association: NONE
user: login: Aniruddh25
created_at: 2025-05-14T01:26:58Z
body: /azp run
author_association: COLLABORATOR
user: login: azure-pipelines[bot]
created_at: 2025-05-14T01:27:21Z
body: <samp>
Azure Pipelines successfully started running 6 pipeline(s).<br>

</samp>
author_association: NONE