Hello
If i return a 204 nocontent, the typescript throws a swagger exception. However, it is a successfull return code. blobtotext works and then the pipe throws the exception. Am i missing something?
Thank you
Can you post some sample Swagger spec + TS output?
"delete": {
"tags": [
"HaileeResponses"
],
"operationId": "HaileeResponses_Delete",
"parameters": [
{
"type": "string",
"name": "id",
"in": "path",
"required": true,
"format": "guid",
"x-nullable": false
}
],
"responses": {
"200": {
"x-nullable": true,
"description": "",
"schema": {
"type": "file"
}
},
"204": {
"description": ""
},
"400": {
"description": "Runtime error",
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"error": {
"$ref": "#/definitions/ErrorViewModel"
}
}
}
}
}
}
delete(id: string): Observable<FileResponse | null> {
let url_ = this.baseUrl + "/HaileeResponses/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json"
})
};
return this.http.request("delete", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processDelete(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processDelete(<any>response_);
} catch (e) {
return <Observable<FileResponse | null>><any>_observableThrow(e);
}
} else
return <Observable<FileResponse | null>><any>_observableThrow(response_);
}));
}
protected processDelete(response: HttpResponseBase): Observable<FileResponse | null> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
const fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
const fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
return _observableOf({ fileName: fileName, data: <any>responseBlob, status: status, headers: _headers });
} else if (status === 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("A server error occurred.", status, _responseText, _headers);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 ? Anonymous7.fromJS(resultData400) : new Anonymous7();
return throwException("A server error occurred.", status, _responseText, _headers, result400);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<FileResponse | null>(<any>null);
}
Ah, I think 200 with file + 204 is a special case and may not be correctly handled at the moment...
There is no file though. I am not sure why it thinks that. I didnt add 200+file either
Can you post the C# controller operation signature?
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
var entity = _haileeService.GetHaileeResponseOperation(id);
if (entity == null)
{
return NotFoundWithMessage($"Hailee Response Operation not found {id}");
}
await _haileeService.DeleteHaileeResponse(id);
return NoContent();
}
and i have a IOperationProcessor as below to add the responses as mentioned in #1353
public class SwaggerResponseTypeAppender : IOperationProcessor
{
public async Task<bool> ProcessAsync(OperationProcessorContext context)
{
var methodResponse = context.MethodInfo.GetCustomAttributes<ProducesResponseTypeAttribute>();
if (context.OperationDescription.Method != SwaggerOperationMethod.Delete && !methodResponse.Any())
{
throw new ApiException(MyExceptionType.MissingResponseType, context.OperationDescription.Operation.OperationId);
}
if (context.OperationDescription.Method == SwaggerOperationMethod.Put || context.OperationDescription.Method == SwaggerOperationMethod.Post)
{
await AddResponse(context, HttpStatusCode.UnprocessableEntity, typeof(ValidationFailedResultViewModel));
}
if (context.OperationDescription.Method == SwaggerOperationMethod.Get || context.OperationDescription.Method == SwaggerOperationMethod.Put)
{
await AddResponse(context, HttpStatusCode.NotFound, null);
}
if (context.OperationDescription.Method == SwaggerOperationMethod.Delete)
{
await AddResponse(context, HttpStatusCode.NoContent, null);
}
await AddResponse(context, HttpStatusCode.BadRequest, typeof(MyExceptionResultViewModel));
return await Task.FromResult(true);
}
private async Task AddResponse(OperationProcessorContext context, HttpStatusCode httpStatusCode, Type type)
{
if (type != null && !context.SchemaResolver.HasSchema(type, false))
{
await context.SchemaGenerator.GenerateAsync(type, null, context.SchemaResolver);
}
var response = new SwaggerResponse();
if (type != null)
{
response.Schema = context.SchemaResolver.GetSchema(type, false);
}
switch (httpStatusCode)
{
case HttpStatusCode.UnprocessableEntity:
response.Description = "Validation error";
break;
case HttpStatusCode.BadRequest:
response.Description = "Runtime error";
break;
case HttpStatusCode.NotFound:
response.Description = "Record not found error";
break;
}
int httpCode = (int)httpStatusCode;
var kvp = new KeyValuePair<string, SwaggerResponse>(httpCode.ToString(), response);
context.OperationDescription.Operation.Responses.Add(kvp);
}
}
A result of IActionResult automatically adds a 200 file response. You can change that with the [SwaggerResponse] attribute (see docs)
I see, can i remove it with IOperationProcessor?
Yes
When I removed 200 from via IOperationProcessor, then the problem is fixed.
New generated code is below, and it doesn't throw exception
protected processDelete(response: HttpResponseBase): Observable<void> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }};
if (status === 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return _observableOf<void>(<any>null);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 ? Anonymous7.fromJS(resultData400) : new Anonymous7();
return throwException("A server error occurred.", status, _responseText, _headers, result400);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<void>(<any>null);
}
I encountered the same problem but I can't remove the ProducesResponseType 200 attribute.
How can I solve or workaround this? 馃
Edit: I am not dealing with a file. This is the part of my swagger.json:
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Incident"
}
}
},
"204": {
"description": "Success"
},
"401": {
"description": "Unauthorized: Authentication not valid",
"schema": {
"$ref": "#/definitions/Indicents",
"type": "Indicents",
"example": {
// ...
}
}
},
"404": {
"description": "Not Found"
},
"500": {
"description": "Internal Server Error: A major internal server problem occurred",
"schema": {
"$ref": "#/definitions/Indicents",
"type": "Indicents",
"example": {
// ...
}
}
}
},
@RSuter I tried creating a workaround for this. In TS it is possible to "just" return everything all 2xx return codes have responded. This is my change: https://github.com/FabianTe/NSwag/commit/7d1ea538281c77d3b5882e52bba8a761a61b9c55
Edit: I figured out my local problem. This fix works and I'll submit it.
@FabianTe please create a PR so that we can look into it and review it...
I have a similar issue where my .net core API returns:
NoContent() to a Task<IActionResult> controller method response when the search provides no results.
Ok(result) when there are search results.
Which has the following attribute:
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(IEnumerable<SearchResponse>), StatusCodes.Status200OK)]
And produces an exception on receiving a 204, which is very unexpected behaviour. Note the explicit 204 exception block.
if (status === 500) {
return response.text().then((_responseText) => {
return throwException("Server Error", status, _responseText, _headers);
});
} else if (status === 204) {
return response.text().then((_responseText) => {
return throwException("Success", status, _responseText, _headers);
});
} else if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(SearchResponse.fromJS(item));
}
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
Related to #2995 and #1259
Thanks @jeremyVignelles , do you think it is still related if the controller method has both attributes:
[ProducesResponseType(typeof(IEnumerable<SearchResponse>), StatusCodes.Status200OK)]
and
[ProducesResponseType(StatusCodes.Status204NoContent)]
Is this valid and should the nswag client manage that? I'll have a look at the other tickets and maybe repost there.
The above is supposed to cater for a search that produces no results.
I'd argue that an empty response should return 200 with [], but there are probably APIs out there that work that way (can return both 200 and 204 without content).
I think that for those cases, the 204 should map to a null result. and should not throw.