1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.exception.spring.boot.autoconfigure.reactive;
18
19 import static java.util.Objects.nonNull;
20 import static java.util.Objects.requireNonNullElse;
21 import static org.springframework.util.StringUtils.hasText;
22
23 import java.time.OffsetDateTime;
24 import java.time.ZoneOffset;
25 import java.util.List;
26 import lombok.AccessLevel;
27 import lombok.Getter;
28 import lombok.Setter;
29 import org.bremersee.exception.RestApiExceptionConstants;
30 import org.bremersee.exception.RestApiExceptionMapper;
31 import org.bremersee.exception.RestApiResponseType;
32 import org.bremersee.exception.model.RestApiException;
33 import org.jspecify.annotations.NonNull;
34 import org.springframework.boot.autoconfigure.web.WebProperties;
35 import org.springframework.boot.webflux.autoconfigure.error.AbstractErrorWebExceptionHandler;
36 import org.springframework.boot.webflux.error.ErrorAttributes;
37 import org.springframework.context.ApplicationContext;
38 import org.springframework.http.HttpStatus;
39 import org.springframework.http.MediaType;
40 import org.springframework.http.codec.ServerCodecConfigurer;
41 import org.springframework.util.AntPathMatcher;
42 import org.springframework.util.PathMatcher;
43 import org.springframework.web.reactive.function.BodyInserters;
44 import org.springframework.web.reactive.function.server.RouterFunction;
45 import org.springframework.web.reactive.function.server.RouterFunctions;
46 import org.springframework.web.reactive.function.server.ServerRequest;
47 import org.springframework.web.reactive.function.server.ServerResponse;
48 import reactor.core.publisher.Mono;
49
50
51
52
53
54
55 public class ApiExceptionHandler extends AbstractErrorWebExceptionHandler {
56
57 @Getter(AccessLevel.PROTECTED)
58 private final List<String> apiPaths;
59
60 @Getter(AccessLevel.PROTECTED)
61 @Setter
62 private PathMatcher pathMatcher = new AntPathMatcher();
63
64 @Getter(AccessLevel.PROTECTED)
65 private final RestApiExceptionMapper restApiExceptionMapper;
66
67
68
69
70
71
72
73
74
75
76
77 public ApiExceptionHandler(
78 List<String> apiPaths,
79 ErrorAttributes errorAttributes,
80 WebProperties.Resources resources,
81 ApplicationContext applicationContext,
82 ServerCodecConfigurer serverCodecConfigurer,
83 RestApiExceptionMapper restApiExceptionMapper) {
84
85 super(errorAttributes, resources, applicationContext);
86 if (serverCodecConfigurer != null) {
87 setMessageReaders(serverCodecConfigurer.getReaders());
88 setMessageWriters(serverCodecConfigurer.getWriters());
89 }
90 this.apiPaths = nonNull(apiPaths) ? apiPaths : List.of();
91 this.restApiExceptionMapper = restApiExceptionMapper;
92 }
93
94 @NonNull
95 @Override
96 protected RouterFunction<ServerResponse> getRoutingFunction(
97 @NonNull ErrorAttributes errorAttributes) {
98
99 return RouterFunctions.route(this::isResponsibleExceptionHandler, this::renderErrorResponse);
100 }
101
102
103
104
105
106
107
108 protected boolean isResponsibleExceptionHandler(ServerRequest request) {
109 return apiPaths.stream().anyMatch(
110 path -> getPathMatcher().match(path, request.path()));
111 }
112
113
114
115
116
117
118
119 @NonNull
120 protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
121
122 RestApiException response = getRestApiExceptionMapper()
123 .build(getError(request), request.path(), null);
124
125 RestApiResponseType restApiResponseType = RestApiResponseType
126 .detectByAccepted(request.headers().accept());
127 if (RestApiResponseType.HEADER == restApiResponseType) {
128 return emptyWithHeaders(response, restApiResponseType.getContentType());
129 } else {
130 return ServerResponse
131 .status(requireNonNullElse(
132 response.getStatus(),
133 HttpStatus.INTERNAL_SERVER_ERROR.value()))
134 .contentType(restApiResponseType.getContentType())
135 .body(BodyInserters.fromValue(response));
136 }
137 }
138
139 private Mono<ServerResponse> emptyWithHeaders(
140 RestApiException response,
141 MediaType contentType) {
142
143 ServerResponse.BodyBuilder builder = ServerResponse
144 .status(requireNonNullElse(
145 response.getStatus(),
146 HttpStatus.INTERNAL_SERVER_ERROR.value()));
147
148 if (hasText(response.getId())) {
149 builder = builder.header(RestApiExceptionConstants.ID_HEADER_NAME, response.getId());
150 }
151
152 String timestamp;
153 if (nonNull(response.getTimestamp())) {
154 timestamp = response.getTimestamp()
155 .format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
156 } else {
157 timestamp = OffsetDateTime.now(ZoneOffset.UTC)
158 .format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
159 }
160 builder = builder.header(RestApiExceptionConstants.TIMESTAMP_HEADER_NAME, timestamp);
161
162 if (hasText(response.getErrorCode())) {
163 builder = builder.header(
164 RestApiExceptionConstants.CODE_HEADER_NAME,
165 response.getErrorCode());
166 builder = builder.header(
167 RestApiExceptionConstants.CODE_INHERITED_HEADER_NAME,
168 String.valueOf(response.getErrorCodeInherited()));
169 }
170
171 if (hasText(response.getMessage())) {
172 builder = builder.header(
173 RestApiExceptionConstants.MESSAGE_HEADER_NAME,
174 response.getMessage());
175 }
176
177 if (hasText(response.getException())) {
178 builder = builder.header(
179 RestApiExceptionConstants.EXCEPTION_HEADER_NAME,
180 response.getException());
181 }
182
183 if (hasText(response.getApplication())) {
184 builder = builder.header(
185 RestApiExceptionConstants.APPLICATION_HEADER_NAME,
186 response.getApplication());
187 }
188
189 if (hasText(response.getPath())) {
190 builder = builder.header(
191 RestApiExceptionConstants.PATH_HEADER_NAME,
192 response.getPath());
193 }
194
195 return builder
196 .contentType(contentType)
197 .body(BodyInserters.empty());
198 }
199
200 }