Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
01fcee0
fix #7963 ClusterControllerTest.watch_withHttp2: use 30000ms timeout
Sumit6307 Jan 29, 2026
1cee03b
feat: add observability metrics for undo log manager
Sumit6307 Feb 2, 2026
628607a
fix(ci): add license header and apply spotless formatting
Sumit6307 Feb 2, 2026
46f4069
test: refactor to use spy instead of mockStatic for better compatibility
Sumit6307 Feb 2, 2026
9475b39
fix: add getRegistry() method for testability - fixes CI build failures
Sumit6307 Feb 2, 2026
0242a9e
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Feb 2, 2026
492578a
feat: add observability metrics for undo log manager
Sumit6307 Feb 5, 2026
e3afae6
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Feb 5, 2026
3b90394
fix: safe access to RegistryFactory to prevent CI failures
Sumit6307 Feb 5, 2026
42d1758
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Mar 5, 2026
49bb1a8
Merge branch '2.x' into feature/undo-log-observability
funky-eyes Mar 17, 2026
e43f3ad
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Mar 25, 2026
ae23264
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Apr 20, 2026
3963d01
feat: optimize metrics registry caching and align counter semantics
Sumit6307 Apr 20, 2026
2ec0203
style: apply spotless code formatting
Sumit6307 Apr 20, 2026
c00fb50
fix: resolve missing metric Id import in test class
Sumit6307 Apr 20, 2026
52754fa
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Apr 23, 2026
37844e3
Merge branch '2.x' into feature/undo-log-observability
Sumit6307 Apr 24, 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
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,25 @@ private static boolean looksLikeJson(byte[] body) {
}
int i = 0;
// Skip optional UTF-8 BOM (0xEF, 0xBB, 0xBF)
if (body.length >= 3
&& (body[0] & 0xFF) == 0xEF
&& (body[1] & 0xFF) == 0xBB
&& (body[2] & 0xFF) == 0xBF) {
if (body.length >= 3 && (body[0] & 0xFF) == 0xEF && (body[1] & 0xFF) == 0xBB && (body[2] & 0xFF) == 0xBF) {
i = 3;
}
// skip leading whitespace (including Unicode NBSP / BOM that survived as whitespace)
while (i < body.length && (body[i] == ' ' || body[i] == '\t'
|| body[i] == '\r' || body[i] == '\n')) {
while (i < body.length && (body[i] == ' ' || body[i] == '\t' || body[i] == '\r' || body[i] == '\n')) {
i++;
}
if (i >= body.length) {
return true;
}
byte first = body[i];
return first == '{' || first == '[' || first == '"'
|| first == 't' || first == 'f' || first == 'n'
|| (first >= '0' && first <= '9') || first == '-';
return first == '{'
|| first == '['
|| first == '"'
|| first == 't'
|| first == 'f'
|| first == 'n'
|| (first >= '0' && first <= '9')
|| first == '-';
}

@Override
Expand Down Expand Up @@ -158,19 +159,18 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
if (node.getRole() == ClusterRole.LEADER) {
headers.add(RAFT_GROUP_HEADER, node.getUnit());
}
Collections.list(request.getHeaderNames())
.forEach(headerName -> {
if (!HttpHeaders.HOST.equalsIgnoreCase(headerName)
&& !HttpHeaders.CONNECTION.equalsIgnoreCase(headerName)
&& !"Keep-Alive".equalsIgnoreCase(headerName)
&& !HttpHeaders.PROXY_AUTHENTICATE.equalsIgnoreCase(headerName)
&& !HttpHeaders.PROXY_AUTHORIZATION.equalsIgnoreCase(headerName)
&& !HttpHeaders.TE.equalsIgnoreCase(headerName)
&& !HttpHeaders.TRAILER.equalsIgnoreCase(headerName)
&& !HttpHeaders.UPGRADE.equalsIgnoreCase(headerName)) {
headers.add(headerName, request.getHeader(headerName));
}
});
Collections.list(request.getHeaderNames()).forEach(headerName -> {
if (!HttpHeaders.HOST.equalsIgnoreCase(headerName)
&& !HttpHeaders.CONNECTION.equalsIgnoreCase(headerName)
&& !"Keep-Alive".equalsIgnoreCase(headerName)
&& !HttpHeaders.PROXY_AUTHENTICATE.equalsIgnoreCase(headerName)
&& !HttpHeaders.PROXY_AUTHORIZATION.equalsIgnoreCase(headerName)
&& !HttpHeaders.TE.equalsIgnoreCase(headerName)
&& !HttpHeaders.TRAILER.equalsIgnoreCase(headerName)
&& !HttpHeaders.UPGRADE.equalsIgnoreCase(headerName)) {
headers.add(headerName, request.getHeader(headerName));
}
});

// Create the HttpEntity with headers and body
HttpMethod httpMethod;
Expand All @@ -197,16 +197,19 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
// headers-only for empty body
httpEntity = new HttpEntity<>(headers);
} else {
// Remove potentially stale length/transfer headers and let the client recompute them
// Remove potentially stale length/transfer headers and let the client recompute
// them
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.remove(HttpHeaders.TRANSFER_ENCODING);
httpEntity = new HttpEntity<>(body, headers);
}
}

try {
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(URI.create(targetUrl), httpMethod, httpEntity, byte[].class);
//Copy headers from proxied response, skipping hop-by-hop and headers we manage ourselves to mitigate
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(
URI.create(targetUrl), httpMethod, httpEntity, byte[].class);
// Copy headers from proxied response, skipping hop-by-hop and headers we manage
// ourselves to mitigate
// security risks from Content-Type manipulation
responseEntity.getHeaders().forEach((key, value) -> {
if (!HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(key)
Expand All @@ -225,7 +228,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
});
// Force a safe Content-Type: reject HTML/XML types that could
// execute scripts; fall back to application/json
String proxiedContentType = responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
String proxiedContentType =
responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
String safeContentType;
if (isSafeContentType(proxiedContentType)) {
safeContentType = proxiedContentType;
Expand All @@ -234,16 +238,20 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
}
response.setContentType(safeContentType);
response.setHeader("X-Content-Type-Options", "nosniff");
response.setStatus(responseEntity.getStatusCode().value());
response.setStatus(
responseEntity.getStatusCode().value());
byte[] responseBody = responseEntity.getBody();
// HEAD responses must not include a message body (RFC 7231 §4.3.2)
if (!HttpMethod.HEAD.equals(httpMethod)
&& responseBody != null && responseBody.length > 0) {
&& responseBody != null
&& responseBody.length > 0) {
// For JSON content type, validate that the body actually looks
// like JSON to prevent XSS via crafted upstream responses
if (safeContentType.toLowerCase(Locale.ROOT).contains("application/json")
&& !looksLikeJson(responseBody)) {
LOGGER.warn("Upstream returned non-JSON body for Content-Type {}, replacing with error response", safeContentType);
LOGGER.warn(
"Upstream returned non-JSON body for Content-Type {}, replacing with error response",
safeContentType);
response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
response.setContentType("application/json;charset=UTF-8");
responseBody = "{\"error\":\"Upstream returned invalid response body\"}"
Expand All @@ -256,7 +264,10 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
// Client likely disconnected (broken pipe); log at debug
// level and do NOT attempt sendError – the response may
// already be committed.
LOGGER.debug("Failed to write proxy response body (client disconnect?): {}", e.getMessage(), e);
LOGGER.debug(
"Failed to write proxy response body (client disconnect?): {}",
e.getMessage(),
e);
}
}
} catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ void setUp() {
NamingServerNode node = new NamingServerNode();
node.setControl(new Node.Endpoint(TARGET_HOST, TARGET_PORT, "http"));

when(namingManager.getInstances(NAMESPACE, CLUSTER))
.thenReturn(Collections.singletonList(node));
when(namingManager.getInstances(NAMESPACE, CLUSTER)).thenReturn(Collections.singletonList(node));
}

/**
Expand All @@ -97,9 +96,7 @@ void getRequestWithBodyShouldStripBody() throws Exception {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
"{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8),
responseHeaders,
HttpStatus.OK);
"{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8), responseHeaders, HttpStatus.OK);

when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(byte[].class)))
.thenReturn(upstreamResponse);
Expand All @@ -115,9 +112,11 @@ void getRequestWithBodyShouldStripBody() throws Exception {
// Body must be null (stripped for GET)
assertNull(capturedEntity.getBody(), "GET request body should be stripped (null)");
// Content-Length and Transfer-Encoding headers must not be forwarded
assertNull(capturedEntity.getHeaders().get(HttpHeaders.CONTENT_LENGTH),
assertNull(
capturedEntity.getHeaders().get(HttpHeaders.CONTENT_LENGTH),
"Content-Length header should be removed for GET");
assertNull(capturedEntity.getHeaders().get(HttpHeaders.TRANSFER_ENCODING),
assertNull(
capturedEntity.getHeaders().get(HttpHeaders.TRANSFER_ENCODING),
"Transfer-Encoding header should be removed for GET");

// Verify filterChain was NOT invoked (proxied)
Expand All @@ -137,8 +136,7 @@ void headRequestShouldStripBody() throws Exception {

HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json");
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
null, responseHeaders, HttpStatus.OK);
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(null, responseHeaders, HttpStatus.OK);

when(restTemplate.exchange(any(URI.class), eq(HttpMethod.HEAD), any(HttpEntity.class), eq(byte[].class)))
.thenReturn(upstreamResponse);
Expand Down Expand Up @@ -167,9 +165,7 @@ void postRequestShouldForwardBody() throws Exception {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
"{\"result\":\"created\"}".getBytes(StandardCharsets.UTF_8),
responseHeaders,
HttpStatus.OK);
"{\"result\":\"created\"}".getBytes(StandardCharsets.UTF_8), responseHeaders, HttpStatus.OK);

when(restTemplate.exchange(any(URI.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(byte[].class)))
.thenReturn(upstreamResponse);
Expand All @@ -182,7 +178,8 @@ void postRequestShouldForwardBody() throws Exception {

byte[] capturedBody = entityCaptor.getValue().getBody();
assertNotNull(capturedBody, "POST body should not be null");
assertEquals(new String(bodyBytes, StandardCharsets.UTF_8),
assertEquals(
new String(bodyBytes, StandardCharsets.UTF_8),
new String(capturedBody, StandardCharsets.UTF_8),
"POST request body should be forwarded as-is");
}
Expand Down Expand Up @@ -215,16 +212,14 @@ void nonJsonBodyWithJsonContentTypeShouldReturn502() throws Exception {
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json");
// Upstream sends HTML disguised as JSON
byte[] htmlBody = "<html><script>alert('xss')</script></html>".getBytes(StandardCharsets.UTF_8);
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
htmlBody, responseHeaders, HttpStatus.OK);
ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(htmlBody, responseHeaders, HttpStatus.OK);

when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(byte[].class)))
.thenReturn(upstreamResponse);

filter.doFilter(request, response, filterChain);

assertEquals(502, response.getStatus(),
"Should return 502 when upstream body is not valid JSON");
assertEquals(502, response.getStatus(), "Should return 502 when upstream body is not valid JSON");
String body = response.getContentAsString();
assertEquals("{\"error\":\"Upstream returned invalid response body\"}", body);
}
Expand All @@ -240,4 +235,3 @@ private MockHttpServletRequest createConsoleRequest(String method) {
return request;
}
}

10 changes: 10 additions & 0 deletions rm-datasource/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,15 @@
<artifactId>json-common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-metrics-core</artifactId>
<version>${project.version}</version>
</dependency>
Comment thread
Sumit6307 marked this conversation as resolved.
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seata-metrics-registry-compact</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
Loading
Loading