Monday, April 7, 2025

Spring Boot 3 serverside forwarding

Introduction

Sometimes you want to forward to another URL on the serverside in Spring Boot 3. 


The example in this blogpost is for forwarding to the Swagger JSON open-api-specification OAS /api-docs resource listing. You might need this when you want to protect that URL via Spring authentication, so it has to first come in to a controller, and only then forward it.

Solution

Below a new controller ApiController is created, with as endpoint GET /open-api. That has bearer authentication on it. And redirects to /apidocs/api-definition. For details on what api-definitions i.e grouped Open API definitions are, see here.

ModelAndView

Using a ModelAndView from Spring MVC:

  @RestController
  @SecurityRequirement(name = "bearerAuth")
  public class ApiController() {
    @Operation(
      summary = "Gets the OpenApi specification of the API",
    )
    @GetMapping("/open-api", produces = ["application/json"])
    @SecurityRequirement(name = AUTHORIZATION)
    fun getOpenApiSpecification(): ModelAndView {
      return ModelAndView("forward:/api-docs/api-definition")
    }

To have this show up in Swagger, also set in application.yml:

  springdoc:
    model-and-view-allowed: true

Servlet context

Using the servlet context, so you don't need a ModelAndView:

  @GetMapping("/open-api", produces = ["application/json"])
  @ResponseStatus(HttpStatus.OK)
  @SecurityRequirement(name = AUTHORIZATION)
  fun getOpenApiSpecification(request: HttpServletRequest, response: HttpServletResponse) {
    request.session.servletContext.getRequestDispatcher("/api-docs/api-definition").forward(request, response)
  }

This dependency is then also needed: 
  <dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
  </dependency










Wednesday, April 2, 2025

How to mock Retrofit2 response Call in a unittest

Introduction

To mock a Retrofit2 call which returns Call<Void?> in the interface client like this:

    @POST("/events")

    fun registerEvent(@Body requestDto: EventRequestDto): Call<Void?>

is not super-obvious, since it can show this error when invoked:

    Cannot invoke "retrofit2.Call.request()" because "call$iv" is null

    java.lang.NullPointerException: Cannot invoke "retrofit2.Call.request()" because "call$iv" is null

Solution

    val mockedResponseRegisterEvent: Call<Void?> = retrofit2.mock.Calls.response(null)
    every { retrofitEventServiceClient.registerEvent(any()) } returns mockedResponseRegisterEvent