This commit is contained in:
2026-09-29 18:06:18 +08:00
parent 54ad26c607
commit 2c60a29e0f
30 changed files with 384 additions and 139 deletions
@@ -0,0 +1,55 @@
package com.baiyue.common.web;
import com.baiyue.common.core.api.ApiResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import feign.FeignException;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
/** Explicit failure classification and bounded, credential-free delivery diagnostics. */
public class EventDeliveryFailure extends IllegalStateException {
private static final Set<String> PERMANENT_CODES = Set.of("A0401", "A0403", "EVT_PROTOCOL");
private final boolean permanent;
public EventDeliveryFailure(boolean permanent, String message) {
super(message);
this.permanent = permanent;
}
public static void requireSuccessful(ApiResponse<?> response) {
requireSuccessful(response, Set.of());
}
public static void requireSuccessful(ApiResponse<?> response, Set<String> additionalPermanentCodes) {
if (response == null) throw new EventDeliveryFailure(false, "Consumer returned an empty response");
if (!response.successful()) {
throw new EventDeliveryFailure(response.code() != null && (PERMANENT_CODES.contains(response.code()) || additionalPermanentCodes.contains(response.code())),
"Consumer rejected event: code=" + response.code() + ", message=" + response.message()
+ ", requestId=" + response.requestId() + ", traceId=" + response.traceId());
}
if (response.data() == null) throw new EventDeliveryFailure(false, "Consumer ACK is missing");
}
public static boolean isPermanent(Throwable failure) {
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
for (Throwable current = failure; current != null && seen.add(current); current = current.getCause()) {
if (current instanceof EventDeliveryFailure delivery && delivery.permanent) return true;
if (current instanceof FeignException http && (http.status() == 401 || http.status() == 403)) return true;
}
return false;
}
public static String diagnostic(Throwable failure, String internalToken) {
Throwable current = failure;
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
while (current.getCause() != null && seen.add(current)
&& ("NoFallbackAvailableException".equals(current.getClass().getSimpleName())
|| "No fallback available.".equals(current.getMessage()))) current = current.getCause();
String message;
if (current instanceof FeignException http) message = "HTTP " + http.status();
else if (current instanceof JsonProcessingException) message = "Invalid event JSON";
else message = current.getMessage() == null ? "unknown" : current.getMessage();
String text = current.getClass().getSimpleName() + ": " + message;
if (internalToken != null && !internalToken.isBlank()) text = text.replace(internalToken, "***");
text = text.replaceAll("(?i)Bearer\\s+[^\\s,;\\\"']+", "Bearer ***")
.replaceAll("(?i)((?:X-Internal-Service-Token|Authorization|access[_-]?token|refresh[_-]?token)\\s*[=:]\\s*)[^\\s,;]+", "$1***")
.replaceAll("[\\r\\n\\t]+", " ");
return text.length() > 500 ? text.substring(0, 500) : text;
}
}
@@ -0,0 +1,55 @@
package com.baiyue.common.web;
import com.baiyue.common.core.api.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import feign.Request;
import feign.Response;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class EventDeliveryFailureTest {
@Test void onlyExplicitProtocolOrAuthRejectionIsPermanent() {
for (String code : new String[]{"EVT_PROTOCOL", "A0401", "A0403"}) {
Throwable error = catchThrowable(() -> EventDeliveryFailure.requireSuccessful(response(code)));
assertThat(EventDeliveryFailure.isPermanent(error)).as(code).isTrue();
}
for (String code : new String[]{"EVT_DEPENDENCY", "A0400", "A0409", "A0429", "S0503", "unknown", null}) {
Throwable error = catchThrowable(() -> EventDeliveryFailure.requireSuccessful(response(code)));
assertThat(error).isInstanceOf(EventDeliveryFailure.class);
assertThat(EventDeliveryFailure.isPermanent(error)).as(code).isFalse();
}
assertThat(EventDeliveryFailure.isPermanent(new IllegalArgumentException("ACK mismatch"))).isFalse();
}
@Test void httpAuthenticationFailureIsPermanentBut429And503Retry() {
for (int status : new int[]{401, 403, 429, 503}) {
var http = http(status);
assertThat(EventDeliveryFailure.isPermanent(new RuntimeException("wrapper", http)))
.isEqualTo(status == 401 || status == 403);
assertThat(EventDeliveryFailure.diagnostic(http, "secret")).contains("HTTP " + status)
.doesNotContain("remote-body", "secret");
}
}
@Test void diagnosticsRemoveTokenHeadersSecretsAndLineBreaks() {
var failure = new IllegalStateException("secret-value\nAuthorization=Bearer abc.def.ghi; X-Internal-Service-Token=another-secret, access_token=third-secret");
String value = EventDeliveryFailure.diagnostic(failure, "secret-value");
assertThat(value).doesNotContain("secret-value", "abc.def.ghi", "another-secret", "third-secret", "\n");
assertThat(EventDeliveryFailure.diagnostic(new RuntimeException("x".repeat(1000)), "")).hasSize(500);
}
@Test void malformedJsonDiagnosticNeverCopiesPersistedPayload() {
Throwable failure = catchThrowable(() -> new ObjectMapper().readTree("private-payload-value"));
assertThat(EventDeliveryFailure.diagnostic(failure, ""))
.contains("Invalid event JSON").doesNotContain("private-payload-value");
}
private ApiResponse<Object> response(String code) {
return new ApiResponse<>(code, "consumer-message", null, "request-id", "trace-id", Instant.now());
}
private FeignException http(int status) {
var request = Request.create(Request.HttpMethod.POST, "http://consumer/events?token=secret", Map.of(), new byte[0], StandardCharsets.UTF_8, null);
return FeignException.errorStatus("receive", Response.builder().request(request).status(status).reason("failure")
.headers(Map.of()).body("remote-body-secret", StandardCharsets.UTF_8).build());
}
}