Skip to content

APIResponses constructed programmatically are not correctly analyzed #758

Closed
@EstebanDugueperoux2

Description

@EstebanDugueperoux2

Describe the bug
All our ressources require auth and have their error response follow "Problem Details" RFC.
Consequently HTTP 401 and 403 responses are commons.

This is an example of API describing these commons API responses:

@RestController
@ApiResponses(value = {
    @ApiResponse(responseCode = "401", description = "Invalid authentication.", content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}),
    @ApiResponse(responseCode = "401", description = "Invalid authentication.",content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}),
    @ApiResponse(responseCode = "403", description = "Missing authorities.",content = {@Content(schema = @Schema(implementation = Problem.class), mediaType = APPLICATION_PROBLEM_JSON_VALUE)}) })
public class HelloController<T> {

  private static final Collection<String> CURRENCIES = new ArrayList<>();
  static {
    CURRENCIES.add("EUR");
    CURRENCIES.add("USD");
  }

  @GetMapping
  @Operation(description = "Get all currencies", summary = "getAllCurrencies")
  @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "All currencies returned") })
  public ResponseEntity<Collection<String>> getAllCurrencies() {
    return ok(CURRENCIES);
  }
}

From that we have a correctly generated swagger:

{
   "openapi":"3.0.1",
   "info":{
      "title":"OpenAPI definition",
      "version":"v0"
   },
   "servers":[
      {
         "url":"http://localhost",
         "description":"Generated server url"
      }
   ],
   "paths":{
      "/":{
         "get":{
            "tags":[
               "hello-controller"
            ],
            "summary":"getAllCurrencies",
            "description":"Get all currencies",
            "operationId":"getAllCurrencies",
            "responses":{
               "401":{
                  "description":"Invalid authentication.",
                  "content":{
                     "application/problem+json":{
                        "schema":{
                           "$ref":"#/components/schemas/Problem"
                        }
                     }
                  }
               },
               "403":{
                  "description":"Missing authorities.",
                  "content":{
                     "application/problem+json":{
                        "schema":{
                           "$ref":"#/components/schemas/Problem"
                        }
                     }
                  }
               },
               "200":{
                  "description":"All currencies returned",
                  "content":{
                     "*/*":{
                        "schema":{
                           "type":"array",
                           "items":{
                              "type":"string"
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   },
   "components":{
      "schemas":{
         "Problem":{
            "type":"object",
            "properties":{
               "instance":{
                  "type":"string",
                  "format":"uri"
               },
               "type":{
                  "type":"string",
                  "format":"uri"
               },
               "parameters":{
                  "type":"object",
                  "additionalProperties":{
                     "type":"object"
                  }
               },
               "status":{
                  "type":"integer",
                  "format":"int32"
               },
               "title":{
                  "type":"string"
               },
               "detail":{
                  "type":"string"
               }
            }
         }
      }
   }
}

To help developer, we provide a generic OpenApiCustomiser to provide these standards response:

public class ResponseRegistrationCustomizer implements OpenApiCustomiser {

	private final List<Map.Entry<String, ApiResponse>> responsesToRegister;

	public ResponseRegistrationCustomizer(@NonNull List<Map.Entry<String, ApiResponse>> responsesToRegister) {
		this.responsesToRegister = responsesToRegister;
	}

	@Override
	public void customise(OpenAPI openApi) {
		responsesToRegister.forEach(entry -> openApi.getComponents().addResponses(entry.getKey(), entry.getValue()));
		log.debug("Registered {} responses in OpenAPI Specification", responsesToRegister.size());
	}
}

and an uncoupled configuration to provide these communs responses:

@Configuration
public class SecurityProblemResponsesConfiguration {

	private static final String HTTP_401_NO_TOKEN = "http401NoToken";
	private static final String HTTP_401_BAD_TOKEN = "http401BadToken";
	private static final String HTTP_403 = "http403";
	public static final String UNAUTHORIZED_401_NO_TOKEN_RESPONSE_REF = "#/components/responses/" + HTTP_401_NO_TOKEN;
	public static final String UNAUTHORIZED_401_BAD_TOKEN_RESPONSE_REF = "#/components/responses/" + HTTP_401_BAD_TOKEN;
	public static final String FORBIDDEN_403_RESPONSE_REF = "#/components/responses/" + HTTP_403;


	@Bean
	public Map.Entry<String, ApiResponse> http401NoTokenResponse() throws IOException {
		return simpleResponse(HTTP_401_NO_TOKEN, "Unauthorized", "Invalid authentication.");
	}
	
	@Bean
	public Map.Entry<String, ApiResponse> http401BadTokenResponse() throws IOException {
		return simpleResponse(HTTP_401_BAD_TOKEN, "Unauthorized", "Invalid authentication.");
	}

	@Bean
	public Map.Entry<String, ApiResponse> http403Example() throws IOException {
		return simpleResponse(HTTP_403, "Forbidden", "Missing authorities.";
	}

	@Bean
	public Map.Entry<String, ApiResponse> http500Response() throws IOException {
		return simpleResponse(HTTP_500, "Internal Server Error", "HTTP 500 JSON Body response example");
	}

	private Map.Entry<String, ApiResponse> simpleResponse(String code, String description) throws IOException {
		ApiResponse response = new ApiResponse().description(description).content(new Content().addMediaType(
				APPLICATION_PROBLEM_JSON_VALUE,
				new MediaType()
						.schema(new Schema<Problem>().$ref("#/components/schemas/Problem"))));
		return new AbstractMap.SimpleEntry<>(code, response);
	}
}

This help keep a REST controller more readable and formatable, especially when @ExampleObject are provided as static JSON file instead of having them directly in REST controller.

From these help classes, now developer can have following REST controller:

@RestController
@ApiResponses(value = {
    @ApiResponse(responseCode = "401", ref = SecurityProblemResponsesConfiguration.UNAUTHORIZED_401_NO_TOKEN_RESPONSE_REF),
    @ApiResponse(responseCode = "401", ref = SecurityProblemResponsesConfiguration.UNAUTHORIZED_401_BAD_TOKEN_RESPONSE_REF),
    @ApiResponse(responseCode = "403", ref = SecurityProblemResponsesConfiguration.FORBIDDEN_403_RESPONSE_REF) })
public class HelloController<T> {

  private static final Collection<String> CURRENCIES = new ArrayList<>();
  static {
    CURRENCIES.add("EUR");
    CURRENCIES.add("USD");
  }

  @GetMapping
  @Operation(description = "Get all currencies", summary = "getAllCurrencies")
  @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "All currencies returned") })
  public ResponseEntity<Collection<String>> getAllCurrencies() {
    return ok(CURRENCIES);
  }

}

but the swagger generated becomes:

{
   "openapi":"3.0.1",
   "info":{
      "title":"OpenAPI definition",
      "version":"v0"
   },
   "servers":[
      {
         "url":"http://localhost",
         "description":"Generated server url"
      }
   ],
   "paths":{
      "/":{
         "get":{
            "tags":[
               "hello-controller"
            ],
            "summary":"getAllCurrencies",
            "description":"Get all currencies",
            "operationId":"getAllCurrencies",
            "responses":{
               "401":{
                  "$ref":"#/components/responses/http401BadToken"
               },
               "403":{
                  "$ref":"#/components/responses/http403"
               },
               "200":{
                  "description":"All currencies returned",
                  "content":{
                     "*/*":{
                        "schema":{
                           "type":"array",
                           "items":{
                              "type":"string"
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   },
   "components":{
      "responses":{
         "http401NoToken":{
            "description":"Invalid authentication.",
            "content":{
               "application/problem+json":{
                  "schema":{
                     "$ref":"#/components/schemas/Problem"
                  }
               }
            }
         },
         "http401BadToken":{
            "description":"Invalid authentication.",
            "content":{
               "application/problem+json":{
                  "schema":{
                     "$ref":"#/components/schemas/Problem"
                  }
               }
            }
         },
         "http403":{
            "description":"Missing authorities.",
            "content":{
               "application/problem+json":{
                  "schema":{
                     "$ref":"#/components/schemas/Problem"
                  }
               }
            }
         }
      }
   }
}

without the "#/components/schemas/Problem".
It seems that schemas are analyzed when they exist in response annotations but not in ref.

Could you confirm me if it is a bug or it is responsibility of APIResponse builder to register this Problem schema among components?

Best Regards.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions