@@ -488,7 +488,27 @@ public Command<DatasetVersion> handleLatestPublished() {
488488 }
489489
490490 protected void validateInternalTimestampIsNotOutdated (DvObject dvObject , String sourceLastUpdateTime ) throws WrappedResponse {
491- Date date = sourceLastUpdateTime != null ? DateUtil .parseDate (sourceLastUpdateTime .replaceFirst ("Z$" , "+0000" ), "yyyy-MM-dd'T'HH:mm:ssZ" ) : null ;
491+ // The timestamp string must always be in UTC, ISO 8601-formatted
492+ // for example: 2026-04-22T14:30:00Z. This is explicitly specified in the
493+ // API guide.
494+ //
495+ // In the intended workflow, the clients will be reusing the last update
496+ // timestamps obtained from the output of other Dataverse APIs, such as
497+ // /versions and /files, where they are always in that form, regardless
498+ // of the actual time zone the server lives in.
499+ //
500+ // For consistency, we do not want to accept any other formats or timezones,
501+ // and will reject anything that does not match "yyyy-MM-dd'T'HH:mm:ss'Z'".
502+ // For that reason there is an explicit check added for .endsWith("Z").
503+ // The "X" in the parsing format string will recognize literal 'Z' as "+0000",
504+ // but it would also accept other ISO 8601 timezones, such as "+0400" for
505+ // EDT, etc. In theory, we could accept all these other notations - in
506+ // case the client decided to convert the UTC timestamp they received from
507+ // Dataverse into that... but there is really no good reason to encourage
508+ // that.
509+ Date date = sourceLastUpdateTime != null && sourceLastUpdateTime .endsWith ("Z" )
510+ ? DateUtil .parseDate (sourceLastUpdateTime , "yyyy-MM-dd'T'HH:mm:ssX" )
511+ : null ;
492512 if (date == null ) {
493513 throw new WrappedResponse (
494514 badRequest (BundleUtil .getStringFromBundle ("jsonparser.error.parsing.date" , Collections .singletonList (sourceLastUpdateTime )))
0 commit comments