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
Regression from 13.6.2 to 13.7.0 #2995
Comments
What is the value of the Please check that your openApi specification document matches your data. |
Thanks for the quick reply, the exception is occurring in throw new SwaggerException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); The <PackageReference Include="NSwag.MSBuild" Version="13.7.0"> |
I think you might be hitting the breaking change I saw:
|
The service is returning data when called from the Swagger web page, I'm afraid it fails when some of the details gets deserialised. Ok<PackageReference Include="NSwag.AspNetCore" Version="13.3.0" />
<PackageReference Include="NSwag.MSBuild" Version="13.3.0"> No content<PackageReference Include="NSwag.AspNetCore" Version="13.7.0" />
<PackageReference Include="NSwag.MSBuild" Version="13.3.0"> |
Your swagger returns data, so I assume it's a 200 ? Why are you getting a 204 when invoking from your code? Are you sure it's the same request ? |
Is this with asp.net core? |
Sorry I didn't phase it properly, the client is WPF4.7 (where the exception is happening), and the server is netcoreapp3.1. |
@jeremyVignelles I can confirm, from the swagger web page it works ok: |
Your swagger UI screenshot shows that your server is returning a 204, but also tells you that it was undocumented. The client code is working as expected, and you should document that your server may return 204s |
in the Responses there is a 200, that was the value that was evaluated before (the 204 is related to the headers, not to the content). What do you mean by documenting 204, (where or how)? |
The 200 you see is the documented expected HTTP Status code. |
I see, the <Exec Command="$(NSwagExe_Core31) run $(ProjectDir)AlpAmsApi.nswag" /> how do I set that in the .nswag file? essentially, without if (status_ == "200")
{
var objectResponse_ = await ReadObjectResponseAsync<Asset>(response_, headers_).ConfigureAwait(false);
return objectResponse_.Object;
}
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new SwaggerException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
} and now regardless it is defined it produces this: if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<Asset>(response_, headers_).ConfigureAwait(false);
return objectResponse_.Object;
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new SwaggerException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
} I think it should still handle 204 because that was expected regardless it was defined or not. In any case if it is required, it should be possible to set it in the |
The .nswag is just the generator's configuration. It's not its business to know about the 200/204.
The old code indeed always handled 200 and 204, more on that below.
That should not happen. If you declare a 204, a if branch should be created for that 204. Could you check that?
Why do you expect 204 to be always generated? It's your own use case that decides if the API can or cannot generate a 204. If you decide that your API doesn't return a nullable reference type (C#9), what should the client return in case of a 204 ? You can't return null because that would contradict the non-nullability of the result. I really don't know what to do, but I'm in favor of being explicit as to when a specific status code is expected. |
Ok, I'm starting to see what is happening, I'm defining the public class AssetController : AuthoriseController
{
[HttpGet]
public Task<Asset> GetAsset(int assetId, int? version) => _assetService.GetAsset(assetId, version);
} [Route("api/[controller]/[action]")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize]
public abstract class AuthoriseController : Controller
{
... 204 is not generated unless the attribute is defined at a method level Even with that, I can't start decorating every single method of every controller in my apps. The assumption on your previous version was correct: if (status_ == "200")
{
return;
}
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new SwaggerException( ...
} If a service returns an object that can be null, then the client should be able to get null. (regardless of the c# version). I think you need to differentiate between a service returning null (on purpose) and the http response being truncated due to a network issue. |
Maybe this will help ? https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/conventions?view=aspnetcore-3.1#apply-web-api-conventions What does it generate when you have this attribute on the method ? a return on the 204 case, or a throw?
It's just a different assumption : If you say "My method returns an orange", you can interpret it either as "It may or may not return an orange", or as "It will always return exactly one orange".
I agree with that. What you are saying is that if a controller method returns a When returning
I'd really like to have a minimal repro for this to really understand what's going on. Feel free to fork https://github.com/jeremyVignelles/TestNSwagNetCoreApp for that. |
Thanks Jeremy, I've created a PR showcasing the situation jeremyVignelles/TestNSwagNetCoreApp#1 |
The link to the repro is https://www.github.com/paulovila/TestNSwagNetCoreApp/tree/master/ . Thanks for the code, maybe I'll find time to have a look at this. |
Related to #1259 in that both should be fixed at the same time IMO. |
This is a breaking change and the version should have been changed to 14.0.0 instead of 13.7.0 |
@RicoSuter, @jeremyVignelles I'd suggest to unlist that package version from nuget, in order to avoid the error spreading across to other users. |
We talked with @RicoSuter on gitter this evening about what to do with this issue and #1259 . The code that was there before #2959 had a special case that would return null in case a 204 was returned, be it declared in the spec or not. It was good for users in that they didn't have to declare a 204, but it had several drawbacks:
I came and tried to implement #2959 for NullableReferenceTypes. I thought it wouldn't break anything, but it turns out I was wrong. There are in fact several issues: Nullability checksI tightened the nullability checks, based on the spec. If the spec didn't declare a return type as nullable, the client code wouldn't accept it (instead of returning null in a NRT context, which would be even worse because there would be a NullReferenceException when a user was expecting to be safe because they enabled the NRT) The fix is to declare nullability properly in the spec, see #3011 and #3014 Unexpected 204I removed that 204 special case. If a 204 is expected to be returned from the API, it must be declared in the spec, and everybody should be fine. It turns out that's not the case. Take this code for example (courtesy of @paulovila ) [HttpGet]
public Task<HelloWorldModel?> NullableModel() => Task.FromResult(default(HelloWorldModel)); ASP.net's API explorer does not generate a 204 response for this code, despite the result being nullable. This is either a bug that should be reported, or at least a behavior we didn't expect, because the result of this, is indeed a 204 with a null body. The fix to that would be to declare a 204 result on every API like so: [HttpGet]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(HelloWorldModel), StatusCodes.Status200OK)]
public Task<HelloWorldModel?> NullableModel() => Task.FromResult(default(HelloWorldModel)); This would be a tedious task, and we think that it could be done in a better way. Since the issue happens in ASP.Net core, we'd like to provide the user with a ready-to-use Comments and PRs welcome ! When 2 responses codes are declared, only one can be the result (in C#)If we declare both 200 and 204, we still have the issue that 200 is the result, but 204 throws an exception. We are aware this is a regression, and would like to fix this by fixing #1259 . See the plan here : #1259 (comment) However, be aware that this will likely be a breaking change for others, and that we need to be careful about what we're doing. Hope you enjoyed the detailed answer, feel free to comment if you have more questions. |
Just checked, in the 13.6.2 version, if the spec was correctly specifying 204 this already throw an exception: So the old behavior is just "as expected" when the spec is wrong (204 missing) and the server returned 204... So in the end the client generator is now correct and before it wasnt - there is only one way to fix this:
|
Would it be possible to define at a base class level an attribute with the desired behaviour for all the methods in the inherited classes ? |
This needs to be tested if asp allows that - otherwise we can build a custom processor which does that (second point in the list). |
It's also possible to work around this by stopping ASP.NET core from auto-generating 204 responses for null results by removing the HttpNoContentOutputFormatter as in the snippet below. services.AddMvcCore(options =>
{
options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
}) |
Hi @RicoSuter @jeremyVignelles is there any update on this issue? |
#3038 (comment) - here it says
We have an event processing system. When a client sends an event, it may be routed, in which case we send back a 202 and you get a routing code, or it may be 204 because it was not routed, and there is no routing code to send. Both are 'OK' outcomes. We ended up having to change our code to always return 202 and return a model, which IMHO is less RESTful. I think if someone has bothered to explicitly document that their API returns a 202 and a 204, then NSwag should just pick that up without passing judgement or requiring them to reimplement their API. |
Thanks @mattwhitfield for your detailed answer. I agree that NSwag should not have to make any decision for those kind of cases, but C# is C#, and you can't return two totally unrelated types from your methods. The default behavior there will be that NSwag will pick one result as the return type, but the other one will throw, which is not consistent with the fact that this is also a success. That said, for these kind of use cases, I'd advise that you use the WrapDTO option which should be able to do just that, though I didn't test it myself. |
Hey @jeremyVignelles whilst I agree with your statement in general, I believe that's not really what's being asked here. What I think we're all looking for, myself included (and @mattwhitfield, correct me if I'm wrong) is for the NSwag client generator to understand As @mattwhitfield described in his scenario, his API either returns Right now, the API client throws an exception on |
@augustoproiete is spot on - as a consumer of an API I expect there to be a result from a method, which may be null. An example would be To my mind, that maps pretty directly to the scenario in question - 2xx may or may not have an entity - but if there is an entity it is a single type. 4xx/5xx signify something going really wrong - and so I'd expect an exception. I do fully agree that supporting 200 with type X and 202 with type Y would be a bad thing overall. |
Regarding the "Unexpected/undocumented 204" issue, I reported that on aspnetcore's bugtracker I looked into NSwag's code this afternoon to find a way to properly merge 2xx, but it requires heavy refactorings I had no time to do. I think that we could merge all success status codes (as long as types are compatible), and also merge that with the "default" response type. |
FWIW, we were able to workaround this issue by copying
That's not a great long-term workaround because of the fact that you have to keep up with changes to the .liquid file but it's a workaround nonetheless. |
Sure @jeremyVignelles take your time, temporarily what someone can do is to change the return type so that the null value is expected inside of an object that is never null, Task<MyClass> GetMyClass() => _myInterface.GetMyClassInternal(); by async Task<(MyClass, string)> GetMyClass()
{
var r = await _myInterface.GetMyClass();
return (r, r != null);
} The generated client, instead of returning a |
@RicoSuter @jeremyVignelles is this still on your radar? |
Hi, The goal would be to merge all success status code in either one type, one nullable type (200|201 for example) or using a TypeUnion (would require a dependency) if the scenario is more complex. At the time, I have no idea what the path would be to introduce such change, so if you want to have a look... |
Somehow the problem didn't get fixed with 13.7.0. Here is an endpoint: #nullable enable
class MyData {}
/// <response code="204" nullable="true"></response>
[HttpGet("Foo/MyData")]
[ProducesResponseType(typeof(MyData), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(Dictionary<string, string[]>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(MessageModel), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(MessageModel), StatusCodes.Status404NotFound)]
public async Task<MyData?> MyEndpoint(Guid orderId) =>
null; Here is schema produced: "Foo/MyData": {
"get": {
"tags": [ ],
"summary": "",
"operationId": "Foo_MyEndpoint",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyData"
}
}
}
},
"204": {
"description": "",
"content": {
"application/json": {
"schema": {
"nullable": true
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "array",
"nullable": true,
"items": {
"type": "string",
"nullable": true
}
}
}
}
}
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageModel"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageModel"
}
}
}
}
}
}
}, And this is code generated: public async System.Threading.Tasks.Task<ApiResponse<MyData>> Foos_GetMyDataAsync(System.Threading.CancellationToken cancellationToken)
{
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append("Foo/MyData");
var client_ = _httpClient;
var disposeClient_ = false;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("GET");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int)response_.StatusCode;
if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<MyData>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
return new ApiResponse<MyData>(status_, headers_, objectResponse_.Object);
}
else
if (status_ == 204)
{
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null);
}
else
if (status_ == 400)
{
var objectResponse_ = await ReadObjectResponseAsync<System.Collections.Generic.IDictionary<string, System.Collections.Generic.ICollection<string>>>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<System.Collections.Generic.IDictionary<string, System.Collections.Generic.ICollection<string>>>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 401)
{
var objectResponse_ = await ReadObjectResponseAsync<MessageModel>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<MessageModel>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == 404)
{
var objectResponse_ = await ReadObjectResponseAsync<MessageModel>(response_, headers_, cancellationToken).ConfigureAwait(false);
if (objectResponse_.Object == null)
{
throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
}
throw new ApiException<MessageModel>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
}
}
finally
{
if (disposeResponse_)
response_.Dispose();
}
}
}
finally
{
if (disposeClient_)
client_.Dispose();
}
} Which clearly states that it throws for 204 when it shouldn't. Versions used: NSwag.AspNetCore Version="13.10.7" - to generate |
Another problem is throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); Model says it's a NRT, but attributes may condradict with it but I didn't find any information about how do I specify that response may be null in some cases but can't in anothers. I've only found an option to set the default, but nothing about overriding it on per-handle basis. For example, I have this handler that returns Any workaround/insights for this? cc @RicoSuter @jeremyVignelles |
I have encountered this with the latest version of NSwag and downgraded to 13.6.2 - however the problem persists somewhat differently. I apologise if this is covered before (it seems to be hinted at in this comment - default response types) but I have a swagger file with a According to the spec:
If I explicitly add a So I am stuck somewhat because 13.7.0 and newer don't seem to understand what a HTTP 204 implies, and 13.6.2 seems to use the schema from |
My solution was to remove the HttpNoContentOutputFormatter as described above and add this comment on my endpoint /// <response code="200" nullable="true">{something}</response> Then, the generated client code is : if (status_ == 200)
{
var objectResponse_ = await ReadObjectResponseAsync<Payload>(response_, headers_, cancellationToken).ConfigureAwait(false);
return objectResponse_.Object;
} .NET 5 |
@r-rc-christopher I think that does indeed work around the runtime error calling the service. However, the generated API client response type will be Btw. instead of removing HttpNoContentOutputFormatter, it can also be configured to not do this status code changing (nut sure what else it does, tbh):
|
Hello, no fix in 13.8 but since we migrated to .NET 7, upgrade was mandatory for us. |
Did not work for me. I still get the null response exception
via regenerated client. Am I missing something?
I am using the client in .NET Framework 4.8 and I don't use nullable types anywhere. Also |
Any updates on this? |
What worked to me was adding both 200 and 204 response types and explicitly allow nullable for 204 one: /// ...
/// <response code="204" nullable="true">No data.</response>
[HttpGet("my-data")]
[ProducesResponseType(typeof(DataModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(DataModel), StatusCodes.Status204NoContent)]
// ... more response types
public async Task<IActionResult> ... |
A SwaggerException is generated in the C# client when upgrading from 13.6.2 to 13.7.0
Beware that the entity returned contains types like enums, DateTime?, byte...
How can I determine what field is failing?
Sample method OK 13.6.2
Sample method failing 13.7.0
The text was updated successfully, but these errors were encountered: