Skip to content

Commit 20e7a11

Browse files
committed
[fix] broken HtmxResponse handling since 9cc7331
1 parent fb0c220 commit 20e7a11

File tree

8 files changed

+498
-131
lines changed

8 files changed

+498
-131
lines changed

htmx-spring-boot/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
<artifactId>spring-boot-starter-thymeleaf</artifactId>
4949
<scope>test</scope>
5050
</dependency>
51+
<dependency>
52+
<groupId>io.rest-assured</groupId>
53+
<artifactId>rest-assured</artifactId>
54+
<scope>test</scope>
55+
</dependency>
5156
<dependency>
5257
<groupId>com.google.code.findbugs</groupId>
5358
<artifactId>jsr305</artifactId>

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
*/
1818
public class HtmxExceptionHandlerExceptionResolver extends ExceptionHandlerExceptionResolver {
1919

20-
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
20+
private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;
2121

22-
public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
23-
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
22+
public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
23+
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
2424
}
2525

2626
@Override
@@ -29,10 +29,14 @@ protected ModelAndView doResolveHandlerMethodException(HttpServletRequest reques
2929
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
3030
if (exceptionHandlerMethod != null) {
3131
Method method = exceptionHandlerMethod.getMethod();
32-
handlerMethodAnnotationHandler.handleMethod(method, request, response);
32+
htmxHandlerMethodHandler.handleMethodAnnotations(method, request, response);
3333
}
3434

35-
return super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
35+
ModelAndView modelAndView = super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
36+
37+
htmxHandlerMethodHandler.handleMethodArgument(request, response);
38+
39+
return modelAndView;
3640
}
3741

3842
}
Lines changed: 10 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,39 @@
11
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
22

3-
import com.fasterxml.jackson.core.JsonProcessingException;
4-
import com.fasterxml.jackson.databind.ObjectMapper;
53
import jakarta.servlet.http.HttpServletRequest;
64
import jakarta.servlet.http.HttpServletResponse;
75
import org.springframework.http.HttpHeaders;
86
import org.springframework.web.method.HandlerMethod;
97
import org.springframework.web.servlet.HandlerInterceptor;
8+
import org.springframework.web.servlet.ModelAndView;
109

1110
import java.lang.reflect.Method;
12-
import java.util.Collection;
13-
import java.util.HashMap;
14-
import java.util.stream.Collectors;
1511

1612
/**
1713
* HandlerInterceptor that adds htmx specific headers to the response.
1814
*/
1915
public class HtmxHandlerInterceptor implements HandlerInterceptor {
2016

21-
private final ObjectMapper objectMapper;
22-
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
17+
private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;
2318

24-
public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
25-
this.objectMapper = objectMapper;
26-
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
19+
public HtmxHandlerInterceptor(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
20+
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
2721
}
2822

2923
@Override
30-
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
31-
32-
HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
33-
if (htmxResponse != null) {
34-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal());
35-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal());
36-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal());
37-
38-
if (htmxResponse.getLocation() != null) {
39-
HtmxLocation location = htmxResponse.getLocation();
40-
if (location.hasContextData()) {
41-
location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
42-
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
43-
} else {
44-
response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
45-
}
46-
}
47-
if (htmxResponse.getReplaceUrl() != null) {
48-
response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative()));
49-
}
50-
if (htmxResponse.getPushUrl() != null) {
51-
response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative()));
52-
}
53-
if (htmxResponse.getRedirect() != null) {
54-
response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative()));
55-
}
56-
if (htmxResponse.isRefresh()) {
57-
response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true");
58-
}
59-
if (htmxResponse.getRetarget() != null) {
60-
response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget());
61-
}
62-
if (htmxResponse.getReselect() != null) {
63-
response.setHeader(HtmxResponseHeader.HX_RESELECT.getValue(), htmxResponse.getReselect());
64-
}
65-
if (htmxResponse.getReswap() != null) {
66-
response.setHeader(HtmxResponseHeader.HX_RESWAP.getValue(), htmxResponse.getReswap().toHeaderValue());
67-
}
68-
}
24+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
25+
htmxHandlerMethodHandler.handleMethodArgument(request, response);
6926
}
7027

7128
@Override
7229
public boolean preHandle(HttpServletRequest request,
7330
HttpServletResponse response,
7431
Object handler) {
7532

76-
if (handler instanceof HandlerMethod) {
77-
Method method = ((HandlerMethod) handler).getMethod();
78-
setVary(request, response);
79-
handlerMethodAnnotationHandler.handleMethod(method, request, response);
33+
setVary(request, response);
34+
35+
if (handler instanceof HandlerMethod handlerMethod) {
36+
htmxHandlerMethodHandler.handleMethodAnnotations(handlerMethod.getMethod(), request, response);
8037
}
8138

8239
return true;
@@ -88,35 +45,4 @@ private void setVary(HttpServletRequest request, HttpServletResponse response) {
8845
}
8946
}
9047

91-
private void setHeaderJsonValue(HttpServletResponse response, HtmxResponseHeader header, Object value) {
92-
try {
93-
response.setHeader(header.getValue(), objectMapper.writeValueAsString(value));
94-
} catch (JsonProcessingException e) {
95-
throw new IllegalArgumentException("Unable to set header " + header.getValue() + " to " + value, e);
96-
}
97-
}
98-
99-
private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> triggers) {
100-
if (triggers.isEmpty()) {
101-
return;
102-
}
103-
104-
// separate event names by commas if no additional details are available
105-
if (triggers.stream().allMatch(t -> t.getEventDetail() == null)) {
106-
String value = triggers.stream()
107-
.map(HtmxTrigger::getEventName)
108-
.collect(Collectors.joining(","));
109-
110-
response.setHeader(headerName.getValue(), value);
111-
return;
112-
}
113-
114-
// multiple events with or without details
115-
var triggerMap = new HashMap<String, Object>();
116-
for (HtmxTrigger trigger : triggers) {
117-
triggerMap.put(trigger.getEventName(), trigger.getEventDetail());
118-
}
119-
setHeaderJsonValue(response, headerName, triggerMap);
120-
}
121-
12248
}

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java renamed to htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodHandler.java

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,68 @@
99

1010
import java.lang.reflect.Method;
1111
import java.time.Duration;
12+
import java.util.Collection;
13+
import java.util.HashMap;
14+
import java.util.stream.Collectors;
1215

1316
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;
1417

1518
/**
16-
* A handler for processing htmx annotations present on exception handler methods.
19+
* A handler for processing {@link HtmxResponse} and annotations present on handler methods.
1720
*
1821
* @since 3.6.2
1922
*/
20-
class HtmxHandlerMethodAnnotationHandler {
23+
class HtmxHandlerMethodHandler {
2124

2225
private final ObjectMapper objectMapper;
2326

24-
public HtmxHandlerMethodAnnotationHandler(ObjectMapper objectMapper) {
27+
public HtmxHandlerMethodHandler(ObjectMapper objectMapper) {
2528
this.objectMapper = objectMapper;
2629
}
2730

28-
public void handleMethod(Method method, HttpServletRequest request, HttpServletResponse response) {
31+
public void handleMethodArgument(HttpServletRequest request, HttpServletResponse response) {
32+
33+
HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
34+
if (htmxResponse != null) {
35+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal());
36+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal());
37+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal());
38+
39+
if (htmxResponse.getLocation() != null) {
40+
HtmxLocation location = htmxResponse.getLocation();
41+
if (location.hasContextData()) {
42+
location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
43+
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
44+
} else {
45+
response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
46+
}
47+
}
48+
if (htmxResponse.getReplaceUrl() != null) {
49+
response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative()));
50+
}
51+
if (htmxResponse.getPushUrl() != null) {
52+
response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative()));
53+
}
54+
if (htmxResponse.getRedirect() != null) {
55+
response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative()));
56+
}
57+
if (htmxResponse.isRefresh()) {
58+
response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true");
59+
}
60+
if (htmxResponse.getRetarget() != null) {
61+
response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget());
62+
}
63+
if (htmxResponse.getReselect() != null) {
64+
response.setHeader(HtmxResponseHeader.HX_RESELECT.getValue(), htmxResponse.getReselect());
65+
}
66+
if (htmxResponse.getReswap() != null) {
67+
response.setHeader(HtmxResponseHeader.HX_RESWAP.getValue(), htmxResponse.getReswap().toHeaderValue());
68+
}
69+
}
70+
}
71+
72+
public void handleMethodAnnotations(Method method, HttpServletRequest request, HttpServletResponse response) {
73+
2974
setHxLocation(request, response, method);
3075
setHxPushUrl(request, response, method);
3176
setHxRedirect(request, response, method);
@@ -39,6 +84,29 @@ public void handleMethod(Method method, HttpServletRequest request, HttpServletR
3984
setHxRefresh(response, method);
4085
}
4186

87+
private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> triggers) {
88+
if (triggers.isEmpty()) {
89+
return;
90+
}
91+
92+
// separate event names by commas if no additional details are available
93+
if (triggers.stream().allMatch(t -> t.getEventDetail() == null)) {
94+
String value = triggers.stream()
95+
.map(HtmxTrigger::getEventName)
96+
.collect(Collectors.joining(","));
97+
98+
response.setHeader(headerName.getValue(), value);
99+
return;
100+
}
101+
102+
// multiple events with or without details
103+
var triggerMap = new HashMap<String, Object>();
104+
for (HtmxTrigger trigger : triggers) {
105+
triggerMap.put(trigger.getEventName(), trigger.getEventDetail());
106+
}
107+
setHeaderJsonValue(response, headerName, triggerMap);
108+
}
109+
42110
private void setHxLocation(HttpServletRequest request, HttpServletResponse response, Method method) {
43111
HxLocation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxLocation.class);
44112
if (methodAnnotation != null) {

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConf
3131
private final ObjectFactory<ViewResolver> viewResolverObjectFactory;
3232
private final ObjectFactory<LocaleResolver> localeResolverObjectFactory;
3333
private final ObjectMapper objectMapper;
34-
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
34+
private final HtmxHandlerMethodHandler handlerMethodHandler;
3535

3636
HtmxMvcAutoConfiguration(@Qualifier("viewResolver") ObjectFactory<ViewResolver> viewResolverObjectFactory,
3737
ObjectFactory<LocaleResolver> localeResolverObjectFactory) {
@@ -42,7 +42,7 @@ public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConf
4242
this.viewResolverObjectFactory = viewResolverObjectFactory;
4343
this.localeResolverObjectFactory = localeResolverObjectFactory;
4444
this.objectMapper = JsonMapper.builder().build();
45-
this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper);
45+
this.handlerMethodHandler = new HtmxHandlerMethodHandler(this.objectMapper);
4646
}
4747

4848
@Override
@@ -52,7 +52,7 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
5252

5353
@Override
5454
public void addInterceptors(InterceptorRegistry registry) {
55-
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper, handlerMethodAnnotationHandler));
55+
registry.addInterceptor(new HtmxHandlerInterceptor(handlerMethodHandler));
5656
}
5757

5858
@Override
@@ -69,7 +69,7 @@ public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handler
6969

7070
@Override
7171
public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
72-
return new HtmxExceptionHandlerExceptionResolver(handlerMethodAnnotationHandler);
72+
return new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
7373
}
7474

7575
@Bean

0 commit comments

Comments
 (0)