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

FIWARE / contract-management / #104

01 Oct 2026 07:34AM UTC coverage: 4.489% (+0.7%) from 3.796%
#104

Pull #28

vramperez
Remove unused exception constructors, explain the handler defer

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pull Request #28: Add failure reasons, downstream error descriptions and configurable log output

218 of 379 new or added lines in 33 files covered. (57.52%)

9 existing lines in 6 files now uncovered.

1606 of 35773 relevant lines covered (4.49%)

0.04 hits per line

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

90.91
/src/main/java/org/fiware/iam/logging/DownstreamError.java
1
package org.fiware.iam.logging;
2

3
import io.micronaut.http.client.exceptions.HttpClientResponseException;
4

5
import reactor.core.Exceptions;
6

7
import java.util.Optional;
8
import java.util.stream.Collectors;
9

10
/**
11
 * Adds what a stacktrace does not show to a log line: the status and body a downstream service answered with. That
12
 * body usually holds the actual reason of a failure (e.g. a validation message of the PAP or the TIL), while the
13
 * trace of a {@link HttpClientResponseException} only carries the status.
14
 * <p>
15
 * Log the throwable itself next to it, so the standard stacktrace is printed as well.
16
 */
17
public final class DownstreamError {
18

19
        static final int MAX_BODY_LENGTH = 1000;
20
        private static final int MAX_CAUSE_DEPTH = 10;
21

22
        private DownstreamError() {
23
        }
24

25
        /**
26
         * The message of the throwable, followed by status and body of the downstream answer it was caused by - if any.
27
         * E.g. {@code [pap_rejected_policy] The PAP rejected policy x. - downstream answered with status=400 body={"detail":"..."}}
28
         * <p>
29
         * The reasons of a composite of several failures are joined with {@code "; "}.
30
         */
31
        public static String reason(Throwable throwable) {
32
                if (Exceptions.isMultiple(throwable)) {
1 ✔
33
                        // several failures collected by a zipDelayError - each of them is a reason
34
                        return Exceptions.unwrapMultipleExcludingTracebacks(throwable).stream()
1 ✔
35
                                        .map(DownstreamError::reason)
1 ✔
36
                                        .collect(Collectors.joining("; "));
1 ✔
37
                }
38
                String message = Optional.ofNullable(throwable.getMessage()).orElseGet(() -> throwable.getClass().getSimpleName());
1 ✔
39
                return httpError(throwable)
1 ✔
40
                                .map(httpError -> "%s - downstream answered with %s".formatted(message, httpError))
1 ✔
41
                                .orElse(message);
1 ✔
42
        }
43

44
        /**
45
         * Status and body of the first downstream http error in the causes of the throwable, empty if it was not caused by one.
46
         */
47
        public static Optional<String> httpError(Throwable throwable) {
48
                Throwable current = throwable;
1 ✔
49
                for (int depth = 0; current != null && depth < MAX_CAUSE_DEPTH; depth++) {
1 ✔
50
                        if (current instanceof HttpClientResponseException hcre) {
1 ✔
51
                                return Optional.of("status=%s body=%s".formatted(hcre.getStatus().getCode(), body(hcre)));
1 ✔
52
                        }
53
                        current = current.getCause();
1 ✔
54
                }
55
                return Optional.empty();
1 ✔
56
        }
57

58
        private static String body(HttpClientResponseException hcre) {
59
                String body;
60
                try {
61
                        body = hcre.getResponse().getBody(String.class).orElse("");
1 ✔
NEW
62
                } catch (RuntimeException e) {
×
NEW
63
                        body = "";
×
64
                }
1 ✔
65
                if (body.isBlank()) {
1 ✔
66
                        return "<empty>";
1 ✔
67
                }
68
                body = body.replaceAll("\\s*\\R\\s*", " ");
1 ✔
69
                return body.length() > MAX_BODY_LENGTH ? body.substring(0, MAX_BODY_LENGTH) + "...(truncated)" : body;
1 ✔
70
        }
71
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc