Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JSRangeErrorException - "Invalid timezone name!"] Fix Intl.DateTimeFormat bug with normalizing timeZone value #571

Closed

Conversation

anton-patrushev
Copy link

@anton-patrushev anton-patrushev commented Aug 13, 2021

Info

I have tried to use hermes-engine v0.8.0 with react-native v0.65-rc4. The main reason was to test Intl API implementation since my RN project uses Hermes engine (instead of JSC) for executing JS code on Android. I have also using date-fns & date-fns-tz libraries. The second one relies on Intl API. That's the reason why I've tried to use new hermes-engine with Intl API support.
But when I started to testing it I had realized what something was going wrong. I had noticed what Intl.DateTimeFormat is broken. Below is an example which was broken by Intl.DateTimeFormat implementation.

That is piece of code which is formats date and applies provided timeZone.

import { format } from "date-fns"
import { utcToZonedTime } from "date-fns-tz"

function formatTimezoneDate(date, timezone) {
  return format(utcToZonedTime(date, timezone), 'MMM do yyyy, h:mm:ss a');
}

const date = new Date("2021-08-03T22:04:46Z");
const timeZone = "America/New_York";

const formattedDate = formatTimezoneDate(date, timeZone);

// actualResult
console.log(formattedDate) // "Aug 3rd 2021, 10:04:46 pm"

// expected Result
// "Aug 3rd 2021, 6:04:46 pm"

utcToZonedTime converts provided date into appropriate timeZone and returns newDate.

utcToZonedTime under the hoods uses tzParsetimeZone function which accepts string timeZone value and returns offsetInMilliseconds number value.
This function call isValidTimezoneIANAString. Here isValidTimezoneIANAString function implementation.

function isValidTimezoneIANAString(timeZoneString) {
  try {
    Intl.DateTimeFormat(undefined, {timeZone: timeZoneString});
    return true;
  } catch (error) {
    return false;
  }
}

I have added console.log(error) to catch statement and have seen what Intl.DateTimeFormat throws the next error:

JSRangeErrorException("Invalid timezone name!")
```js

That is why utcToZonedTime function always converts date into UTC timezone (tzParseTimeZone always returns 0. value)
I have found only one place in Hermes source code where it can throw JSRangeErrorException("Invalid timezone name!").

    Object timeZone = JSObjects.Get(options, "timeZone");
    if (JSObjects.isUndefined(timeZone)) {
      timeZone = DefaultTimeZone();
    } else {
      String normalizedTimeZone = normalizeTimeZoneName(JSObjects.getJavaString(timeZone));
      if (!isValidTimeZoneName(normalizedTimeZone)) {
        throw new JSRangeErrorException("Invalid timezone name!");
      }
    }
    mTimeZone = timeZone;

Here is isValidTimeZoneName method implementation under the hood. It is simple as possible. Everything should be ok.

import java.util.TimeZone;

// ...

  @Override
  public boolean isValidTimeZone(String timeZone) {
    return TimeZone.getTimeZone(timeZone).getID().equals(timeZone);
  }

But isValidTimeZone method receive already messed up timeZone string value. In my case instead of receiving America/New_York value it accepts AMERICA/NEW_YORK. This happens because of normalizeTimeZoneName method. This method transforms timeZone string into uppercase version.
That's why TimeZone.getTimeZone("AMERICA/NEW_YORK").getID().equals("AMERICA/NEW_YORK") will always returns false. (TimeZone.getTimeZone("AMERICA/NEW_YORK").getID() always returns "GMT: string)

Brief Changes

  • fix normalizeTimeZoneName method to fit into JDK 1.8 java.util.TimeZone specs.
    As far as I understand there are some differences between 1.7 & 1.8 java.util.TimeZone implementation. JDK 1.7 supply case insensitive TimeZone module, while JDK 1.8 provides case sensitive module for managing TimeZones. That's why calling the next piece of code will have different behaviors.
// on JDK 1.7 it will be "America/New_York", but 1.8 version makes a fallback to "GMT" id.
TimeZone.getTimeZone("AMERICA/NEW_YORK").getID();

I have fixed normalizeTimeZoneName function to follow ECMAScripts specs and make compatibility with JDK 1.8 and higher versions.

String id1 = normalizeTimeZoneName("America/New_York");
String id2 = normalizeTimeZoneName("AMERICA/NEW_YORK");
String id3 = normalizeTimeZoneName("america/new_york");
String id4 = normalizeTimeZoneName("UtC");
String id5 = normalizeTimeZoneName("gMt+07:00");

System.out.println(id1); // "America/New_York"
System.out.println(id2); // "America/New_York"
System.out.println(id3); // "America/New_York"
System.out.println(id4); // "UTC"
System.out.println(id5); // "GMT+07:00"
        
System.out.println(isValidTimeZoneName(id1)); // true
System.out.println(isValidTimeZoneName(id2)); // true
System.out.println(isValidTimeZoneName(id3)); // true
System.out.println(isValidTimeZoneName(id4)); // true
System.out.println(isValidTimeZoneName(id5)); // true

That's how it behaves now. Tested on my End. Sorry for not providing unit tests for it. I wasn't able to find out how your test system is designed.

@facebook-github-bot
Copy link
Contributor

Hi @anton-patrushev!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@anton-patrushev anton-patrushev force-pushed the fix-intl-date-time-format branch from 2cb8965 to c42e5b8 Compare August 13, 2021 09:51
@facebook-github-bot facebook-github-bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Aug 13, 2021
@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@anton-patrushev anton-patrushev marked this pull request as draft August 13, 2021 13:35
@anton-patrushev anton-patrushev force-pushed the fix-intl-date-time-format branch from 0d37e60 to 7991f8c Compare August 13, 2021 17:53
@anton-patrushev anton-patrushev marked this pull request as ready for review August 13, 2021 18:06
@facebook-github-bot
Copy link
Contributor

@mhorowitz has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@mhorowitz
Copy link
Contributor

Thank you for the detailed bug report and the fix!

CircleCI seems to be having issues on Android, but our internal CI system should run tests on this. If you want to run t hem yourself, take a look at https://github.com/facebook/hermes/blob/main/android/intltest/build.gradle which is where the tests are run from. There are instructions at the top. The actual test cases come from https://github.com/tc39/test262

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@anton-patrushev
Copy link
Author

@mhorowitz So what are next steps you propose?
I have tried to run Intl tests for Android. I think I wasn't successful since almost all of them was failed due to some unknown reason. I have implemented one test, which will ensure what Intl.DateTimeFormat API works correctly, but I wasn't able to check, is this test works correctly or not. I have committed that test. But anyway, can we merge this PR (maybe by applying some requested changes or whatever)? Or maybe you could run this test on your internal CI?
It will be great if someone could run this test using this fixed version of Hermes (my fork) and version of Hermes from the main repo and compare the result. It will indicates that my fix is really working (actually for me it works in the real app).

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@anton-patrushev
Copy link
Author

@mhorowitz Any updates?

@facebook-github-bot
Copy link
Contributor

@mhorowitz has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@mhorowitz
Copy link
Contributor

mhorowitz commented Sep 10, 2021

Unfortunately, the regression tests didn't behave as I expected internally, either. I got them running, and the regression tests pass. The test you added in 23616f7 did not, though. I had to tweak it a bit, now I have this:

  rt.evaluateJavaScript(
      new StringBuilder()
          .append("var date = new Date('2021-08-13T14:00:00Z');\n")
          .append("var formattedDate = Intl.DateTimeFormat('en-US', {\n")
          .append("timeZone: 'America/New_York',\n")
          .append("month: 'numeric',\n")
          .append("day: 'numeric',\n")
          .append("hour: 'numeric',\n")
          .append("minute: 'numeric'\n")
          .append("}).format(date);\n")
          .toString());

This passes. However, I realized this doesn't (I think) test the underlying fundamental problem of case folding the time zone. If I modify the test to mangle the case:

          .append("timeZone: 'AMERICA/new_YORK',\n")

The output is GMT, not US/Eastern:

com.facebook.hermes.test.HermesInstrumentationTest > testDateTimeFormat[Pixel_3_API_29(AVD) - 10] FAILED
        org.junit.ComparisonFailure: expected:<'8/13, [10:00 A]M'> but was:<'8/13, [2:00 P]M'>

It's possible I'm misunderstanding the change you've made, but I expected the unusual casing would succeed based on the description in the PR. What am I missing?

@anton-patrushev
Copy link
Author

@mhorowitz Did you run these tests on my branch (with modified Java code) or on other branch, which has the same Java code as the main branch (before my fix)?
This important since I believe AMERICA/new_YORK should work with my fix too. (Because I have implemented function, which is reliable to transform this random case strings into Java TZ compatible format to get the right TimeZone ID).

I will be back with answer (I have an empty project with manually patched hermes-engine executable, which I made locally (thanks to your docs 🤗), to make some kind of manual E2E QA test).
I'm sure it works with America/New_York, but don't remember have I tested it with random case or not 🤔.

@mhorowitz
Copy link
Contributor

@anton-patrushev I did my testing on 692ef46 which appears to to be the top revision in your PR. Is there some other revision I should be testing?

@anton-patrushev
Copy link
Author

@mhorowitz Could I ask you to run this test case (which you mentioned and tweaked) on the main hermes branch to ensure it failed. It should failed since Android Intl API doesn't work for me from the main branch.
In that case we can assume my fix works indeed, otherwise I should think about other issue root-case.
I will be back with my manual tests results.
Thanks in advance!

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@anton-patrushev
Copy link
Author

@mhorowitz FYI
I have created repo with Intl API usage example.
And I have provided some screenshots.
This one uses the build from current branch with my fix.
Android-Hermes-Local-Fix

So you can compare it with the stable hermes-engine package. It doesn't work correctly for this example.
Android-Hermes-Stable

But my fix still doesn't support case insensitivity :(
I will try to fix it and will be back with updates.

@mhorowitz
Copy link
Contributor

mhorowitz commented Sep 22, 2021

@anton-patrushev Thanks for the screen shots! This is consistent with what I see. Your changes prevent Intl from throwing an exception, but do not fix the issues with timezone case sensitivity. I'll hold off merging this for now while you investigate the fix for that.

@andreialecu
Copy link

We're also running into this problem which unfortunately makes Hermes Intl unusable for us.

@anton-patrushev unless I'm reading it wrong, I believe the remaining problem is here:

https://github.com/anton-patrushev/hermes/blob/c31fe892e1a40e37276ad7c29142eecb4168586f/lib/Platform/Intl/java/com/facebook/hermes/intl/DateTimeFormat.java#L246-L252

mTimeZone should be set to the result of the normalization, otherwise it will remain mangled.

@anton-patrushev
Copy link
Author

We're also running into this problem which unfortunately makes Hermes Intl unusable for us.

@anton-patrushev unless I'm reading it wrong, I believe the remaining problem is here:

https://github.com/anton-patrushev/hermes/blob/c31fe892e1a40e37276ad7c29142eecb4168586f/lib/Platform/Intl/java/com/facebook/hermes/intl/DateTimeFormat.java#L246-L252

mTimeZone should be set to the result of the normalization, otherwise it will remain mangled.

Thanks for highlighting it @andreialecu!
I will try to investigate it anytime soon. Seems like your assumption is right!
I will be back with answer!

But just to be on the same page.
@mhorowitz My fix not only prevent Intl from crashing with random case timezone name, but it make Intl works correctly with already normalized timezone name (e.g. America/New_York). Since as for me, it throws an error too, even when I use America/New_York in the ideal case 🤷‍♂️.

I will be back with final fix to support case insensitivity too!

@andreialecu
Copy link

andreialecu commented Sep 23, 2021

I'd like to point out something regarding the implementation in this PR. There are a bunch of timezones which may not pass through the capitalization function properly. Take a look at: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

Examples: Etc/GMT+1, US/Alaska or Africa/Dar_es_Salaam

However, I think it's possible to normalize the timezone in a different way. java.util.TimeZone.getAvailableIDs() does return an array with available time zone names. So if the same upper-casing function was applied to the items in that array and then compared to the input, there should be a match.

The original casing of the match would then be the normalized time zone name.

Hopefully this makes sense. :)

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@anton-patrushev
Copy link
Author

Finally, I'm back :)
So, as @andreialecu proposed, I have reimplemented the whole approach for normalizing and validating timeZone, which comes from the JS side into Android Java internals.

If you would have any questions regarding my approach do not hesitate to ask me.
You can check my repo with the Hermes Intl usage example.
I have updated screenshots. This one with my final fix on Android.
Image

@mhorowitz Please take a look.

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@facebook-github-bot
Copy link
Contributor

@anton-patrushev has updated the pull request. You must reimport the pull request before landing.

@anton-patrushev
Copy link
Author

@mhorowitz
Is there any chance it would be merged?

@anton-patrushev
Copy link
Author

@mhorowitz any updates?

@mhorowitz
Copy link
Contributor

I hope to look at it this week. For the future, I don't tend to check GitHub daily, and almost never on weekends, so pinging me on Saturday morning isn't likely to get a reply until at least Monday.

@anton-patrushev
Copy link
Author

Can someone provide any info on this?
I found it quite critical for Hermes usage in the Android RN apps.
Sorry for tagging @mhorowitz, but you are the only person who was assigned to this PR (and related issue) from the Facebook team.

Can I do something on my end to make it merged?

@andreialecu
Copy link

Hey @Huxpro, hope you don't mind the ping. This issue is preventing Intl from working in most (I think) real-life apps on Android.

Mind taking a look at the PR, please?

@facebook-github-bot
Copy link
Contributor

@mhorowitz has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

Copy link
Contributor

@mhorowitz mhorowitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/tc39/test262/blob/main/test/intl402/DateTimeFormat/timezone-invalid.js is failing, I suspect due to applying additional locale comparisons as noted in code comments below.

I found this by running the test suite on device. The instructions are a bit scattered in the source tree, so here's what you need.

  • edit hermes/android/build.gradle to include
    "${hermesHostBuild}/build_host_hermesc/ImportHermesc.cmake",
    On line 31 or so. it should be pretty clear where it goes.
  • Run an android emulator, or connect a device
  • Run these commands
export HERMES_WS_DIR=.../parent-of-hermes-checkout
cd "$HERMES_WS_DIR"
hermes/utils/build/configure.py ./build_host_hermesc
cmake --build ./build_host_hermesc --target hermesc
cd hermes/android
./gradlew intl:prepareTest262
./gradlew intl:connectedAndroidTest

The complete tests take a couple minutes to run on my emulator. On a real device they may take longer. logcat will print output fairly continuously while it runs.

You may trip over a bug which causes a crash result like
Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.''. Check device logcat for details
If so, you can apply https://gist.github.com/mhorowitz/f314397b97f0419c53011dded9c89b89 to comment out the problematic tests.

At that point, when you rerun the tests (the final command above) you should see output like
> There were failing tests. See the report at: file:///.../build_android/intltest/reports/androidTests/connected/flavors/debugAndroidTest/index.html
Open that file in your browser, and it will show you failures. Probably, you will see HermesIntlDateFormatTest has failed. Unfortunately, the specific failure does not appear here. You can grep for it in logcat:

$ adb logcat -d | grep 'Failed Tests:'
10-06 18:50:10.244  6896  6917 V HermesIntlNumberFormatTest: Failed Tests: test262/test/intl402/DateTimeFormat/timezone-invalid.js : Invalid time zone name Europe/İstanbul was not rejected. Expected a RangeError to be thrown but no exception was thrown at all

.filter(new Predicate<String>() {
@Override
public boolean test(String tz) {
return tz.equalsIgnoreCase(timeZone);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is incorrect. https://tc39.es/ecma402/#sec-case-sensitivity-and-case-mapping only allows mapping/folding ascii a-z <-> A-Z. equalsIgnoreCase is probably applying additional locale-specific comparisons.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I don't find it critical. It's strange requirement. But ok, that's how it is.
Any way, I believe my changes will allow map A-Z to a-z and vice versa and it shouldn't be critical issue. I'm finding not working America/New_York (e.g. timezones from IANA TZ db in its original look) more important & crucial.

So I will prefer to leave it as it's right now (since it works, but with such small issues). I guess it will good enough to add test suites which will cover this case sensitivity issue in the future. And fix it.

I will try to do my best and fix it according to ECMA standards and back with result.

Copy link

@andreialecu andreialecu Oct 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just need to bring this back:

https://github.com/facebook/hermes/pull/571/files#diff-bd6622e75c91b32a4ac2f2a7f9ad2dc1e6c9ffa72c71e78070f3fc57c8d4f4e3L382-L397

Screenshot 2021-10-14 at 20 03 01

Instead of using the built-in case insensitive comparison methods.

.filter(new Predicate<String>() {
@Override
public boolean test(String tz) {
return tz.equalsIgnoreCase(timeZone);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment on lines +249 to +250
} catch (JSRangeErrorException error) {
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just remove this try/catch, it doesn't do anything.

@anton-patrushev
Copy link
Author

@mhorowitz Thanks for your review!
I will dig into this and review all required & proposed changes on this weekend. Will be back with answers.
And if someone have enough time and want to help - I will appreciate any kind of your support!

facebook-github-bot pushed a commit that referenced this pull request Nov 12, 2021
Summary:
<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The two fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/hermes) and create your branch from `main`.
  2. If you've fixed a bug or added code that should be tested, add tests!
  3. Ensure it builds and the test suite passes. [tips](https://github.com/facebook/hermes/blob/HEAD/doc/BuildingAndRunning.md)
  4. Format your code with `.../hermes/utils/format.sh`
  5. If you haven't already, complete the CLA.
-->

Please feel free to edit anything as necessary.
Picking up where #571 left off

Error:
![intl_error](https://user-images.githubusercontent.com/30021449/139603461-2ae20e1d-7ead-4fe2-ab9a-da5f647b3d19.png)
Cause: #627 (comment)

_Coauthored with anton-patrushev_ 🎉
_A special thanks to hcwesson_ 🙏

<!--
  Explain the **motivation** for making this change.
  What existing problem does the pull request solve?
-->

Pull Request resolved: #627

Test Plan:
<!--
  Demonstrate the code is solid.
  Example: The exact commands you ran and their output,
  screenshots / videos if the pull request changes the user interface.
-->

Followed testing instructions provided by mhorowitz #571 (review)

![image](https://user-images.githubusercontent.com/30021449/139600222-316bb2e1-f718-4d15-b02e-281374a26eac.png)

Reviewed By: kodafb

Differential Revision: D32240632

Pulled By: neildhar

fbshipit-source-id: d9582d5908b22addb31516834a58649182da5c64
@anton-patrushev
Copy link
Author

Closed that, since #627 had been merged into main.
Thanks to @calebmackdavenport !!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Do not delete this pull request or issue due to inactivity.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants