Skip to content

Commit 33cf931

Browse files
committed
feat(api): support int64_as_string parameter for GET requests (#6568)
Add an opt-in `int64_as_string` query parameter on TRON HTTP GET endpoints. When set, int64/uint64 protobuf fields in the response are serialized as quoted JSON strings to avoid precision loss in clients whose native number type cannot safely represent integers above 2^53 - 1 (e.g. JavaScript). Scope: GET only. POST is intentionally unsupported because reading the request body in a centralized location (RateLimiterServlet.service or a Filter) would consume request.getReader() and break downstream servlets that read the body themselves. Most TRON query endpoints support both GET and POST, so clients that need precision can use the GET form. POST- only write endpoints return Transaction proto whose int64 fields would break round-trip JsonFormat.merge if quoted, so they should not enable this flag in the first place. - JsonFormat: add INT64_AS_STRING ThreadLocal + setInt64AsString / clearInt64AsString / isInt64AsString helpers; split printFieldValue INT64/SINT64/SFIXED64 and UINT64/FIXED64 branches so they emit quoted strings only when the flag is set. - Util: add INT64_AS_STRING constant + getInt64AsString (URL query, mirrors getVisible). - RateLimiterServlet.service: set ThreadLocal from URL query on GET only; clear in finally so reused Tomcat threads do not leak state across requests. - GetBurnTrx / GetPendingSize / GetTransactionCountByBlockNum: emit quoted int64 in their hand-built JSON responses when isInt64AsString is true. - JsonFormatInt64AsStringTest: covers default behavior, int64 / uint64 quoting, non-int64 fields unaffected, nested / map / boundary values (2^53 +/- 1, Long.MAX/MIN, -1), state cleanup (normal close, after exception, explicit clear), thread isolation, thread-reuse anti-pollution. Backward compatibility: requests without int64_as_string=true produce byte-identical responses to develop -- the new code paths are gated entirely on the new flag. Closes #6568.
1 parent 09127ab commit 33cf931

7 files changed

Lines changed: 355 additions & 7 deletions

File tree

framework/src/main/java/org/tron/core/services/http/GetBurnTrxServlet.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ public class GetBurnTrxServlet extends RateLimiterServlet {
1919
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
2020
try {
2121
long value = manager.getDynamicPropertiesStore().getBurnTrxAmount();
22-
response.getWriter().println("{\"burnTrxAmount\": " + value + "}");
22+
String out = JsonFormat.isInt64AsString()
23+
? "{\"burnTrxAmount\": \"" + value + "\"}"
24+
: "{\"burnTrxAmount\": " + value + "}";
25+
response.getWriter().println(out);
2326
} catch (Exception e) {
2427
logger.error("", e);
2528
try {

framework/src/main/java/org/tron/core/services/http/GetPendingSizeServlet.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ public class GetPendingSizeServlet extends RateLimiterServlet {
1919
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
2020
try {
2121
long value = manager.getPendingSize();
22-
response.getWriter().println("{\"pendingSize\": " + value + "}");
22+
String out = JsonFormat.isInt64AsString()
23+
? "{\"pendingSize\": \"" + value + "\"}"
24+
: "{\"pendingSize\": " + value + "}";
25+
response.getWriter().println(out);
2326
} catch (Exception e) {
2427
logger.error("", e);
2528
try {

framework/src/main/java/org/tron/core/services/http/GetTransactionCountByBlockNumServlet.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
4040

4141
private void fillResponse(long num, HttpServletResponse response) throws IOException {
4242
long count = wallet.getTransactionCountByBlockNum(num);
43-
response.getWriter().println("{\"count\": " + count + "}");
43+
String out = JsonFormat.isInt64AsString()
44+
? "{\"count\": \"" + count + "\"}"
45+
: "{\"count\": " + count + "}";
46+
response.getWriter().println(out);
4447
}
4548
}

framework/src/main/java/org/tron/core/services/http/JsonFormat.java

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,41 @@ public class JsonFormat {
9090
BalanceContract.TransactionBalanceTrace.class
9191
);
9292

93+
/**
94+
* Thread-local flag controlling whether int64/uint64 fields are serialized as JSON strings.
95+
* Set via {@link #setInt64AsString(boolean)} early in request handling and cleared via
96+
* {@link #clearInt64AsString()} in a finally block. Centralized in
97+
* {@code RateLimiterServlet.service} for GET requests. Does not support nested scopes.
98+
*/
99+
private static final ThreadLocal<Boolean> INT64_AS_STRING =
100+
ThreadLocal.withInitial(() -> false);
101+
102+
/**
103+
* Set whether int64/uint64 protobuf fields are serialized as quoted JSON strings to avoid
104+
* precision loss in clients whose native number type cannot safely represent integers above
105+
* 2^53 - 1 (e.g. JavaScript). Must be paired with {@link #clearInt64AsString()} in a
106+
* finally block.
107+
*/
108+
public static void setInt64AsString(boolean enabled) {
109+
INT64_AS_STRING.set(enabled);
110+
}
111+
112+
/**
113+
* Clear the int64-as-string thread-local. Always call from a finally block to avoid
114+
* polluting subsequent requests on the same (reused) thread.
115+
*/
116+
public static void clearInt64AsString() {
117+
INT64_AS_STRING.remove();
118+
}
119+
120+
/**
121+
* Whether the current thread is in int64-as-string mode. Used by servlets that build
122+
* JSON literals manually (i.e. do not go through {@link #printToString}).
123+
*/
124+
public static boolean isInt64AsString() {
125+
return INT64_AS_STRING.get();
126+
}
127+
93128
/**
94129
* Outputs a textual representation of the Protocol Message supplied into the parameter output.
95130
* (This representation is the new version of the classic "ProtocolPrinter" output from the
@@ -340,26 +375,41 @@ private static void printFieldValue(FieldDescriptor field, Object value,
340375
throws IOException {
341376
switch (field.getType()) {
342377
case INT32:
343-
case INT64:
344378
case SINT32:
345-
case SINT64:
346379
case SFIXED32:
347-
case SFIXED64:
348380
case FLOAT:
349381
case DOUBLE:
350382
case BOOL:
351383
// Good old toString() does what we want for these types.
352384
generator.print(value.toString());
353385
break;
354386

387+
case INT64:
388+
case SINT64:
389+
case SFIXED64:
390+
if (INT64_AS_STRING.get()) {
391+
generator.print("\"");
392+
generator.print(value.toString());
393+
generator.print("\"");
394+
} else {
395+
generator.print(value.toString());
396+
}
397+
break;
398+
355399
case UINT32:
356400
case FIXED32:
357401
generator.print(unsignedToString((Integer) value));
358402
break;
359403

360404
case UINT64:
361405
case FIXED64:
362-
generator.print(unsignedToString((Long) value));
406+
if (INT64_AS_STRING.get()) {
407+
generator.print("\"");
408+
generator.print(unsignedToString((Long) value));
409+
generator.print("\"");
410+
} else {
411+
generator.print(unsignedToString((Long) value));
412+
}
363413
break;
364414

365415
case STRING:

framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
102102
String contextPath = req.getContextPath();
103103
String url = Strings.isNullOrEmpty(req.getServletPath())
104104
? MetricLabels.UNDEFINED : contextPath + req.getServletPath();
105+
// int64_as_string is honored only on GET requests (URL query). POST is intentionally
106+
// unsupported because reading the body here would consume request.getReader() and
107+
// break downstream servlets that read it themselves.
108+
if ("GET".equalsIgnoreCase(req.getMethod())) {
109+
JsonFormat.setInt64AsString(Util.getInt64AsString(req));
110+
}
105111
try {
106112
resp.setContentType("application/json; charset=utf-8");
107113

@@ -119,6 +125,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
119125
} catch (Exception unexpected) {
120126
logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected);
121127
} finally {
128+
// CRITICAL: this clear pairs with the setInt64AsString call above. Removing it
129+
// will leak int64_as_string state across requests on reused Tomcat threads,
130+
// producing intermittent quoted/unquoted output that is very hard to debug.
131+
JsonFormat.clearInt64AsString();
122132
if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) {
123133
((IPreemptibleRateLimiter) rateLimiter).release();
124134
}

framework/src/main/java/org/tron/core/services/http/Util.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class Util {
6666

6767
public static final String PERMISSION_ID = "Permission_id";
6868
public static final String VISIBLE = "visible";
69+
public static final String INT64_AS_STRING = "int64_as_string";
6970
public static final String TRANSACTION = "transaction";
7071
public static final String TRANSACTION_EXTENSION = "transactionExtension";
7172
public static final String VALUE = "value";
@@ -346,6 +347,20 @@ public static boolean existVisible(final HttpServletRequest request) {
346347
return Objects.nonNull(request.getParameter(VISIBLE));
347348
}
348349

350+
/**
351+
* Read int64_as_string from URL query parameter. Mirrors
352+
* {@link #getVisible(HttpServletRequest)}. The flag is honored only on GET requests
353+
* (read by {@link RateLimiterServlet#service}); POST requests do not support it
354+
* because that would require caching the request body to allow re-reading by
355+
* downstream servlets.
356+
*/
357+
public static boolean getInt64AsString(final HttpServletRequest request) {
358+
if (StringUtil.isNotBlank(request.getParameter(INT64_AS_STRING))) {
359+
return Boolean.parseBoolean(request.getParameter(INT64_AS_STRING));
360+
}
361+
return false;
362+
}
363+
349364
public static boolean getVisiblePost(final String input) {
350365
boolean visible = false;
351366
if (StringUtil.isNotBlank(input)) {

0 commit comments

Comments
 (0)