Exception Handling in OfficeFloor REST YAML Composition

This tutorial demonstrates how exceptions (called escalations in OfficeFloor) are handled within the YAML REST composition when integrating with Spring Boot.

Four levels of exception handling are covered:

  1. Method escalation catches the exception and routes it to a handler declared on the specific method in the YAML.
  2. Composition escalation catches the exception and routes it to a handler declared at the composition level, applying across all methods in the file.
  3. Global escalation catches the exception with an application-wide handler registered via a file in officefloor/escalation/, using the same OfficeFloor function injection pattern as all other handlers. This is the preferred OfficeFloor-native alternative to Spring's @RestControllerAdvice.
  4. Spring ControllerAdvice lets the exception propagate out of the OfficeFloor composition and be handled by Spring's @RestControllerAdvice / @ExceptionHandler mechanism. Use this when integrating with an existing Spring application that already has @ControllerAdvice handlers, or when a third-party library registers its own advice.

    Tutorial Source

The Exception

All three scenarios throw the same checked exception. Declaring it as a checked exception (extends Exception) ensures the throws clause appears in the method signature, which is required for OfficeFloor to discover and route the escalation through the YAML configuration.

public class MockException extends Exception {

    public MockException(String message) {
        super(message);
    }
}

Method Escalation

Method escalation routes an exception thrown by a specific method to a handler step declared within the same YAML composition.

YAML configuration

The escalations block sits under the step that throws the exception. Each entry maps the fully-qualified exception class name to the name of the handler step to invoke.

service:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.MethodService
  escalations:
    net.officefloor.tutorial.springrestexceptionhttpserver.MockException: handler

handler:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.MethodExceptionHandler

When service throws MockException, OfficeFloor routes execution to the handler step rather than letting the exception propagate further.

Service method

The service method simply declares and throws the exception:

public class MethodService {
    public void service() throws MockException {
        throw new MockException("thrown");
    }
}

Handler method

The handler receives the thrown exception via the @Parameter annotation, which binds the escalation object as a method parameter. From there it can inspect the exception and write a response using ObjectResponse:

public class MethodExceptionHandler {
    public void handle(@Parameter MockException ex, ObjectResponse<String> response) {
        response.send("Method handled: " + ex.getMessage());
    }
}

The @Parameter annotation is provided by OfficeFloor and signals that the parameter value is passed from the previous step's output — in the escalation case this is the thrown exception.

Composition Escalation

Composition escalation moves the escalation routing up to the composition level. Rather than declaring it on a single method, it applies to every method in the composition. This is useful when multiple service steps share a common failure mode that should be handled in one place.

YAML configuration

The composition block at the top of the YAML file holds the shared escalation mappings:

composition:
  escalations:
    net.officefloor.tutorial.springrestexceptionhttpserver.MockException: handler

service:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.CompositionService

handler:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.CompositionExceptionHandler

Any step in the composition that throws MockException — whether it is service or any other step added later — is automatically routed to the handler step.

Service and handler methods

The service class is identical in structure to the method-escalation example:

public class CompositionService {
    public void service() throws MockException {
        throw new MockException("thrown");
    }
}

The handler class is also the same pattern, just producing a different response to illustrate which level caught the exception:

public class CompositionExceptionHandler {
    public void handle(@Parameter MockException ex, ObjectResponse<String> response) {
        response.send("Composition handled: " + ex.getMessage());
    }
}

The key difference from method escalation is entirely in the YAML — the Java classes are written the same way regardless of which level handles the escalation.

Global Escalation

Global escalation registers application-wide handlers for exception types across all REST endpoints. Any endpoint that throws the exception, regardless of which YAML file it belongs to, is routed to the corresponding handler automatically.

This is the preferred approach when replacing Spring <<<@RestControllerAdvice>.>> Handler methods are plain OfficeFloor functions using the same @Parameter and ObjectResponse pattern as all other composition steps. No Spring-specific annotations are required.

Global escalation also handles exceptions thrown by governance (for example, a TransactionSystemException thrown during transaction commit), because OfficeFloor routes governance failures through the same escalation mechanism as function-level exceptions.

File naming

Create one YAML file per exception type inside officefloor/escalation/. The file name must be the fully qualified class name of the exception, with a .yml extension:

officefloor/escalation/
  net.officefloor.tutorial.springrestexceptionhttpserver.EscalationException.yml
  net.officefloor.tutorial.springrestexceptionhttpserver.EscalationNotFoundException.yml

OfficeFloor discovers all files in that directory on startup and registers each one as a global escalation handler. When the same handler class provides methods for multiple exception types, the method property identifies which method handles each file's exception.

Exception types

This tutorial defines two exception types handled globally:

public class EscalationException extends Exception {

    public EscalationException(String message) {
        super(message);
    }
}
public class EscalationNotFoundException extends Exception {

    public EscalationNotFoundException(String message) {
        super(message);
    }
}

YAML configuration — one file per exception type

Each escalation YAML file names the handler class and the specific method to call via the method property:

File: officefloor/escalation/net.officefloor.tutorial.springrestexceptionhttpserver.EscalationException.yml

handle:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.GlobalExceptionHandler
  method: handleBadRequest

File: officefloor/escalation/net.officefloor.tutorial.springrestexceptionhttpserver.EscalationNotFoundException.yml

handle:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.GlobalExceptionHandler
  method: handleNotFound

The REST endpoint YAMLs require no escalation configuration — the global handlers wire automatically:

service:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.EscalationService
service:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.EscalationNotFoundService

Service methods

The services simply throw their respective exceptions:

public class EscalationService {
    public void service() throws EscalationException {
        throw new EscalationException("thrown");
    }
}
public class EscalationNotFoundService {
    public void service() throws EscalationNotFoundException {
        throw new EscalationNotFoundException("entity not found");
    }
}

Handler class

One handler class provides a dedicated method for each exception type. Each method receives its exception via @Parameter and returns a ResponseEntity carrying the HTTP status code and a ProblemDetail body:

public class GlobalExceptionHandler {

    public void handleBadRequest(
            @Parameter EscalationException ex,
            ObjectResponse<ResponseEntity<ProblemDetail>> response) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        pd.setDetail(ex.getMessage());
        response.send(ResponseEntity.badRequest().body(pd));
    }

    public void handleNotFound(
            @Parameter EscalationNotFoundException ex,
            ObjectResponse<ResponseEntity<ProblemDetail>> response) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
        pd.setDetail(ex.getMessage());
        response.send(ResponseEntity.status(HttpStatus.NOT_FOUND).body(pd));
    }
}

Priority: method escalation takes precedence over composition escalation, which in turn takes precedence over global escalation. An endpoint can always override the global handler by declaring its own escalation in the YAML.

Spring ControllerAdvice

When neither a method, composition, nor global escalation is configured for an exception type, the exception propagates out of the OfficeFloor composition and is handled by Spring's standard exception handling infrastructure. This allows OfficeFloor REST endpoints to participate in an application's existing @RestControllerAdvice setup without extra configuration.

Use this approach only when integrating with existing Spring infrastructure. For new applications or full OfficeFloor migrations, prefer global escalation so that all exception handling follows the OfficeFloor function pattern.

YAML configuration

The YAML for this endpoint needs no escalation configuration at all:

service:
  class: net.officefloor.tutorial.springrestexceptionhttpserver.SpringAdviceService

Service method

The service method throws the exception in the same way as before:

public class SpringAdviceService {
    public void service() throws MockException {
        throw new MockException("thrown");
    }
}

ExceptionControllerAdvice

The @RestControllerAdvice class provides the @ExceptionHandler method. Spring resolves the HTTP response status via @ResponseStatus:

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(MockException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleMockException(MockException ex) {
        return "Spring handled: " + ex.getMessage();
    }
}

Because this is a standard Spring mechanism, all the usual Spring features apply — returning ResponseEntity, reading the HttpServletRequest, accessing Spring beans, and so on.

Note: @RestControllerAdvice exception handling works in both MockMvc tests and in a real deployed server. No special configuration is required.

Choosing an approach

Approach When to use
Method escalation Handle an exception only for a specific step in the composition, for example a step with its own distinct recovery path.
Composition escalation Handle an exception the same way for every step in the composition, common for infrastructure errors such as database timeouts.
Global escalation Handle an exception consistently across all REST endpoints. Preferred for new applications and for replacing <<<@RestControllerAdvice>.>> Keeps all handler logic within OfficeFloor's function injection model and handles both function-level and governance exceptions.
Spring ControllerAdvice Re-use existing application-wide Spring exception handling, or integrate with Spring libraries that register their own advice. Not recommended for new code.

Unit Tests

The MockMvc tests call each endpoint and assert the response:

package net.officefloor.tutorial.springrestexceptionhttpserver;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class ExceptionHandlingTutorialTest {

    @Autowired
    protected MockMvc mvc;

    @Test
    public void method_exception_handling() throws Exception {
        this.mvc.perform(get("/exception/method"))
                .andExpect(status().isOk())
                .andExpect(content().string("Method handled: thrown"));
    }

    @Test
    public void composition_exception_handling() throws Exception {
        this.mvc.perform(get("/exception/composition"))
                .andExpect(status().isOk())
                .andExpect(content().string("Composition handled: thrown"));
    }

    @Test
    public void spring_controller_advice() throws Exception {
        this.mvc.perform(get("/exception/spring"))
                .andExpect(status().isBadRequest())
                .andExpect(content().string("Spring handled: thrown"));
    }

    @Test
    public void global_escalation_bad_request() throws Exception {
        this.mvc.perform(get("/exception/escalation"))
                .andExpect(status().isBadRequest())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON));
    }

    @Test
    public void global_escalation_not_found() throws Exception {
        this.mvc.perform(get("/exception/not-found"))
                .andExpect(status().isNotFound())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON));
    }
}

The same approaches are also verified against a real embedded server (no MockMvc) to confirm they work in production deployments. The global escalation tests additionally assert the ProblemDetail response body:

@AutoConfigureTestRestTemplate
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ExceptionHandlingIntegrationTest {

    @Autowired
    private TestRestTemplate client;

    @Test
    public void method_exception_handling() {
        ResponseEntity<String> response = client.getForEntity("/exception/method", String.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("Method handled: thrown", response.getBody());
    }

    @Test
    public void composition_exception_handling() {
        ResponseEntity<String> response = client.getForEntity("/exception/composition", String.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("Composition handled: thrown", response.getBody());
    }

    @Test
    public void spring_controller_advice() {
        ResponseEntity<String> response = client.getForEntity("/exception/spring", String.class);
        assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
        assertEquals("Spring handled: thrown", response.getBody());
    }

    @Test
    public void global_escalation_bad_request() {
        ResponseEntity<ProblemDetail> response = client.getForEntity("/exception/escalation", ProblemDetail.class);
        assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
        ProblemDetail body = response.getBody();
        assertNotNull(body);
        assertEquals("thrown", body.getDetail());
    }

    @Test
    public void global_escalation_not_found() {
        ResponseEntity<ProblemDetail> response = client.getForEntity("/exception/not-found", ProblemDetail.class);
        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
        ProblemDetail body = response.getBody();
        assertNotNull(body);
        assertEquals("entity not found", body.getDetail());
    }
}

Next

The Spring REST Validation tutorial demonstrates Bean Validation integration with OfficeFloor REST YAML composition.