springfox / springfox

Automated JSON API documentation for API's built with Spring
http://springfox.io
Apache License 2.0
5.93k stars 1.54k forks source link

open-api 3.0.3 servers section missing for spring webflux #3810

Closed gdinant closed 2 years ago

gdinant commented 3 years ago

Hello everyone,

I have been running springfox with traditional Spring applications under tomcat for some time and I'm really happy with it.

Lately I started with webflux applications under netty. The same springfox configuration is applied to both projects, there is one slight difference in the end is that the servers section in the api-docs is missing which led to the swagger-ui being unusable.

Would you have any idea why so? Either I'm missing some settings or if there is really something odd in the setup of webflux apps in springfox. I couldn't find any leads on this topic.

Thanks for your support

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>
gdinant commented 3 years ago

Side note, I tried for the sake of testing to set the server manually in the Docket to see whether that would influence somehow the output but it didn't.

@Bean
public Docket configureControllerPackageAndConvertors() {

    return new Docket(DocumentationType.OAS_30).select()
        .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
        .build()
        .servers(new Server("http://localhost:9000", "test", "test", List.of(), List.of()));
}

Regarding this I'm a bit confused by how DocumentationBuilder works.

ApiDocumentationScanner does the following:

public Documentation scan(DocumentationContext context) {
    ApiListingReferenceScanResult result = apiListingReferenceScanner.scan(context);
    ApiListingScanningContext listingContext = new ApiListingScanningContext(context,
        result.getResourceGroupRequestMappings());

    Map<String, List<ApiListing>> apiListings = apiListingScanner.scan(listingContext);
    Set<Tag> tags = toTags(apiListings);
    tags.addAll(context.getTags());
    DocumentationBuilder group = new DocumentationBuilder()
        .name(context.getGroupName())
        .apiListingsByResourceGroupName(apiListings)
        .produces(context.getProduces())
        .consumes(context.getConsumes())
        .host(context.getHost())
        .schemes(context.getProtocols())
        .basePath(ROOT)
        .extensions(context.getVendorExtentions())
        .tags(tags);

    Set<ApiListingReference> apiReferenceSet = new TreeSet<>(listingReferencePathComparator());
    apiReferenceSet.addAll(apiListingReferences(apiListings, context));

    group.resourceListing(r ->
        r.apiVersion(context.getApiInfo().getVersion())
            .apis(apiReferenceSet.stream()
                .sorted(context.getListingReferenceOrdering())
                .collect(toList()))
            .securitySchemes(context.getSecuritySchemes())
            .info(context.getApiInfo())
            .servers(context.getServers()));
    return group.build();
}

servers will be added to ResourceListing but in DocumentationBuilder.build() the servers will be compiled not from the servers contained in ResourceListing but from the local attributes servers which isn't settable.

public Documentation build() {
    return new Documentation(
        this.groupName, 
        this.basePath, 
        this.tags, 
        this.apiListings, 
        this.resourceListing, 
        this.produces, 
        this.consumes, 
        this.host, 
        this.schemes, 
        (Collection)this.servers.values().stream().map(ServerBuilder::build).collect(Collectors.toList()), 
        this.externalDocumentation, 
        this.vendorExtensions);
}

So two issues here:

1) Why is the inferred URL isn't working with WebFlux 2) In the event of a bug somewhere, how I can set this value manually?

Xiejinhua123 commented 3 years ago

I also found that after the value of servers is set, there is no change to the front-end, and the host configuration is not reflected in the front-end. It seems that only the basic configuration takes effect.The configuration information is the same as above.

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

gdinant commented 3 years ago

I'd like it fixed :)

Jipos commented 3 years ago

The problem in the ApiDocumentationScanner, mentioned by @gdinant isn't the only issue.

If you add the following workaround:

  // Override Springfox bean to correctly set servers
  // See https://github.com/springfox/springfox/issues/3810
  @Primary
  @Component
  public static class CustomApiDocumentationScanner extends ApiDocumentationScanner {

    public CustomApiDocumentationScanner(
        ApiListingReferenceScanner apiListingReferenceScanner,
        ApiListingScanner apiListingScanner) {
      super(apiListingReferenceScanner, apiListingScanner);
    }

    @Override
    public Documentation scan(DocumentationContext context) {
      Documentation documentation = super.scan(context);
      documentation.getResourceListing().getServers().forEach(documentation::addServer); // <=== FIX ===
      return documentation;
    }

  }

the servers get set correctly in the Documentation object, but they still aren't returned correctly when requesting /v3/api-docs.

This is because the OpenApiControllerWebMvc.getDocumentation method, which handles requests to /v3/api-docs, and which retrieves the Documentation object, which the above code snippet fixes, also executes the following:

      OpenAPI oas = this.mapper.mapDocumentation(documentation);
      OpenApiTransformationContext<HttpServletRequest> context = new OpenApiTransformationContext(oas, servletRequest);
      List<WebMvcOpenApiTransformationFilter> filters = this.transformations.getPluginsFor(DocumentationType.OAS_30);

      WebMvcOpenApiTransformationFilter each;
      for(Iterator var8 = filters.iterator(); var8.hasNext(); context = context.next(each.transform(context))) {
        each = (WebMvcOpenApiTransformationFilter)var8.next();
      }

One of the filters, which it applies on the context is the WebMvcBasePathAndHostnameTransformationFilter. What this class does is:

  public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) {
    OpenAPI openApi = context.getSpecification();
    context.request().ifPresent((servletRequest) -> {
      ForwardedHeaderExtractingRequest filter = new ForwardedHeaderExtractingRequest(servletRequest, new UrlPathHelper());
      openApi.servers(Collections.singletonList(SpecGeneration.inferredServer(this.requestPrefix, filter.adjustedRequestURL())));
    });
    return openApi;
  }

It replaces the list of servers in the OpenAPI object (which is the POJO version of the OAS3 spec) with a default value.

This issue is related to #3735.

You can add your own filter, to try to undo the problem. Unfortunately, after the WebMvcBasePathAnedHostnameTransformationFilter is executed, the openApi object no longer contains the necessary server information.

A workaround is to configure your own filter with the necessary information so that it can set the information. Hardcoding the information would work too.

@Order(0)
@Component
public class ServerSettingOpenApiTransformationFilter implements WebMvcOpenApiTransformationFilter {

  public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) {
    OpenAPI openApi = context.getSpecification();

    io.swagger.v3.oas.models.servers.Server server = new io.swagger.v3.oas.models.servers.Server();
    server.setUrl(https://foobar.com);
    server.setDescription("Foobar");

    openApi.servers(List.of(server));

    return openApi;
  }

  public boolean supports(DocumentationType delimiter) {
    return delimiter == DocumentationType.OAS_30;
  }
}

Since the WebMvcBasePathAnedHostnameTransformationFilter is annotated with @Order(-2147483648), which is the highest precedence, it will get executed first, and thus allow us to override it's changes. If you omit the @Order(0) annotation, it will work too, since Spring will give unannotated components a lower precedence than annotated ones.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] commented 2 years ago

This issue has been automatically closed because it has not had recent activity. Please re-open a new issue if this is still an issue.