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

Fix https://github.com/tmenier/Flurl/issues/794 #800

Merged
merged 1 commit into from
Jan 15, 2024

Conversation

rwasef1830
Copy link
Contributor

@rwasef1830 rwasef1830 commented Jan 10, 2024

PR containing failing test demonstrating issue #794
@tmenier

Issue is due to stream.Length accessing Length property throws on GzipStream in DefaultJsonSerializer.

@rwasef1830 rwasef1830 changed the title Added failing test for issue https://github.com/tmenier/Flurl/issues/794 Fix https://github.com/tmenier/Flurl/issues/794 Jan 10, 2024
@rwasef1830
Copy link
Contributor Author

rwasef1830 commented Jan 10, 2024

Fixed by removing the length check. It's not always possible to know the length of the HTTP response stream. If the server returns an undeserializable or empty body, it's better to just throw exception. An empty string is not the same as "null" which is a valid JSON that deserializes to null. An empty string is invalid JSON.

@rwasef1830
Copy link
Contributor Author

whoops, will fix that.

@tmenier
Copy link
Owner

tmenier commented Jan 12, 2024

Yeah, it should be fixable, just not quite that simple.

@tmenier
Copy link
Owner

tmenier commented Jan 12, 2024

I should also look into getting the build/test workflow to run automatically when a PR is opened, I don't think it was ever my intent to require an approval there

@rwasef1830
Copy link
Contributor Author

rwasef1830 commented Jan 12, 2024

@tmenier fixed, the assert just needed to change from checking null response to checking for FlurlParsingException (which is the correct behavior).

This could be considered a breaking change for an edge case behavor but it's a correction of a previously incorrect behavior. Not sure if it warrants version bump or not.

@tmenier
Copy link
Owner

tmenier commented Jan 12, 2024

Previously an empty steam resulted in a null object. How is that "incorrect"? To me it makes more sense intuitively than throwing an exception. Would something like this fix your scenario and not break the other test?

public T Deserialize<T>(Stream stream) =>
    stream == null ? default :
    steam.CanRead && stream.Length == 0 : default :
    JsonSerializer.Deserialize<T>(stream, _options);

Need to step away, sorry in advance if I don't get back to this today.

@rwasef1830
Copy link
Contributor Author

rwasef1830 commented Jan 12, 2024

The problem is you cannot touch stream.Length and stream is never null. The problem is specifically JSON. There is no such thing as a valid JSON that's composed of an empty string.

I tried using various way of getting the stream length but the issue stems from HttpBaseResponse underneathe, it throws when you touch Length in ResponseHeadersRead mode. Apparently it's not read from the HTTP response headers, and the server could omit it altogether if it wanted.

A more sophisticated way perhaps is try to read a single byte from the stream, and if we immediately get empty, then we could return empty and preserve the existing behavior, but then if we got anything at all, we'll need to wrap the stream into another stream decorator that "rewinds" back that single character.

It would work but it would entail an extra Stream wrapper allocation. If that's a good enough solution I can implement it.

@tmenier
Copy link
Owner

tmenier commented Jan 15, 2024

Thanks for your work on this. You're right that my suggestion didn't work, but it was close. I tested with CanSeek instead of CanRead and got the desired result:

public T Deserialize<T>(Stream stream) =>
    stream.CanSeek && stream.Length == 0 ? default :
    JsonSerializer.Deserialize<T>(stream, _options);

There is no such thing as a valid JSON that's composed of an empty string.

You might be right about that, but then I suspect Newtonsoft had it wrong all along. I looked back to why I did it this way initially and it was specifically to fix that failing test on switching from Newtonsoft to STJ. I wanted to minimize the surprises people encountered when upgrading to 4.0. The recommended way to deserialize a stream in Newtonsoft will return null on an empty stream vs failing. Or at least that's my assumption, given that my test did not fail when Newtonsoft was the default.

I know the code above isn't perfect, but I think it solves your issue without breaking any existing test. You might have been able to convince me to skip the zero-length check in the 4.0 release, but doing it now could break someone in a minor release.

@rwasef1830
Copy link
Contributor Author

@tmenier Thank you for an awesome library and for responding to my PR. I had considered using CanSeek but I was worried that it might throw but hadn't actually tested it. I agree with your considerations, we should keep surprises to a minimum in minor versions updates.

I will amend my PR shortly to the desired behavior.

@rwasef1830
Copy link
Contributor Author

@tmenier Here you go all squashed and cleaned up.

@tmenier tmenier merged commit 4daa713 into tmenier:dev Jan 15, 2024
1 check passed
@tmenier
Copy link
Owner

tmenier commented Jan 15, 2024

I'll get it released soon. Thanks again!

@rwasef1830 rwasef1830 deleted the issue-794 branch January 16, 2024 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants