Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
75abf92
6.x json2xml array support (#2364)
christiangoerdes Dec 4, 2025
642c0c5
Restrict JAVA_OPTS path normalization to log4j.configurationFile in s…
christiangoerdes Dec 5, 2025
28968c1
fix javadoc
christiangoerdes Dec 5, 2025
2c0f3e2
Merge remote-tracking branch 'origin/6.X' into 6.X
christiangoerdes Dec 5, 2025
4ba8b06
fix javadoc
christiangoerdes Dec 5, 2025
1a32a4c
Release 6.3.11 (#2384)
github-actions[bot] Dec 5, 2025
3beaa7e
Snapshot version (#2385)
github-actions[bot] Dec 5, 2025
8fff2aa
Add support for reference schemas in JSON/YAML schema validation (6.X…
christiangoerdes Dec 19, 2025
bee72fa
Release 6.3.12 (#2478)
github-actions[bot] Dec 19, 2025
bc28c5d
Snapshot version (#2479)
github-actions[bot] Dec 19, 2025
a59eaf2
fix startup error when schemaMappings are not set (#2567)
christiangoerdes Jan 8, 2026
e66837c
Support rewriter on internal destinations (#2357) (#2565)
rrayst Jan 8, 2026
335ab95
Release 6.3.13 (#2568)
github-actions[bot] Jan 8, 2026
c955776
Snapshot version (#2572)
github-actions[bot] Jan 8, 2026
d8a5e40
show load balancer node status (and failure reasons) in adminConsole …
rrayst Jan 9, 2026
53ebec9
[targeting 6.X] fix ByteStreamLogging: drop fake zeros (#2596)
rrayst Jan 12, 2026
34879a2
Release 6.3.14 (#2602)
github-actions[bot] Jan 12, 2026
534eb8e
Snapshot version (#2603)
github-actions[bot] Jan 13, 2026
c2b719f
6.X: Add default tcp health check (#2600)
christiangoerdes Jan 13, 2026
e68cf74
fix: handle query parameter validation in StringValidator
predic8 Jan 13, 2026
f939c83
Release 6.3.15 (#2608)
github-actions[bot] Jan 13, 2026
73c3676
Snapshot version (#2609)
github-actions[bot] Jan 13, 2026
201905c
6.X: add support for custom HttpClient configurations in JWK (#2645)
christiangoerdes Jan 20, 2026
40be779
Refactor AccessLogInterceptorService to use `DateTimeFormatter` inste…
christiangoerdes Jan 23, 2026
f57e1b6
Fix `BalancerUtil` to also return clusters and balancers defined in `…
christiangoerdes Jan 23, 2026
1d587e0
fix: consider production mode in invalid path error helper (#2682)
predic8 Jan 23, 2026
8a34d08
Release 6.3.16 (#2679)
github-actions[bot] Jan 23, 2026
9390de1
Snapshot version (#2687)
github-actions[bot] Jan 23, 2026
ccd774d
Add test to reproduce `Invalid server response: 19`
christiangoerdes Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,5 @@ maven-plugin/target/surefire/
/docs/router-conf.xsd
.vscode/
/core/derby.log
/distribution/conf/apis.yaml
/distribution/conf/membrane.log
2 changes: 1 addition & 1 deletion annot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<groupId>org.membrane-soa</groupId>
<artifactId>service-proxy-parent</artifactId>
<relativePath>../pom.xml</relativePath>
<version>6.3.11-SNAPSHOT</version>
<version>6.3.17-SNAPSHOT</version>
</parent>

<dependencies>
Expand Down
2 changes: 1 addition & 1 deletion core/.factorypath
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><factorypath>
<factorypathentry enabled="true" id="M2_REPO/org/membrane-soa/service-proxy-annot/6.3.11-SNAPSHOT/service-proxy-annot-6.3.11-SNAPSHOT.jar" kind="VARJAR" runInBatchMode="false"/>
<factorypathentry enabled="true" id="M2_REPO/org/membrane-soa/service-proxy-annot/6.3.17-SNAPSHOT/service-proxy-annot-6.3.17-SNAPSHOT.jar" kind="VARJAR" runInBatchMode="false"/>
</factorypath>
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<groupId>org.membrane-soa</groupId>
<artifactId>service-proxy-parent</artifactId>
<relativePath>../pom.xml</relativePath>
<version>6.3.11-SNAPSHOT</version>
<version>6.3.17-SNAPSHOT</version>
</parent>

<!-- Open Telemetry -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.predic8.membrane.core.http.*;
import com.predic8.membrane.core.interceptor.*;
import com.predic8.membrane.core.interceptor.Interceptor.*;
import com.predic8.membrane.core.interceptor.balancer.ExchangeNodeStatusTracker;
import com.predic8.membrane.core.model.*;
import com.predic8.membrane.core.proxies.Proxy;
import com.predic8.membrane.core.proxies.*;
Expand All @@ -28,7 +29,9 @@
import java.text.*;
import java.util.*;

import static com.predic8.membrane.core.exchange.Exchange.TRACK_NODE_STATUS;
import static com.predic8.membrane.core.exchange.ExchangeState.*;
import static java.lang.Boolean.TRUE;

public abstract class AbstractExchange {
private static final Logger log = LoggerFactory.getLogger(AbstractExchange.class.getName());
Expand Down Expand Up @@ -68,6 +71,8 @@ public abstract class AbstractExchange {

private ArrayList<Interceptor> interceptorStack = new ArrayList<>(10);

private ExchangeNodeStatusTracker nodeStatusTracker;

private int estimatedHeapSize = -1;

public AbstractExchange() {}
Expand Down Expand Up @@ -408,6 +413,7 @@ public int resetHeapSizeEstimation() {

protected int estimateHeapSize() {
return 2600 +
(nodeStatusTracker != null ? nodeStatusTracker.estimateHeapSize() : 0) +
(originalRequestUri != null ? originalRequestUri.length() : 0) +
(request != null ? request.estimateHeapSize() : 0) +
(response != null ? response.estimateHeapSize() : 0);
Expand Down Expand Up @@ -436,6 +442,7 @@ public static <T extends AbstractExchange> T updateCopy(T source, T copy, Runnab
copy.setRemoteAddr(source.getRemoteAddr());
copy.setRemoteAddrIp(source.getRemoteAddrIp());
copy.setInterceptorStack(new ArrayList<>(source.getInterceptorStack()));
copy.setNodeStatusTracker(source.getNodeStatusTracker() == null ? null : source.getNodeStatusTracker().clone());

return copy;
}
Expand Down Expand Up @@ -480,4 +487,16 @@ public Message getMessage(Flow flow) {
return request;
return response; // RESPONSE and ABORT flow both work on the response
}

public ExchangeNodeStatusTracker getNodeStatusTracker() {
return nodeStatusTracker;
}

public void setNodeStatusTracker(ExchangeNodeStatusTracker nodeStatusTracker) {
this.nodeStatusTracker = nodeStatusTracker;
}

public void initNodeStatusTracker() {
this.nodeStatusTracker = new ExchangeNodeStatusTracker(destinations.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ public class Exchange extends AbstractExchange {

private Connection targetConnection;

private int[] nodeStatusCodes;

private Exception[] nodeExceptions;

private long id;

Expand Down Expand Up @@ -158,22 +155,6 @@ public Map<String, String> getStringProperties() {
.collect(toMap(Map.Entry::getKey, e -> (String) e.getValue()));
}

public void setNodeStatusCode(int tryCounter, int code) {
if (nodeStatusCodes == null) {
nodeStatusCodes = new int[getDestinations().size()];
}
nodeStatusCodes[tryCounter % getDestinations().size()] = code;
}

public void trackNodeException(int tryCounter, Exception e) {
if (!TRUE.equals(properties.get(TRACK_NODE_STATUS)))
return;
if (nodeExceptions == null) {
nodeExceptions = new Exception[getDestinations().size()];
}
nodeExceptions[tryCounter % getDestinations().size()] = e;
}

@Override
public void detach() {
super.detach();
Expand All @@ -200,14 +181,6 @@ public void setId(long id) {
this.id = id;
}

public int[] getNodeStatusCodes() {
return nodeStatusCodes;
}

public Exception[] getNodeExceptions() {
return nodeExceptions;
}

public String getInboundProtocol() {
Proxy rule = getProxy();
if (!(rule instanceof SSLableProxy sp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public Outcome handleRequest(Exchange exc) {
try {
exc.getDestinations().add(getForwardingDestination(exc));
} catch (URISyntaxException e) {
ProblemDetails pd = user(false, "invalid-path")
ProblemDetails pd = user(getRouter().isProduction(), "invalid-path")
.title("Invalid request path")
.detail(getMessageForURISyntaxException(exc, e))
.internal("path", exc.getRequest().getUri());
Expand Down Expand Up @@ -109,12 +109,12 @@ protected String getAddressFromTargetElement(Exchange exc) throws MalformedURLEx

if (p.getTargetURL() != null) {
String targetURL = p.getTarget().compileUrl(exc, REQUEST);
if (targetURL.startsWith("http")) {
if (targetURL.startsWith("http") || targetURL.startsWith("internal")) {
String basePath = UriUtil.getPathFromURL(router.getUriFactory(), targetURL);
if (basePath.isEmpty() || "/".equals(basePath)) {
URL base = new URL(targetURL);
if (basePath == null || basePath.isEmpty() || "/".equals(basePath)) {
URI base = new URI(targetURL);
// Resolve and normalize slashes consistently with the branch below.
return new URL(base, getUri(exc)).toString();
return base.resolve(getUri(exc)).toString();
}
}
return targetURL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,19 @@ protected void creatExchangeMetaTable(String id) {
end();
}

protected void createExchangeNodeStatusTable(String id) {
table().attr("cellpadding", "0", "cellspacing", "0", "border", "0",
"class", "display", "id", id);
thead();
tr();
createThs("Node", "Result");
end();
end();
tbody();
end();
end();
}

protected void creatHeaderTable(String id) {
table().attr("cellpadding", "0", "cellspacing", "0", "border", "0",
"class", "display", "id", id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public Response getExchange(QueryParameter params, String relativeRootPath) thro
return Response.notFound().build();
}

return json(gen -> writeExchange(exc, gen));
return json(gen -> writeExchange(exc, gen, true));
}

@Mapping("/admin/rest/exchanges(/?\\?.*)?")
Expand All @@ -241,7 +241,7 @@ public Response getExchanges(QueryParameter params, String relativeRootPath) thr
gen.writeStartObject();
gen.writeArrayFieldStart("exchanges");
for (AbstractExchange e : res.getExchanges()) {
writeExchange(e, gen);
writeExchange(e, gen, false);
}
gen.writeEndArray();
gen.writeNumberField("total", res.getCount());
Expand All @@ -250,7 +250,7 @@ public Response getExchanges(QueryParameter params, String relativeRootPath) thr
});
}

private void writeExchange(AbstractExchange exc, JsonGenerator gen) throws IOException {
private void writeExchange(AbstractExchange exc, JsonGenerator gen, boolean detail) throws IOException {
gen.writeStartObject();
gen.writeNumberField("id", exc.getId());
if (exc.getResponse() != null) {
Expand Down Expand Up @@ -310,9 +310,48 @@ private void writeExchange(AbstractExchange exc, JsonGenerator gen) throws IOExc

gen.writeNumberField("duration", exc.getTimeResReceived() - exc.getTimeReqSent());
gen.writeStringField("msgFilePath", JDBCUtil.getFilePath(exc));

if (detail)
writeExchangeDetailFields(exc, gen);

gen.writeEndObject();
}

private static void writeExchangeDetailFields(AbstractExchange exc, JsonGenerator gen) throws IOException {
List<String> destinations = exc.getDestinations();
if (destinations != null) {
gen.writeArrayFieldStart("destinations");
for (String destination : destinations) {
if (destination == null)
gen.writeNull();
else
gen.writeString(destination);
}
gen.writeEndArray();
}
if (exc.getNodeStatusTracker() != null) {
int[] nodeStatusCodes = exc.getNodeStatusTracker().getNodeStatusCodes();
if (nodeStatusCodes != null) {
gen.writeArrayFieldStart("nodeStatusCodes");
for (int statusCode : nodeStatusCodes) {
gen.writeNumber(statusCode);
}
gen.writeEndArray();
}
Exception[] nodeExceptions = exc.getNodeStatusTracker().getNodeExceptions();
if (nodeExceptions != null) {
gen.writeArrayFieldStart("nodeExceptions");
for (Exception ex : nodeExceptions) {
if (ex == null || ex.getMessage() == null)
gen.writeNull();
else
gen.writeString(ex.getMessage());
}
gen.writeEndArray();
}
}
}

public static String getClientAddr(boolean useXForwardedForAsClientAddr, AbstractExchange exc) {
return AdminApiInterceptor.getClientAddr(useXForwardedForAsClientAddr, exc);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,10 @@ protected void createTabContent() {
end();
div().id("tab1");
creatExchangeMetaTable("request-meta");
div().id("node-status").style("display:none");
h3().text("Node Status").end();
createExchangeNodeStatusTable("request-node-status");
end();
h3().text("Content").end();
div().align("right").a().id("request-download-button").text("Download").end().end();
div().id("requestContentTab");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import org.springframework.context.*;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.*;

import static com.predic8.membrane.core.http.Request.*;
Expand All @@ -40,7 +42,9 @@
* Periodically checks the health of all clusters registered
* on the router and updates each {@link Node}'s status accordingly.
* When initialized, it schedules a task to call each {@link Node}'s health
* endpoint and marks nodes as {@link Status#UP} or {@link Status#DOWN} based on the HTTP response.
* endpoint and marks nodes as {@link Status#UP} or {@link Status#DOWN} based on the result:
* If a health URL is configured for the node, it performs an HTTP request against that endpoint.
* Otherwise, it performs a TCP check against the node's host and port.
* This ensures the load balancer always has up-to-date status for routing decisions.
* @example <a href="https://github.com/membrane/api-gateway/tree/master/distribution/examples/loadbalancing/6-health-monitor">health monitor example</a>
* @topic 4. Monitoring, Logging and Statistics
Expand Down Expand Up @@ -113,13 +117,16 @@ public void run() {
}

private Status isHealthy(Node node) {
String url = getNodeHealthEndpoint(node);
try {
return getStatus(node, doCall(url));
} catch (Exception e) {
log.warn("Calling health endpoint failed: {}, {}", url, e.getMessage());
return DOWN;
String healthUrl = node.getHealthUrl();
if(healthUrl != null) {
try {
return getStatus(node, doCall(healthUrl));
} catch (Exception e) {
log.warn("Calling health endpoint failed: {}, {}", healthUrl, e.getMessage());
return DOWN;
}
}
return tcpHealthy(node);
}

private static Status getStatus(Node node, Exchange exc) {
Expand All @@ -140,10 +147,18 @@ private static Status getStatus(Node node, Exchange exc) {
return UP;
}

private static String getNodeHealthEndpoint(Node node) {
return node.getHealthUrl() != null
? node.getHealthUrl()
: getUrl(node.getHost(), node.getPort());
public Status tcpHealthy(Node node) {
final String host = node.getHost();
final int port = node.getPort();

try(Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), httpClientConfig.getConnection().getTimeout());
log.debug("Node {}:{} is healthy", host, port);
return UP;
} catch (Exception e) {
log.warn("TCP health check failed for {}:{} - {}", host, port, e);
return DOWN;
}
}

private Exchange doCall(String url) throws Exception {
Expand Down
Loading