Skip to content

Commit b9bdf15

Browse files
authored
fix: ListTask's statusTimestampAfter only accept ISO 8601 format (#640)
Fixes #639 🦕 Signed-off-by: Jeff Mesnil <jmesnil@ibm.com>
1 parent b63f28c commit b9bdf15

4 files changed

Lines changed: 47 additions & 27 deletions

File tree

spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ private static JsonProcessingException convertProtoBufExceptionToJsonProcessingE
489489
}
490490

491491
// Extract field name if present in error message - check common prefixes
492-
String[] prefixes = {"Cannot find field: ", "Invalid value for", "Invalid enum value:"};
492+
String[] prefixes = {"Cannot find field: ", "Invalid value for", "Invalid enum value:", "Failed to parse"};
493493
for (String prefix : prefixes) {
494494
if (message.contains(prefix)) {
495495
return new InvalidParamsJsonMappingException(ERROR_MESSAGE.formatted(message.substring(message.indexOf(prefix) + prefix.length())), id);

spec-grpc/src/test/java/io/a2a/grpc/utils/JSONRPCUtilsTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
88
import static org.junit.jupiter.api.Assertions.assertNotNull;
99
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
import static org.junit.jupiter.api.Assertions.fail;
1011

1112
import com.google.gson.JsonSyntaxException;
1213
import io.a2a.jsonrpc.common.json.InvalidParamsJsonMappingException;
@@ -143,6 +144,42 @@ public void testParseInvalidProtoStructure_ThrowsInvalidParamsJsonMappingExcepti
143144
assertEquals(ERROR_MESSAGE.formatted("invalid_field in message a2a.v1.CreateTaskPushNotificationConfigRequest"), exception.getMessage());
144145
}
145146

147+
@Test
148+
public void testParseNumericalTimestampThrowsInvalidParamsJsonMappingException() {
149+
String valideRequest = """
150+
{
151+
"jsonrpc": "2.0",
152+
"method": "ListTasks",
153+
"id": "1",
154+
"params": {
155+
"statusTimestampAfter": "2023-10-27T10:00:00Z"
156+
}
157+
}
158+
""";
159+
String invalidRequest = """
160+
{
161+
"jsonrpc": "2.0",
162+
"method": "ListTasks",
163+
"id": "2",
164+
"params": {
165+
"statusTimestampAfter": "1"
166+
}
167+
}
168+
""";
169+
170+
try {
171+
A2ARequest<?> request = JSONRPCUtils.parseRequestBody(valideRequest, null);
172+
assertEquals(1, request.getId());
173+
} catch (JsonProcessingException e) {
174+
fail(e);
175+
}
176+
InvalidParamsJsonMappingException exception = assertThrows(
177+
InvalidParamsJsonMappingException.class,
178+
() -> JSONRPCUtils.parseRequestBody(invalidRequest, null)
179+
);
180+
assertEquals(2, exception.getId());
181+
}
182+
146183
@Test
147184
public void testParseMissingField_ThrowsInvalidParamsError() throws JsonMappingException {
148185
String missingRoleMessage= """

transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -268,25 +268,12 @@ public HTTPRestResponse listTasks(@Nullable String contextId, @Nullable String s
268268
paramsBuilder.tenant(tenant);
269269
if (statusTimestampAfter != null) {
270270
try {
271-
// Try parsing as Unix milliseconds first (integer)
272-
long millis = Long.parseLong(statusTimestampAfter);
273-
if (millis < 0L) {
274-
Map<String, Object> errorData = new HashMap<>();
275-
errorData.put("parameter", "statusTimestampAfter");
276-
errorData.put("reason", "Must be a non-negative timestamp value, got: " + millis);
277-
throw new InvalidParamsError(null, "Invalid params", errorData);
278-
}
279-
paramsBuilder.statusTimestampAfter(Instant.ofEpochMilli(millis));
280-
} catch (NumberFormatException nfe) {
281-
// Fall back to ISO-8601 format
282-
try {
283-
paramsBuilder.statusTimestampAfter(Instant.parse(statusTimestampAfter));
284-
} catch (DateTimeParseException e) {
285-
Map<String, Object> errorData = new HashMap<>();
286-
errorData.put("parameter", "lastUpdatedAfter");
287-
errorData.put("reason", "Must be valid Unix milliseconds or ISO-8601 timestamp");
288-
throw new InvalidParamsError(null, "Invalid params", errorData);
289-
}
271+
paramsBuilder.statusTimestampAfter(Instant.parse(statusTimestampAfter));
272+
} catch (DateTimeParseException e) {
273+
Map<String, Object> errorData = new HashMap<>();
274+
errorData.put("parameter", "statusTimestampAfter");
275+
errorData.put("reason", "Must be an ISO-8601 timestamp");
276+
throw new InvalidParamsError(null, "Invalid params", errorData);
290277
}
291278
}
292279
if (includeArtifacts != null) {

transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -887,16 +887,12 @@ public void testListTasksNegativeTimestampReturns422() {
887887
@Test
888888
public void testListTasksUnixMillisecondsTimestamp() {
889889
RestHandler handler = new RestHandler(CARD, requestHandler, internalExecutor);
890-
taskStore.save(MINIMAL_TASK, false);
891890

892-
// Unix milliseconds timestamp should be accepted
893-
String timestampMillis = String.valueOf(System.currentTimeMillis() - 10000);
891+
// Unix milliseconds timestamp are no longer accepted
894892
RestHandler.HTTPRestResponse response = handler.listTasks(null, null, null, null,
895-
null, timestampMillis, null, "", callContext);
893+
null, "1234567", null, "", callContext);
896894

897-
Assertions.assertEquals(200, response.getStatusCode());
898-
Assertions.assertEquals("application/json", response.getContentType());
899-
Assertions.assertTrue(response.getBody().contains("tasks"));
895+
Assertions.assertEquals(422, response.getStatusCode());
900896
}
901897

902898
@Test

0 commit comments

Comments
 (0)