question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

APIResponses constructed programmatically are not correctly analyzed

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
bnasslahsencommented, Jul 1, 2020

@EstebanDugueperoux2,

Yes i have forgot to mention that as well. This is the working Test class:

@TestPropertySource(properties = "springdoc.remove-broken-reference-definitions=false")
public class SpringDocApp126Test extends AbstractSpringDocTest {

	@SpringBootApplication
	static class SpringDocTestApp {

		@Bean
		public OpenApiCustomiser responseRegistrationCustomizer(List<Map.Entry<String, ApiResponse>> responsesToRegister) {
			ResolvedSchema resolvedSchema = ModelConverters.getInstance()
					.resolveAsResolvedSchema(new AnnotatedType(Problem.class));
			return openApi -> {
				openApi.getComponents().addSchemas("Problem", resolvedSchema.schema);
				responsesToRegister.forEach(entry -> openApi.getComponents().addResponses(entry.getKey(), entry.getValue()));
			};
		}
		
	}
}

1reaction
bnasslahsencommented, Jun 30, 2020

Hi @EstebanDugueperoux2,

The #/components/schemas/Problem, is not generated by any other controllers. So it will not exist on the schema. If you declare it, on the OpenAPICustomiser, your test will pass:

@Bean
public OpenApiCustomiser responseRegistrationCustomizer(List<Map.Entry<String, ApiResponse>> responsesToRegister) {
	ResolvedSchema resolvedSchema = ModelConverters.getInstance()
			.resolveAsResolvedSchema(new AnnotatedType(Problem.class));
	return openApi -> {
		openApi.getComponents().addSchemas("Problem", resolvedSchema.schema);
		responsesToRegister.forEach(entry -> openApi.getComponents().addResponses(entry.getKey(), entry.getValue()));
	};
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Swagger declaration schema = @Schema(implementation ...
I have one API endpoint, the request body expects a HashMap. There is not much information on how to fix the "Example value"...
Read more >
F.A.Q - Springdoc-openapi
Can I customize OpenAPI object programmatically? You can Define your own OpenAPI Bean: If you need the definitions to appear globally (within every...
Read more >
Spring Boot RESTful API with Swagger 2 - DZone Integration
However, with RESTFul web services, there is no WSDL. ... message = "Successfully retrieved list"), @ApiResponse(code = 401, message = "You ...
Read more >
Speech-to-Text request construction - Google Cloud
The file must not be compressed (for example, gzip). ... Time offsets are especially useful for analyzing longer audio files, where you may...
Read more >
Using the Amazon SES API to send email - AWS Documentation
Formatted—Amazon SES composes and sends a properly formatted email message. You need only supply "From:" and "To:" addresses, a subject, and a message...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found