Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System.Text.Json - can't serialize exception #43026

Closed
AlexeyKhrenov opened this issue Oct 5, 2020 · 11 comments
Closed

System.Text.Json - can't serialize exception #43026

AlexeyKhrenov opened this issue Oct 5, 2020 · 11 comments

Comments

@AlexeyKhrenov
Copy link

Description

We're trying to use System.Text.Json for serialization and deserialization inside our API project with GraphQL. Sometimes the response object from GraphQL contains errors (exceptions). And serialization of this object fails. So I've come up with a simplified example for the problem:

static void Main(string[] args)
        {
            try
            {
                throw new InvalidOperationException("New exception");
            }
            catch (Exception ex)
            {
                var str = JsonSerializer.Serialize(ex);
            }
            Console.WriteLine("Hello World!");
        }

The code above throws System.NotSupportedException: Serialization and deserialization of 'System.Type' instances are not supported

Expected

Exceptions are serialized

Workaround

Use Newtonsoft serializer in ASP.NET pipeline.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Oct 5, 2020
@Clockwork-Muse
Copy link
Contributor

.... first of all, you generally shouldn't return raw exceptions in an API. Not only because of potential security vulnerabilities, but also simply because there's going to be a bunch of implementation details which are in no way relevant to your callers (line numbers, full call stack). They should get logged, and some generic "we messed up" error returned (HTTP 500, maybe).

If this is in reference to your caller doing something not supported by your API (calling Add on a collection with the maximum number of items, for example), that's not exceptional. That's a "foreseeable" error on your end. You should be returning some sort of specific error type relevant to the operation at hand. Since graphql mostly uses JSON you might want to look into the problem details RFC.

I get a completely different error in dotnetfiddle, which might be its own problem...

@AlexeyKhrenov
Copy link
Author

You're absolutely right. What was stated is absolutely right in broad sense. But I wouldn't like to share the whole picture related to our API in this narrow discussion. And also there's no need for people to give any recomendations when they don't have the whole picture and especially when the recomendation doesn't relate to the subject of the topic.
As for the defence of legitimacy of cases when I really need to serialize an exception I can think out a lot of cases: when I'm trying to log an exception as JSON and etc.
I would be happy to have a possibility to just specify inside JsonSerializerOptions that System.Type should be ignored during serialization
Unfortunately, right now I don't have any workaround other than just moving to Newtonsoft

@jozkee
Copy link
Member

jozkee commented Oct 9, 2020

It has been concluded in the past that (de)serialization of System.Type is not secure #31567 (comment), therefore the are no plans on changing/removing the exception that you are reporting.

Unfortunately, right now I don't have any workaround other than just moving to Newtonsoft

@AlexeyKhrenov Does adding a converter for Exceptions solves your scenario?

class ExceptionConverter : JsonConverter<Exception>
{
    public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WriteString("Message", value.Message);
        // Add any other propoerties that you may want to include in your JSON.
        // ...
        writer.WriteEndObject();
    }
}

Then add such converter to your JsonSerializerOptions:

static void Main(string[] args)
{
    var options = new JsonSerializerOptions();
    options.Converters.Add(new ExceptionConverter());

    try
    {
        throw new InvalidOperationException("New exception");
    }
    catch (Exception ex)
    {
        var str = JsonSerializer.Serialize(ex, options);
    }
    Console.WriteLine("Hello World!");
}

I get a completely different error in dotnetfiddle, which might be its own problem...

@Clockwork-Muse that error is because serialization of your Exception instance is falling into an infinite loop. Throwing for System.Type is new behavior on 5.0.

@stephentoub stephentoub removed the untriaged New issue has not been triaged by the area owner label Oct 9, 2020
@stephentoub stephentoub added this to the Future milestone Oct 9, 2020
@AlexeyKhrenov
Copy link
Author

Thank you, @jozkee ! Implementing a custom JsonConverter will definetely solve the issue.

@knocte
Copy link
Contributor

knocte commented Dec 9, 2020

Thank you, @jozkee ! Implementing a custom JsonConverter will definetely solve the issue.

But @jozkee's code snippet only contains the part to write (serialize) exceptions, how to read (deserialize) them later? Did you implement that @AlexeyKhrenov ?

@AlexeyKhrenov
Copy link
Author

Hi, @knocte ! No, unfortunately we switched to another lib

knocte added a commit to nblockchain/geewallet that referenced this issue Dec 16, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 16, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

NOTE: we had to use UTF-8 (instead of Encoding.Default) because otherwise
the tests (committed from my macOS) would fail on Windows CI. Don't ask
me why, I hate encodings too.

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 16, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

NOTE: we had to use ASCII (instead of Encoding.Default) because otherwise
the tests (committed from my macOS) would fail on Windows CI (UTF-8 would
fail too because of [3]0. Don't ask me why, I hate encodings too.

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 16, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

NOTE: we had to use Base64 because using any of the System.Text encodings
(like Encoding.Default, ASCII, UTF-8 or UTF-7) would make the tests fail
fail in weird ways (or only pass for certain OSs, e.g. see [3]). Don't
ask me why, I hate encodings too.

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 20, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 20, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Dec 21, 2020
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-default-encoding-based-string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Jan 4, 2021
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-base64string format.

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
knocte added a commit to nblockchain/geewallet that referenced this issue Jan 4, 2021
JSON deserialization of exceptions is extremely broken in JSON.NET [1]
(and even in System.Text.Json [2]), so let's use a hybrid between binary
serialization and JSON serialization: it will have a JSON part that hints
about the most important human-readable details, but it will use a JSON
element to marshall in binary-to-base64string format (I decided to use
base64string after having many issues, e.g. [3], with UTF8 and other
encodings).

(I could have written a JsonConverter for this, but I was having trouble
finding one out there that someone else had written already, then I decided
to go for the simplest solution that I can write myself. I hate JSON type
converters anyway.)

[1] JamesNK/Newtonsoft.Json#801
[2] dotnet/runtime#43026
[3] https://stackoverflow.com/a/13674508/544947
@sreejith-ms
Copy link

@Clockwork-Muse This is an example from eShopOnContainers to return exception in the developer environment along with ProblemDetails.

simon-wacker added a commit to building-envelope-data/metabase that referenced this issue Jul 13, 2021
…` like `Exception` (because it is not supported as it poses security issues, confer to dotnet/runtime#41920 and dotnet/runtime#43026
@JakenVeina
Copy link
Contributor

JakenVeina commented Jul 31, 2021

Bump for this issue. I was not expecting to have to write a massive custom serializer to be able to log exceptions as JSON. If deserialization of Type objects is considered insecure, there should be a way to ignore them, during serialization, or not support them for deserialization.

@eiriktsarpalis
Copy link
Member

Currently, .NET provides no way to deserialize exceptions without using the ISerializable pattern from BinaryFormatter. This is an inherently insecure mechanism which we are planning to deprecate in future releases. Issue #43482 is tracking work for a successor mechanism for safe exception serialization and deserialization, but it's very much in an early discussion stage. Once in place we will definitely be bringing first-class exception serialization support in System.Text.Json building on top of that mechanism.

Recommendation is to use a custom converter as described in #43026 (comment) for exception serialization, however we recommend against writing converters that support exception deserialization.

@ghost
Copy link

ghost commented Oct 22, 2021

I just came across this yesterday / this morning. I ended up taking @jozkee 's partial answer and enhanced it by using reflection to get all properties of the Exception sans the TargetSite (which for me in dotcore 3.1) was / is the offending property. This should handle most exception types (even custom types w/ custom properties). Enjoy

    public class ExceptionConverter<TExceptionType> : JsonConverter<TExceptionType>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            return typeof(Exception).IsAssignableFrom(typeToConvert);
        }

        public override TExceptionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            throw new NotSupportedException("Deserializing exceptions is not allowed");
        }

        public override void Write(Utf8JsonWriter writer, TExceptionType value, JsonSerializerOptions options)
        {
            var serializableProperties = value.GetType()
                .GetProperties()
                .Select(uu => new { uu.Name, Value = uu.GetValue(value) })
                .Where(uu => uu.Name != nameof(Exception.TargetSite));

            if (options?.IgnoreNullValues == true)
            {
                serializableProperties = serializableProperties.Where(uu => uu.Value != null);
            }

            var propList = serializableProperties.ToList();

            if (propList.Count == 0)
            {
                // Nothing to write
                return;
            }

            writer.WriteStartObject();

            foreach (var prop in propList)
            {
                writer.WritePropertyName(prop.Name);
                JsonSerializer.Serialize(writer, prop.Value, options);
            }

            writer.WriteEndObject();
        }
    }

Used & tested like so:

           [Fact]
        public void Serialize_NestedExceptions_ContainsNulls_Success()
        {
            var converter = new ExceptionConverter<Exception>();

            var jsonOpts = new JsonSerializerOptions();
            jsonOpts.Converters.Add(converter);

            Exception ex;

            try
            {
                throw new ArgumentOutOfRangeException("MyParamName", 42, "Some out of range error occured");
            }
            catch (Exception inny)
            {
                try
                {
                    throw new Exception("I AM ERROR", inny);
                }
                catch (Exception outer)
                {
                    ex = outer;
                }
            }

            var json = JsonSerializer.Serialize(ex, jsonOpts);

            Assert.Contains("42", json);
            Assert.Contains("MyParamName", json);
            Assert.Contains("I AM ERROR", json);
            Assert.Contains("Some out of range error occured", json);
            Assert.Contains("null", json);
        }

@eiriktsarpalis
Copy link
Member

@bdriggs-axian note that you can avoid much of the reflection code by adding by adding where TExceptionType : Exception constraint at your type definition. It might also make sense to wrap that with a JsonConverterFactory.

@ghost ghost locked as resolved and limited conversation to collaborators Nov 22, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants