• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

felleslosninger / einnsyn-backend / 21687028034

04 Feb 2026 08:22PM UTC coverage: 83.003% (-0.4%) from 83.423%
21687028034

push

github

web-flow
Merge pull request #585 from felleslosninger/gne-code-quality-fixes

EIN-X: Github Code Quality fixes

2281 of 3179 branches covered (71.75%)

Branch coverage included in aggregate %.

7188 of 8229 relevant lines covered (87.35%)

3.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.0
src/main/java/no/einnsyn/backend/error/EInnsynExceptionHandler.java
1
package no.einnsyn.backend.error;
2

3
import com.google.gson.JsonParseException;
4
import io.micrometer.core.instrument.MeterRegistry;
5
import java.util.List;
6
import java.util.stream.Collectors;
7
import lombok.extern.slf4j.Slf4j;
8
import no.einnsyn.backend.common.exceptions.models.AuthenticationException;
9
import no.einnsyn.backend.common.exceptions.models.AuthorizationException;
10
import no.einnsyn.backend.common.exceptions.models.BadRequestException;
11
import no.einnsyn.backend.common.exceptions.models.ConflictException;
12
import no.einnsyn.backend.common.exceptions.models.EInnsynException;
13
import no.einnsyn.backend.common.exceptions.models.InternalServerErrorException;
14
import no.einnsyn.backend.common.exceptions.models.MethodNotAllowedException;
15
import no.einnsyn.backend.common.exceptions.models.NotFoundException;
16
import no.einnsyn.backend.common.exceptions.models.TooManyUnverifiedOrdersException;
17
import no.einnsyn.backend.common.exceptions.models.ValidationException;
18
import no.einnsyn.backend.common.exceptions.models.ValidationException.FieldError;
19
import org.jetbrains.annotations.NotNull;
20
import org.springframework.context.MessageSourceResolvable;
21
import org.springframework.dao.DataIntegrityViolationException;
22
import org.springframework.http.HttpHeaders;
23
import org.springframework.http.HttpStatus;
24
import org.springframework.http.HttpStatusCode;
25
import org.springframework.http.ResponseEntity;
26
import org.springframework.http.converter.HttpMessageNotReadableException;
27
import org.springframework.transaction.TransactionSystemException;
28
import org.springframework.web.HttpRequestMethodNotSupportedException;
29
import org.springframework.web.bind.MethodArgumentNotValidException;
30
import org.springframework.web.bind.annotation.ControllerAdvice;
31
import org.springframework.web.bind.annotation.ExceptionHandler;
32
import org.springframework.web.context.request.ServletWebRequest;
33
import org.springframework.web.context.request.WebRequest;
34
import org.springframework.web.method.annotation.HandlerMethodValidationException;
35
import org.springframework.web.servlet.NoHandlerFoundException;
36
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
37
import org.springframework.web.servlet.resource.NoResourceFoundException;
38

39
@ControllerAdvice
40
@Slf4j
4✔
41
public class EInnsynExceptionHandler extends ResponseEntityExceptionHandler {
42

43
  private static final String RESPONSE_STATUS = "responseStatus";
44
  private static final String METRIC_NAME = "ein_exception";
45
  private static final String METRIC_LEVEL_KEY = "level";
46
  private static final String METRIC_EXCEPTION_KEY = "exception";
47
  private static final String METRIC_LEVEL_WARNING = "warning";
48
  private static final String METRIC_LEVEL_ERROR = "error";
49

50
  private final MeterRegistry meterRegistry;
51

52
  public EInnsynExceptionHandler(MeterRegistry meterRegistry) {
53
    super();
2✔
54
    this.meterRegistry = meterRegistry;
3✔
55
  }
1✔
56

57
  private void logAndCountWarning(EInnsynException ex, HttpStatusCode statusCode) {
58
    var exceptionName = ex.getClass().getSimpleName();
4✔
59
    log.atWarn()
3✔
60
        .setMessage(ex.getMessage())
4✔
61
        .addKeyValue(RESPONSE_STATUS, String.valueOf(statusCode))
2✔
62
        .log();
1✔
63
    meterRegistry
21✔
64
        .counter(
1✔
65
            METRIC_NAME,
66
            METRIC_LEVEL_KEY,
67
            METRIC_LEVEL_WARNING,
68
            METRIC_EXCEPTION_KEY,
69
            exceptionName)
70
        .increment();
1✔
71
  }
1✔
72

73
  private void logAndCountError(EInnsynException ex, HttpStatusCode statusCode) {
74
    var exceptionName = ex.getClass().getSimpleName();
×
75
    log.atError()
×
76
        .setMessage(ex.getMessage())
×
77
        .addKeyValue(RESPONSE_STATUS, String.valueOf(statusCode))
×
78
        .setCause(ex)
×
79
        .log();
×
80
    meterRegistry
×
81
        .counter(
×
82
            METRIC_NAME, METRIC_LEVEL_KEY, METRIC_LEVEL_ERROR, METRIC_EXCEPTION_KEY, exceptionName)
83
        .increment();
×
84
  }
×
85

86
  @ExceptionHandler(Exception.class)
87
  public ResponseEntity<Object> handleException(Exception ex) {
88
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
×
89
    var internalServerErrorException =
×
90
        new InternalServerErrorException("Internal server error", ex);
91
    logAndCountError(internalServerErrorException, httpStatus);
×
92
    return new ResponseEntity<>(internalServerErrorException.toClientResponse(), null, httpStatus);
×
93
  }
94

95
  @ExceptionHandler(TransactionSystemException.class)
96
  public ResponseEntity<Object> handleException(TransactionSystemException ex) {
97
    if (ex.getRootCause() instanceof EInnsynException eInnsynException) {
×
98
      return handleException(eInnsynException);
×
99
    }
100

101
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
×
102
    var internalServerErrorException =
×
103
        new InternalServerErrorException("Transaction system exception", ex);
104
    logAndCountError(internalServerErrorException, httpStatus);
×
105
    var clientResponse = internalServerErrorException.toClientResponse();
×
106
    return new ResponseEntity<>(clientResponse, null, httpStatus);
×
107
  }
108

109
  /**
110
   * 400 Bad Request
111
   *
112
   * @param ex The exception
113
   * @return The response entity
114
   */
115
  @ExceptionHandler(IllegalArgumentException.class)
116
  public ResponseEntity<Object> handleException(IllegalArgumentException ex) {
117
    var httpStatus = HttpStatus.BAD_REQUEST;
×
118
    var badRequestException = new BadRequestException(ex.getMessage(), ex);
×
119
    logAndCountWarning(badRequestException, httpStatus);
×
120
    var clientResponse = badRequestException.toClientResponse();
×
121
    return new ResponseEntity<>(clientResponse, null, httpStatus);
×
122
  }
123

124
  /**
125
   * 400 Bad Request
126
   *
127
   * @param ex The exception
128
   * @return The response entity
129
   */
130
  @ExceptionHandler(BadRequestException.class)
131
  public ResponseEntity<Object> handleException(BadRequestException ex) {
132
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
133
    logAndCountWarning(ex, httpStatus);
4✔
134
    var clientResponse = ex.toClientResponse();
3✔
135
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
136
  }
137

138
  /**
139
   * 403 Forbidden
140
   *
141
   * @param ex The exception
142
   * @return The response entity
143
   */
144
  @ExceptionHandler(AuthorizationException.class)
145
  public ResponseEntity<Object> handleException(AuthorizationException ex) {
146
    var httpStatus = HttpStatus.FORBIDDEN;
2✔
147
    logAndCountWarning(ex, httpStatus);
4✔
148
    var clientResponse = ex.toClientResponse();
3✔
149
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
150
  }
151

152
  /**
153
   * 401 Unauthorized
154
   *
155
   * @param ex The exception
156
   * @return The response entity
157
   */
158
  @ExceptionHandler(AuthenticationException.class)
159
  public ResponseEntity<Object> handleException(AuthenticationException ex) {
160
    var httpStatus = HttpStatus.UNAUTHORIZED;
2✔
161
    logAndCountWarning(ex, httpStatus);
4✔
162
    var clientResponse = ex.toClientResponse();
3✔
163
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
164
  }
165

166
  /*
167
   * 404 Not Found
168
   *
169
   * @param ex The exception
170
   * @return The response entity
171
   */
172
  @ExceptionHandler(NotFoundException.class)
173
  public ResponseEntity<Object> handleException(NotFoundException ex) {
174
    var httpStatus = HttpStatus.NOT_FOUND;
×
175
    logAndCountWarning(ex, httpStatus);
×
176
    var clientResponse = ex.toClientResponse();
×
177
    return new ResponseEntity<>(clientResponse, null, httpStatus);
×
178
  }
179

180
  /**
181
   * DataIntegrityViolation
182
   *
183
   * <p>Likely a unique constraint violation, foreign key constraint violation, not-null constraint
184
   * or similar.
185
   *
186
   * @param ex The exception
187
   */
188
  @ExceptionHandler(DataIntegrityViolationException.class)
189
  public ResponseEntity<Object> handleDataIntegrityViolationException(Exception ex) {
190
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
×
191
    var internalServerErrorException =
×
192
        new InternalServerErrorException("Data integrity violation", ex);
193
    logAndCountError(internalServerErrorException, httpStatus);
×
194
    var clientResponse = internalServerErrorException.toClientResponse();
×
195
    return new ResponseEntity<>(clientResponse, null, httpStatus);
×
196
  }
197

198
  /**
199
   * Conflict
200
   *
201
   * @param ex The exception
202
   */
203
  @ExceptionHandler(ConflictException.class)
204
  public ResponseEntity<Object> handleConflictException(ConflictException ex) {
205
    var httpStatus = HttpStatus.CONFLICT;
2✔
206
    logAndCountWarning(ex, httpStatus);
4✔
207
    var clientResponse = ex.toClientResponse();
3✔
208
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
209
  }
210

211
  /**
212
   * Too many unverified orders.
213
   *
214
   * @param ex the exception
215
   * @return the response entity
216
   */
217
  @ExceptionHandler(TooManyUnverifiedOrdersException.class)
218
  public ResponseEntity<Object> handleTooManyUnverifiedOrdersException(
219
      TooManyUnverifiedOrdersException ex) {
220
    var httpStatus = HttpStatus.TOO_MANY_REQUESTS;
2✔
221
    logAndCountWarning(ex, httpStatus);
4✔
222
    var clientResponse = ex.toClientResponse();
3✔
223
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
224
  }
225

226
  /**
227
   * Input validation errors, @Valid and @Validated on JSON fields.
228
   *
229
   * @param ex The exception
230
   * @param headers The headers
231
   * @param status The status
232
   * @param request The request
233
   */
234
  @Override
235
  protected ResponseEntity<Object> handleMethodArgumentNotValid(
236
      @NotNull MethodArgumentNotValidException ex,
237
      @NotNull HttpHeaders headers,
238
      @NotNull HttpStatusCode status,
239
      @NotNull WebRequest request) {
240

241
    var fieldErrors =
1✔
242
        ex.getFieldErrors().stream()
3✔
243
            .map(
1✔
244
                e ->
245
                    new FieldError(
4✔
246
                        e.getField(),
1✔
247
                        e.getRejectedValue() == null ? null : e.getRejectedValue().toString(),
9✔
248
                        e.getDefaultMessage()))
2✔
249
            .toList();
2✔
250

251
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
252
    var fieldErrorString =
1✔
253
        fieldErrors.stream()
2✔
254
            .map(f -> "\"" + f.getFieldName() + "\": \"" + f.getValue() + "\"")
8✔
255
            .collect(Collectors.joining(", "));
4✔
256
    var validationException =
8✔
257
        new ValidationException(
258
            "Field validation error on fields: " + fieldErrorString, ex, fieldErrors);
259

260
    logAndCountWarning(validationException, httpStatus);
4✔
261
    var clientResponse = validationException.toClientResponse();
3✔
262
    return handleExceptionInternal(
8✔
263
        validationException, clientResponse, headers, httpStatus, request);
264
  }
265

266
  /**
267
   * 404
268
   *
269
   * <p>When no handler is found for the request.
270
   *
271
   * @param ex The exception
272
   * @param headers The headers
273
   * @param status The status
274
   * @param request The request
275
   */
276
  @Override
277
  protected ResponseEntity<Object> handleNoHandlerFoundException(
278
      @NotNull NoHandlerFoundException ex,
279
      @NotNull HttpHeaders headers,
280
      @NotNull HttpStatusCode status,
281
      @NotNull WebRequest request) {
282
    var httpStatus = HttpStatus.NOT_FOUND;
×
283
    var uri =
284
        (request instanceof ServletWebRequest servletWebRequest)
×
285
            ? servletWebRequest.getRequest().getRequestURI()
×
286
            : request.getDescription(false);
×
287
    var notFoundException = new NotFoundException("No handler found: " + uri, ex);
×
288
    logAndCountWarning(notFoundException, httpStatus);
×
289
    var clientResponse = notFoundException.toClientResponse();
×
290
    return handleExceptionInternal(notFoundException, clientResponse, headers, httpStatus, request);
×
291
  }
292

293
  /**
294
   * 404
295
   *
296
   * <p>When a resource is not found.
297
   *
298
   * @param ex The exception
299
   * @param headers The headers
300
   * @param status The status
301
   * @param request The request
302
   */
303
  @Override
304
  protected ResponseEntity<Object> handleNoResourceFoundException(
305
      @NotNull NoResourceFoundException ex,
306
      @NotNull HttpHeaders headers,
307
      @NotNull HttpStatusCode status,
308
      @NotNull WebRequest request) {
309
    var httpStatus = HttpStatus.NOT_FOUND;
2✔
310
    var uri =
311
        (request instanceof ServletWebRequest servletWebRequest)
6!
312
            ? servletWebRequest.getRequest().getRequestURI()
4✔
313
            : request.getDescription(false);
1✔
314
    var notFoundException = new NotFoundException("Resource not found: " + uri, ex);
7✔
315
    logAndCountWarning(notFoundException, httpStatus);
4✔
316
    var clientResponse = notFoundException.toClientResponse();
3✔
317
    return handleExceptionInternal(notFoundException, clientResponse, headers, httpStatus, request);
8✔
318
  }
319

320
  /**
321
   * Path-variable validation errors. Most likely non-existent IDs.
322
   *
323
   * @param ex The exception
324
   * @param headers The headers
325
   * @param status The status
326
   * @param request The request
327
   */
328
  @Override
329
  protected ResponseEntity<Object> handleHandlerMethodValidationException(
330
      @NotNull HandlerMethodValidationException ex,
331
      @NotNull HttpHeaders headers,
332
      @NotNull HttpStatusCode status,
333
      @NotNull WebRequest request) {
334

335
    var notFound = false;
2✔
336
    for (var error : ex.getAllErrors()) {
11✔
337
      var defaultMessage = error.getDefaultMessage();
3✔
338
      if (defaultMessage != null && defaultMessage.contains("not found")) {
6!
339
        notFound = true;
2✔
340
        break;
1✔
341
      }
342
    }
1✔
343

344
    // 404
345
    if (notFound) {
2✔
346
      var httpStatus = HttpStatus.NOT_FOUND;
2✔
347
      var notFoundException =
4✔
348
          new NotFoundException("Not found: " + request.getDescription(false), ex);
5✔
349
      logAndCountWarning(notFoundException, httpStatus);
4✔
350
      var clientResponse = notFoundException.toClientResponse();
3✔
351
      return handleExceptionInternal(
8✔
352
          notFoundException, clientResponse, headers, httpStatus, request);
353
    }
354

355
    // Bad request
356
    else {
357
      var httpStatus = HttpStatus.BAD_REQUEST;
2✔
358

359
      var errorMessages = buildValidationMessages(ex);
4✔
360
      var userMessage = summarizeValidationMessages(errorMessages);
4✔
361
      var badRequestException = new BadRequestException(userMessage, ex);
6✔
362
      var exceptionName = badRequestException.getClass().getSimpleName();
4✔
363
      log.atWarn()
3✔
364
          .setMessage(badRequestException.getMessage())
4✔
365
          .addKeyValue(RESPONSE_STATUS, String.valueOf(httpStatus))
4✔
366
          .addKeyValue("validationErrors", errorMessages)
4✔
367
          .addKeyValue("request", request.getDescription(false))
2✔
368
          .log();
1✔
369
      meterRegistry
21✔
370
          .counter(
1✔
371
              METRIC_NAME,
372
              METRIC_LEVEL_KEY,
373
              METRIC_LEVEL_WARNING,
374
              METRIC_EXCEPTION_KEY,
375
              exceptionName)
376
          .increment();
1✔
377
      var clientResponse = badRequestException.toClientResponse();
3✔
378
      return handleExceptionInternal(
8✔
379
          badRequestException, clientResponse, headers, httpStatus, request);
380
    }
381
  }
382

383
  /**
384
   * JSON parse errors
385
   *
386
   * @param ex The exception
387
   * @param headers The headers
388
   * @param status The status
389
   * @param request The request
390
   */
391
  @Override
392
  protected ResponseEntity<Object> handleHttpMessageNotReadable(
393
      @NotNull HttpMessageNotReadableException ex,
394
      @NotNull HttpHeaders headers,
395
      @NotNull HttpStatusCode status,
396
      @NotNull WebRequest request) {
397

398
    if (ex.getCause() instanceof JsonParseException jsonParseException
9!
399
        && jsonParseException.getCause() instanceof BadRequestException badRequestException) {
9✔
400
      return handleException(badRequestException);
4✔
401
    }
402

403
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
404
    var badRequestException = new BadRequestException(ex.getMessage(), ex);
7✔
405
    logAndCountWarning(badRequestException, httpStatus);
4✔
406

407
    // Don't send standard message, HttpMessageNotReadableException may contain valuable info:
408
    var censoredBadRequestException = new BadRequestException("Failed to parse the request.", ex);
6✔
409
    var clientResponse = censoredBadRequestException.toClientResponse();
3✔
410
    return handleExceptionInternal(
8✔
411
        badRequestException, clientResponse, headers, httpStatus, request);
412
  }
413

414
  @Override
415
  protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
416
      HttpRequestMethodNotSupportedException ex,
417
      HttpHeaders headers,
418
      HttpStatusCode status,
419
      WebRequest request) {
420

421
    var supportedMethods = ex.getSupportedHttpMethods();
3✔
422
    var methodNotAllowedException =
3✔
423
        new MethodNotAllowedException(
424
            "Method "
425
                + ex.getMethod()
6✔
426
                + " is not supported for this endpoint. Supported methods are: "
427
                + supportedMethods);
428

429
    // Add Allow header with supported methods
430
    if (supportedMethods != null) {
2!
431
      headers.setAllow(supportedMethods);
3✔
432
    }
433

434
    var errorResponse = methodNotAllowedException.toClientResponse();
3✔
435
    return handleExceptionInternal(
8✔
436
        methodNotAllowedException, errorResponse, headers, status, request);
437
  }
438

439
  private List<String> buildValidationMessages(HandlerMethodValidationException ex) {
440
    return ex.getAllErrors().stream()
6✔
441
        .map(this::resolveValidationMessage)
2✔
442
        .filter(message -> message != null && !message.isBlank())
9!
443
        .toList();
1✔
444
  }
445

446
  private String summarizeValidationMessages(List<String> messages) {
447
    if (messages.isEmpty()) {
3!
448
      return "Validation failed.";
×
449
    }
450
    var limit = 5;
2✔
451
    var summary = messages.stream().limit(limit).collect(Collectors.joining("; "));
10✔
452
    if (messages.size() > limit) {
4!
453
      summary += "; and " + (messages.size() - limit) + " more.";
×
454
    }
455
    return "Validation failed: " + summary;
3✔
456
  }
457

458
  private String resolveValidationMessage(MessageSourceResolvable error) {
459
    var defaultMessage = error.getDefaultMessage();
3✔
460
    if (defaultMessage != null && !defaultMessage.isBlank()) {
5!
461
      return defaultMessage;
2✔
462
    }
463
    var codes = error.getCodes();
×
464
    if (codes != null && codes.length > 0) {
×
465
      return codes[0];
×
466
    }
467
    return "Validation error";
×
468
  }
469
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc