Skip to content

Commit 4783f68

Browse files
committed
feat(api): add centralized int64_as_string support for #6568
Add a thread-local in JsonFormat that, when set, serializes int64/uint64 proto fields 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). The flag is read in strict mode: - GET requests: URL query, parsed once in RateLimiterServlet.service - POST requests: request body, parsed once in PostParams.getPostParams The thread-local is cleared in service()'s finally block so reused threads do not leak state across requests. POST URL query is intentionally not read in service() because calling getParameter() on a POST consumes application/x-www-form-urlencoded bodies and would break downstream getReader(). This also gives clients a single, unambiguous contract: GET reads the URL, POST reads the body. - 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) + getInt64AsStringPost (JSON body, mirrors getVisiblePost). Also extract Util.decodeAddress (shared between getAddress and servlets that read the address from a JSON body) and add Util.processAddressError (shared "INVALID address" error response). - PostParams.getPostParams: call JsonFormat.setInt64AsString with the parsed body value; cleanup is centralized in RateLimiterServlet. - RateLimiterServlet.service: set ThreadLocal from URL query on GET; clear in finally for both GET and POST.
1 parent 09127ab commit 4783f68

4 files changed

Lines changed: 124 additions & 14 deletions

File tree

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

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,42 @@ 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 and in {@code PostParams.getPostParams} for POST.
98+
* Does not support nested scopes — a single set/clear pair per request.
99+
*/
100+
private static final ThreadLocal<Boolean> INT64_AS_STRING =
101+
ThreadLocal.withInitial(() -> false);
102+
103+
/**
104+
* Set whether int64/uint64 protobuf fields are serialized as quoted JSON strings to avoid
105+
* precision loss in clients whose native number type cannot safely represent integers above
106+
* 2^53 - 1 (e.g. JavaScript). Must be paired with {@link #clearInt64AsString()} in a
107+
* finally block.
108+
*/
109+
public static void setInt64AsString(boolean enabled) {
110+
INT64_AS_STRING.set(enabled);
111+
}
112+
113+
/**
114+
* Clear the int64-as-string thread-local. Always call from a finally block to avoid
115+
* polluting subsequent requests on the same (reused) thread.
116+
*/
117+
public static void clearInt64AsString() {
118+
INT64_AS_STRING.remove();
119+
}
120+
121+
/**
122+
* Whether the current thread is in int64-as-string mode. Used by servlets that build
123+
* JSON literals manually (i.e. do not go through {@link #printToString}).
124+
*/
125+
public static boolean isInt64AsString() {
126+
return INT64_AS_STRING.get();
127+
}
128+
93129
/**
94130
* Outputs a textual representation of the Protocol Message supplied into the parameter output.
95131
* (This representation is the new version of the classic "ProtocolPrinter" output from the
@@ -340,26 +376,41 @@ private static void printFieldValue(FieldDescriptor field, Object value,
340376
throws IOException {
341377
switch (field.getType()) {
342378
case INT32:
343-
case INT64:
344379
case SINT32:
345-
case SINT64:
346380
case SFIXED32:
347-
case SFIXED64:
348381
case FLOAT:
349382
case DOUBLE:
350383
case BOOL:
351384
// Good old toString() does what we want for these types.
352385
generator.print(value.toString());
353386
break;
354387

388+
case INT64:
389+
case SINT64:
390+
case SFIXED64:
391+
if (INT64_AS_STRING.get()) {
392+
generator.print("\"");
393+
generator.print(value.toString());
394+
generator.print("\"");
395+
} else {
396+
generator.print(value.toString());
397+
}
398+
break;
399+
355400
case UINT32:
356401
case FIXED32:
357402
generator.print(unsignedToString((Integer) value));
358403
break;
359404

360405
case UINT64:
361406
case FIXED64:
362-
generator.print(unsignedToString((Long) value));
407+
if (INT64_AS_STRING.get()) {
408+
generator.print("\"");
409+
generator.print(unsignedToString((Long) value));
410+
generator.print("\"");
411+
} else {
412+
generator.print(unsignedToString((Long) value));
413+
}
363414
break;
364415

365416
case STRING:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public static PostParams getPostParams(HttpServletRequest request) throws Except
2828
input = getJsonString(input);
2929
}
3030
boolean visible = Util.getVisiblePost(input);
31+
// Strict mode: POST reads int64_as_string from the body only. Set the ThreadLocal here
32+
// so all downstream JsonFormat.printToString calls in this request see the right value.
33+
// Cleared in RateLimiterServlet.service finally block.
34+
JsonFormat.setInt64AsString(Util.getInt64AsStringPost(input));
3135
return new PostParams(input, visible);
3236
}
3337
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ 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+
// Strict mode: GET reads int64_as_string from URL query here; POST reads it from the
106+
// request body in PostParams.getPostParams (or in servlets that manually parse the body).
107+
// Calling getParameter() on POST would consume application/x-www-form-urlencoded bodies
108+
// and break downstream getReader() — so we deliberately skip URL parsing on POST.
109+
if ("GET".equalsIgnoreCase(req.getMethod())) {
110+
JsonFormat.setInt64AsString(Util.getInt64AsString(req));
111+
}
105112
try {
106113
resp.setContentType("application/json; charset=utf-8");
107114

@@ -119,6 +126,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp)
119126
} catch (Exception unexpected) {
120127
logger.error("Http Api {}, Method:{}. Error:", url, req.getMethod(), unexpected);
121128
} finally {
129+
JsonFormat.clearInt64AsString();
122130
if (rateLimiter instanceof IPreemptibleRateLimiter && acquireResource) {
123131
((IPreemptibleRateLimiter) rateLimiter).release();
124132
}

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

Lines changed: 57 additions & 10 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,18 @@ 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)}. Intended for doGet on whitelisted query
353+
* servlets and for any doPost that reads visible from the URL query.
354+
*/
355+
public static boolean getInt64AsString(final HttpServletRequest request) {
356+
if (StringUtil.isNotBlank(request.getParameter(INT64_AS_STRING))) {
357+
return Boolean.parseBoolean(request.getParameter(INT64_AS_STRING));
358+
}
359+
return false;
360+
}
361+
349362
public static boolean getVisiblePost(final String input) {
350363
boolean visible = false;
351364
if (StringUtil.isNotBlank(input)) {
@@ -358,6 +371,21 @@ public static boolean getVisiblePost(final String input) {
358371
return visible;
359372
}
360373

374+
/**
375+
* Read int64_as_string from the POST body JSON string. Mirrors
376+
* {@link #getVisiblePost(String)}. Used by {@link PostParams#getPostParams} and by
377+
* servlets that manually read the request body (e.g. GetPaginatedProposalListServlet).
378+
*/
379+
public static boolean getInt64AsStringPost(final String input) {
380+
if (StringUtil.isNotBlank(input)) {
381+
JSONObject jsonObject = JSON.parseObject(input);
382+
if (jsonObject.containsKey(INT64_AS_STRING)) {
383+
return Boolean.parseBoolean(jsonObject.getString(INT64_AS_STRING));
384+
}
385+
}
386+
return false;
387+
}
388+
361389
public static String getContractType(final String input) {
362390
String contractType = null;
363391
JSONObject jsonObject = JSON.parseObject(input);
@@ -483,6 +511,20 @@ public static void processError(Exception e, HttpServletResponse response) {
483511
}
484512
}
485513

514+
/**
515+
* Write an "INVALID address" error JSON to the response. Used by servlets that decode an
516+
* address from the request (e.g. GetRewardServlet, GetBrokerageServlet) to keep the error
517+
* payload shape consistent and avoid duplicating the catch block.
518+
*/
519+
public static void processAddressError(Exception e, HttpServletResponse response) {
520+
try {
521+
response.getWriter()
522+
.println("{\"Error\": " + "\"INVALID address, " + e.getMessage() + "\"}");
523+
} catch (IOException ioe) {
524+
logger.debug("IOException: {}", ioe.getMessage());
525+
}
526+
}
527+
486528
public static String convertOutput(Account account) {
487529
if (account.getAssetIssuedID().isEmpty()) {
488530
return JsonFormat.printToString(account, false);
@@ -509,17 +551,22 @@ public static void printAccount(Account reply, HttpServletResponse response, Boo
509551
}
510552

511553
public static byte[] getAddress(HttpServletRequest request) throws Exception {
512-
byte[] address = null;
513-
String addressParam = "address";
514-
String addressStr = checkGetParam(request, addressParam);
515-
if (StringUtils.isNotBlank(addressStr)) {
516-
if (StringUtils.startsWith(addressStr, Constant.ADD_PRE_FIX_STRING_MAINNET)) {
517-
address = Hex.decode(addressStr);
518-
} else {
519-
address = decodeFromBase58Check(addressStr);
520-
}
554+
return decodeAddress(checkGetParam(request, "address"));
555+
}
556+
557+
/**
558+
* Decode an address string (hex with mainnet prefix, or base58check) to raw bytes.
559+
* Returns null for blank input. Shared by {@link #getAddress(HttpServletRequest)} and by
560+
* servlets that parse the address from a manually-read JSON body (e.g. GetRewardServlet).
561+
*/
562+
public static byte[] decodeAddress(String addressStr) {
563+
if (StringUtils.isBlank(addressStr)) {
564+
return null;
565+
}
566+
if (StringUtils.startsWith(addressStr, Constant.ADD_PRE_FIX_STRING_MAINNET)) {
567+
return Hex.decode(addressStr);
521568
}
522-
return address;
569+
return decodeFromBase58Check(addressStr);
523570
}
524571

525572
private static String checkGetParam(HttpServletRequest request, String key) throws Exception {

0 commit comments

Comments
 (0)