Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import tools.jackson.databind.json.JsonMapper;

Expand All @@ -29,8 +30,10 @@ public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConf
}

@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new HtmxRequestMappingHandlerMapping();
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
var adapter = new RequestMappingHandlerAdapter();
Copy link
Collaborator

@xhaggi xhaggi Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add back the getRequestMappingHandlerMapping method? Otherwise, the request mapping is broken.

adapter.setResponseBodyAdvice(List.of(new HtmxResponseBodyAdvice(handlerMethodHandler)));
return adapter;
}

@Override
Expand All @@ -46,7 +49,9 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
return new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
var resolver = new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
resolver.setResponseBodyAdvice(List.of(new HtmxResponseBodyAdvice(handlerMethodHandler)));
return resolver;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
* A {@link ResponseBodyAdvice} implementation that adds htmx response headers
* based on {@link HtmxResponse}.
*
* @since 5.0.0
*/
public class HtmxResponseBodyAdvice implements ResponseBodyAdvice<Object> {

private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;

public HtmxResponseBodyAdvice(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
}

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true; // Apply to all @ResponseBody methods
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {

if (request instanceof ServletServerHttpRequest servletRequest
&& response instanceof ServletServerHttpResponse servletResponse) {
htmxHandlerMethodHandler.handleMethodArgument(servletRequest.getServletRequest(),
servletResponse.getServletResponse());
}
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import({ HtmxResponseBodyAdviceIT.TestRestController.class })
@AutoConfigureRestTestClient
public class HtmxResponseBodyAdviceIT {

@Autowired
RestTestClient webClient;

@Test
public void testTrigger() throws Exception {

get("/trigger")
.expectHeader()
.valueEquals("HX-Trigger", "trigger1,trigger2");
}

@Test
public void testExceptionHandler() throws Exception {

get("/throw-exception")
.expectHeader()
.valueEquals("HX-Retarget", "#container");
}

private RestTestClient.ResponseSpec get(String uri) {

return webClient
.get()
.uri(uri)
.exchange()
.expectStatus()
.isOk();
}

@RestController
static class TestRestController {

@GetMapping("/trigger")
public Object triggerResponseBody(HtmxResponse response) {

response.addTrigger("trigger1");
response.addTrigger("trigger2");
return "response";
}

@GetMapping("/throw-exception")
public Object throwException() {

throw new RuntimeException();
}

@ExceptionHandler(RuntimeException.class)
public Object handleError(RuntimeException ex, HtmxResponse htmxResponse) {

htmxResponse.setRetarget("#container");
return "view";
}

}

}
Loading