Skip to content

Conversation

@sun
Copy link
Collaborator

@sun sun commented Jul 24, 2025

Problem

  • With WP_DEBUG enabled, WordPress logs the following message for every request/bootstrap:

Notice: Function _load_textdomain_just_in_time was called incorrectly. Translation loading for the jwt-auth domain was triggered too early. This is usually an indicator for some code in the plugin or theme running too early. Translations should be loaded at the init action or later. Please see Debugging in WordPress for more information. (This message was added in version 6.7.0.) in /wp-includes/functions.php on line 6121

Cause

  • It wasn't obvious what triggered it, because the plugin loads the gettext domain normally in the init hook.
  • Only when forcibly throwing an exception in wp_trigger_error() and checking the stack trace, the cause becomes clear — the plugin tries to translate some strings too early, directly when the code is loaded, before the gettext domain is even registered:
Stack trace:
#0 /wp-includes/functions.php(6061): wp_trigger_error('', 'Function _load_...')
#1 /wp-includes/l10n.php(1371): _doing_it_wrong('_load_textdomai...', 'Function _load_...', '(This message w...')
#2 /wp-includes/l10n.php(1409): _load_textdomain_just_in_time('jwt-auth')
#3 /wp-includes/l10n.php(195): get_translations_for_domain('jwt-auth')
#4 /wp-includes/l10n.php(307): translate('Authorization h...', 'jwt-auth')
#5 /wp-content/plugins/jwt-auth/class-auth.php(59): __('Authorization h...', 'jwt-auth')  <-- HERE
#6 /wp-content/plugins/jwt-auth/class-setup.php(36): JWTAuth\Auth->__construct()
#7 /wp-content/plugins/jwt-auth/class-setup.php(25): JWTAuth\Setup->__construct()
#8 /wp-content/plugins/jwt-auth/jwt-auth.php(32): JWTAuth\Setup::getInstance()
#9 /wp-settings.php(545): include_once('/site...')
#10 phar:///bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1375): require('/site...')
#11 phar:///bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1294): WP_CLI\Runner->load_wordpress()
#12 phar:///bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start()
#13 phar:///bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(83): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState))
#14 phar:///bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(32): WP_CLI\bootstrap()
#15 phar:///bin/wp/php/boot-phar.php(20): include('phar:///Users/s...')
#16 /bin/wp(4): include('phar:///Users/s...')
#17 {main}

Proposed solution

  1. The early translation and collection of translatable strings is completely unnecessary and can be dissolved.

    In fact, fixing this is even a micro performance optimization, because

    • multiple strings were translated whereas only one will be needed
    • none of the strings nor their translations will be needed if there is no error

Compatibility

  • The $messages property was private, so not accessible from elsewhere; not even child classes.

@sun sun requested a review from dominic-ks July 24, 2025 11:27
@sun sun added the bug Something isn't working label Jul 24, 2025
sun added a commit to makers99/wp-cli-shared-patches that referenced this pull request Jul 24, 2025
@dominic-ks dominic-ks assigned dominic-ks and unassigned dominic-ks Jul 24, 2025
Copy link
Collaborator

@dominic-ks dominic-ks left a comment

Choose a reason for hiding this comment

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

Yep makes sense.

@sun sun merged commit 4b4ca48 into usefulteam:master Jul 24, 2025
@sun
Copy link
Collaborator Author

sun commented Jul 24, 2025

I did not add a changelog entry before merging. But I believe that should be generated from the commit log when creating a release? 🤔 — let's cover this in #134 (comment)

@dominic-ks dominic-ks mentioned this pull request Jul 25, 2025
@sun
Copy link
Collaborator Author

sun commented Aug 1, 2025

Unfortunately this is causing an infinite recursion, but only under limited circumstances; e.g., when accessing WooCommerce Statistics in the admin backend on /wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&period=month&compare=previous_period. 🤔

I'm still debugging why this is only happening in certain situations, but for now here is the relevant infinite recursion part of stack trace:

#53938 /wp-content/plugins/jwt-auth/class-auth.php(370): __('Authorization h...', 'jwt-auth')
#53939 /wp-content/plugins/jwt-auth/class-auth.php(646): JWTAuth\Auth->validate_token(false)
#53940 /wp-includes/class-wp-hook.php(324): JWTAuth\Auth->determine_current_user(false)
#53941 /wp-includes/plugin.php(205): WP_Hook->apply_filters(false, Array)
#53942 /wp-includes/user.php(3753): apply_filters('determine_curre...', false)
#53943 /wp-includes/pluggable.php(70): _wp_get_current_user()
#53944 /wp-includes/l10n.php(98): wp_get_current_user()
#53945 /wp-includes/l10n.php(153): get_user_locale()
#53946 /wp-includes/l10n.php(1364): determine_locale()
#53947 /wp-includes/l10n.php(1409): _load_textdomain_just_in_time('jwt-auth')
#53948 /wp-includes/l10n.php(195): get_translations_for_domain('jwt-auth')
#53949 /wp-includes/l10n.php(307): translate('Authorization h...', 'jwt-auth')
#53950 /wp-content/plugins/jwt-auth/class-auth.php(370): __('Authorization h...', 'jwt-auth')
#53951 /wp-content/plugins/jwt-auth/class-auth.php(646): JWTAuth\Auth->validate_token(false)
#53952 /wp-includes/class-wp-hook.php(324): JWTAuth\Auth->determine_current_user(false)
#53953 /wp-includes/plugin.php(205): WP_Hook->apply_filters(false, Array)
#53954 /wp-includes/user.php(3753): apply_filters('determine_curre...', false)
#53955 /wp-includes/pluggable.php(70): _wp_get_current_user()
#53956 /wp-includes/l10n.php(98): wp_get_current_user()
#53957 /wp-includes/l10n.php(153): get_user_locale()
#53958 /wp-includes/l10n.php(1364): determine_locale()
#53959 /wp-includes/l10n.php(1409): _load_textdomain_just_in_time('jwt-auth')
#53960 /wp-includes/l10n.php(195): get_translations_for_domain('jwt-auth')
#53961 /wp-includes/l10n.php(307): translate('Authorization h...', 'jwt-auth')
#53962 /wp-content/plugins/jwt-auth/class-auth.php(370): __('Authorization h...', 'jwt-auth')
#53963 /wp-content/plugins/jwt-auth/class-auth.php(646): JWTAuth\Auth->validate_token(false)
#53964 /wp-includes/class-wp-hook.php(324): JWTAuth\Auth->determine_current_user(false)
#53965 /wp-includes/plugin.php(205): WP_Hook->apply_filters(false, Array)
#53966 /wp-includes/user.php(3753): apply_filters('determine_curre...', false)
#53967 /wp-includes/pluggable.php(70): _wp_get_current_user()
#53968 /wp-includes/capabilities.php(911): wp_get_current_user()
#53969 /wp-content/plugins/enable-media-replace/classes/emr-plugin.php(46): current_user_can('upload_files')
#53970 /wp-includes/class-wp-hook.php(324): EnableMediaReplace\EnableMediaReplacePlugin->runtime('')
#53971 /wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters(NULL, Array)
#53972 /wp-includes/plugin.php(517): WP_Hook->do_action(Array)
#53973 /wp-settings.php(578): do_action('plugins_loaded')
#53974 /wp-config.php(76): require_once('...')
#53975 /wp-load.php(50): require_once('...')
#53976 /wp-blog-header.php(13): require_once('...')
#53977 /index.php(17): require('...')

In this case, the entry point seems to be in the enable-media-replace plugin, which might have problematic code on its own (indirectly triggering initialization of authentication in plugins_loaded already) – but regardless of that, the infinite recursion between jwt-auth and l10n.php must not happen.

What is happening:

  • The string translation was invoked too early previously, so the strings were not actually translated.
  • With the new code, the gettext translations are actually getting invoked, and it seems the gettext translation system is trying to determine the locale, also checking the user's locale, which in turn invokes determine_current_user and thus jwt-auth again.

A quick fix would be to just remove the string translations altogether - the strings were not translated previously either.

A more sophisticated fix would probably involve checking whether a certain hook was invoked already (e.g., 'init'?) to only translate the strings if that is the case.

@sun
Copy link
Collaborator Author

sun commented Aug 1, 2025

Based on https://developer.wordpress.org/apis/hooks/action-reference/, only invoking our validate_token() if did_action('load_textdomain') should resolve it, but the recursion is still triggered because the textdomain gets loaded:

#0 /wp-includes/class-wp-hook.php(324): wp_validate_auth_cookie(false)
#1 /wp-includes/plugin.php(205): WP_Hook->apply_filters(false, Array)
#2 /wp-includes/user.php(3753): apply_filters('determine_curre...', false)
#3 /wp-includes/pluggable.php(70): _wp_get_current_user()
#4 /wp-includes/l10n.php(98): wp_get_current_user()
#5 /wp-includes/l10n.php(153): get_user_locale()
#6 /wp-includes/l10n.php(1364): determine_locale()
#7 /wp-includes/l10n.php(1409): _load_textdomain_just_in_time('jwt-auth')
#8 /wp-includes/l10n.php(195): get_translations_for_domain('jwt-auth')
#9 /wp-includes/l10n.php(307): translate('Authorization h...', 'jwt-auth')
#10 /wp-content/plugins/jwt-auth/class-auth.php(370): __('Authorization h...', 'jwt-auth')
#11 /wp-content/plugins/jwt-auth/class-auth.php(650): JWTAuth\Auth->validate_token(false)
#12 /wp-includes/class-wp-hook.php(324): JWTAuth\Auth->determine_current_user(false)
#13 /wp-includes/plugin.php(205): WP_Hook->apply_filters(false, Array)
#14 /wp-includes/user.php(3753): apply_filters('determine_curre...', false)
#15 /wp-includes/pluggable.php(70): _wp_get_current_user()
#16 /wp-includes/capabilities.php(911): wp_get_current_user()
#17 /wp-content/plugins/enable-media-replace/classes/emr-plugin.php(46): current_user_can('upload_files')
#18 /wp-includes/class-wp-hook.php(324): EnableMediaReplace\EnableMediaReplacePlugin->runtime('')
#19 /wp-includes/class-wp-hook.php(348): WP_Hook->apply_filters(NULL, Array)
#20 /wp-includes/plugin.php(517): WP_Hook->do_action(Array)
#21 /wp-settings.php(578): do_action('plugins_loaded')
#22 /wp-config.php(76): require_once('/Users/sun/site...')
#23 /wp-load.php(50): require_once('/Users/sun/site...')
#24 /wp-blog-header.php(13): require_once('/Users/sun/site...')
#25 /index.php(17): require('/Users/sun/site...')

The scenario indeed seems to be a special case that is only happening on certain REST routes in wp-admin:

determine_locale() is specifically checking for is_admin() and a URL query parameter ?_locale=user, which is triggering the call to get_user_locale().

Logging the stack trace in our determine_current_user callback shows that there are several invocations and code paths from other plugins, too. The other invocations are not running into the infinite recursion, but the only reason for that is that get_user_locale() is not getting invoked. That is fairly fragile. It could happen through many other means.

This is extremely tough to resolve — it essentially means we can only pursue one of these possible paths:

  • A) Do not translate any strings in validate_token() and child functions.
  • B) Only translate strings if _locale=user is not passed, otherwise output English strings. (hoping that there are no other cases)
  • C) Prevent the specific recursion from get_user_locale().

@sun
Copy link
Collaborator Author

sun commented Aug 1, 2025

Also created an issue for enable-media-replace calling current_user_can() too early: https://wordpress.org/support/topic/enable-media-replace-plugin-checks-user-access-too-early/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants