Skip to content

Scalar source URLs resolve to "null/<groupName>" on second request when using GroupedOpenApi #3230

@sanvisser

Description

@sanvisser

Describe the bug

When using GroupedOpenApi beans (or springdoc.group-configs), the Scalar UI works on the first page load but breaks on every subsequent load (including hard refresh). The rendered HTML contains null/groupName as source URLs instead of the correct /v3/api-docs/groupName.

For example, the generated Scalar config contains:

  "sources":[
    {"url":"null/default","title":"default","default":false},
    {"url":"null/service-to-service","title":"service-to-service","default":false}
  ]

Root cause from analysis:

AbstractScalarController.getDocs() builds the sources array from group configs, then calls scalarProperties.setUrl(null). Although SpringBootScalarProperties is @scope("prototype"), the controller is a singleton that holds a single reference to the properties instance. The setUrl(null) call mutates that instance, so on the next request buildApiDocsUrl() reads getUrl() which returns null, and the source URLs are built as null + "/" + groupName.

Relevant flow in AbstractScalarController.getDocs():

  1. configureProperties() calls buildApiDocsUrl() which returns scalarProperties.getUrl() → correct on first call
  2. Sources are built: url + "/" + group → correct
  3. scalarProperties.setUrl(null) → corrupts the instance
  4. Next request: scalarProperties.getUrl() → null → sources become "null/default"

To Reproduce

  • Spring Boot 3.4.x
  • springdoc-openapi-starter-webmvc-scalar 2.8.15 (pulls in scalar-core 0.5.8, scalar-webmvc 0.5.8)

Minimal config:

(application.yml)

  springdoc:   
    cache:
      disabled: true 
    swagger-ui: 
      enabled: false          
    api-docs:
      path: /v3/api-docs

  scalar:
    url: /v3/api-docs
    path: /scalar
  @Bean
  fun defaultOpenApi(): GroupedOpenApi =
      GroupedOpenApi.builder()
          .group("default")
          .pathsToMatch("/**")
          .build()

  @Bean
  fun serviceApi(): GroupedOpenApi =
      GroupedOpenApi.builder()
          .group("service-to-service")
          .packagesToScan("com.example.s2s")
          .build()

scalar:
path: /scalar

Steps:

  1. Start the application
  2. Open /scalar in the browser → works correctly
  3. Refresh the page → source URLs are null/default, null/service-to-service

Workaround

We resolved the issue by removing the GroupedOpenApi beans entirely, so Scalar uses scalar.url directly without entering the sources/group code path. This works but means we lose the ability to split our API docs into separate groups (e.g. public API vs service-to-service endpoints). We'd like to keep using groups with Scalar.

Expected behavior

Source URLs should be correct on every request, e.g. /v3/api-docs/default and /v3/api-docs/service-to-service.

Screenshots
first load:

Image

second load (only pressed refresh):

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions