Skip to content

Commit 709e1c3

Browse files
authored
feat(api): support int64_as_string parameter for GET requests (#6699)
* 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. * refactor(api): address review feedback on int64_as_string PR Three small adjustments per review on PR #6699: - GetTransactionCountByBlockNumServlet: add trailing newline at end of file to satisfy checkstyle. - Util.getInt64AsString: align control flow with the existing Util.getVisible (single-return via local boolean, Boolean.valueOf instead of Boolean.parseBoolean). Functionally identical -- both return true only when the parameter value is "true" (case-insensitive). - Util.INT64_AS_STRING -> Util.INT64_AS_STRING_PARAM: rename the public parameter-name constant to avoid potential confusion with the unrelated private ThreadLocal field of the same simple name in JsonFormat. The user-facing query parameter remains "int64_as_string" -- only the Java identifier changes.
1 parent 03bd4af commit 709e1c3

9 files changed

Lines changed: 525 additions & 9 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/GetRewardServlet.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) {
2424
if (address != null) {
2525
value = manager.getMortgageService().queryReward(address);
2626
}
27-
response.getWriter().println("{\"reward\": " + value + "}");
27+
String out = JsonFormat.isInt64AsString()
28+
? "{\"reward\": \"" + value + "\"}"
29+
: "{\"reward\": " + value + "}";
30+
response.getWriter().println(out);
2831
} catch (DecoderException | IllegalArgumentException e) {
2932
try {
3033
response.getWriter()

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

Lines changed: 5 additions & 2 deletions
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
}
45-
}
48+
}

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
@@ -116,6 +116,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
116116
String contextPath = req.getContextPath();
117117
String url = Strings.isNullOrEmpty(req.getServletPath())
118118
? MetricLabels.UNDEFINED : contextPath + req.getServletPath();
119+
// int64_as_string is honored only on GET requests (URL query). POST is intentionally
120+
// unsupported because reading the body here would consume request.getReader() and
121+
// break downstream servlets that read it themselves.
122+
if ("GET".equalsIgnoreCase(req.getMethod())) {
123+
JsonFormat.setInt64AsString(Util.getInt64AsString(req));
124+
}
119125
try {
120126
resp.setContentType("application/json; charset=utf-8");
121127

@@ -133,6 +139,10 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
133139
} catch (Exception unexpected) {
134140
logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected);
135141
} finally {
142+
// CRITICAL: this clear pairs with the setInt64AsString call above. Removing it
143+
// will leak int64_as_string state across requests on reused Tomcat threads,
144+
// producing intermittent quoted/unquoted output that is very hard to debug.
145+
JsonFormat.clearInt64AsString();
136146
if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) {
137147
((IPreemptibleRateLimiter) rateLimiter).release();
138148
}

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

Lines changed: 16 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_PARAM = "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";
@@ -348,6 +349,21 @@ public static boolean existVisible(final HttpServletRequest request) {
348349
return Objects.nonNull(request.getParameter(VISIBLE));
349350
}
350351

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

0 commit comments

Comments
 (0)