Repository: oppia/oppia

Issue:
Platform features have a robustness issue in production
**Describe the bug**
In the 3.0.4 release we suddenly started seeing an invalid user locale error. See this line for where it's raised:

https://github.com/oppia/oppia/blob/1a57e851b621ddaee8ef1b73ac4c52fa4b0e8a2b/core/domain/platform_parameter_domain.py#L161

**To Reproduce**
Repro steps are unknown since this was an observed in-production issue. However, navigating to www.oppia.org/platform_features_evaluation_handler?client_type=Web will repro this issue (you can also navigate to this link with a local dev server, e.g. http://localhost:8181/platform_features_evaluation_handler?client_type=Web should also trigger the 400 error).

**Observed behavior**
N/A

**Expected behavior**
We should gracefully fail here. Per code, it seems like this can happen when the client doesn't have a user_locale defined. While that itself is potentially problematic, this is a mission critical system that needs to be robust even against unexpected failures. See the following code that assembles the context dictionary:

https://github.com/oppia/oppia/blob/1a57e851b621ddaee8ef1b73ac4c52fa4b0e8a2b/core/controllers/platform_feature.py#L37

We ought to have resilience against the 'None' cases. In general, if we don't have a property that's required to evaluate a platform parameter, we must assume the default value for that parameter (we can't reasonably partially evaluate it).

**Screenshots**
N/A

**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
URL: www.oppia.org/platform_features_evaluation_handler?client_type=Web

User agent: ``Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0``

Platform features have a robustness issue in production
**Describe the bug**
In the 3.0.4 release we suddenly started seeing an invalid user locale error. See this line for where it's raised:

https://github.com/oppia/oppia/blob/1a57e851b621ddaee8ef1b73ac4c52fa4b0e8a2b/core/domain/platform_parameter_domain.py#L161

**To Reproduce**
Repro steps are unknown since this was an observed in-production issue. However, navigating to www.oppia.org/platform_features_evaluation_handler?client_type=Web will repro this issue (you can also navigate to this link with a local dev server, e.g. http://localhost:8181/platform_features_evaluation_handler?client_type=Web should also trigger the 400 error).

**Observed behavior**
N/A

**Expected behavior**
We should gracefully fail here. Per code, it seems like this can happen when the client doesn't have a user_locale defined. While that itself is potentially problematic, this is a mission critical system that needs to be robust even against unexpected failures. See the following code that assembles the context dictionary:

https://github.com/oppia/oppia/blob/1a57e851b621ddaee8ef1b73ac4c52fa4b0e8a2b/core/controllers/platform_feature.py#L37

We ought to have resilience against the 'None' cases. In general, if we don't have a property that's required to evaluate a platform parameter, we must assume the default value for that parameter (we can't reasonably partially evaluate it).

**Screenshots**
N/A

**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
URL: www.oppia.org/platform_features_evaluation_handler?client_type=Web

User agent: ``Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0``



Discussion:
/cc @MegrezZhu 
/cc @MegrezZhu 

Resolving patch:
diff --git a/core/domain/platform_feature_services.py b/core/domain/platform_feature_services.py
--- a/core/domain/platform_feature_services.py
+++ b/core/domain/platform_feature_services.py
@@ -33,10 +33,15 @@
 from __future__ import absolute_import  # pylint: disable=import-only-modules
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
+
 from constants import constants
 from core import platform_feature_list
 from core.domain import platform_parameter_domain
 from core.domain import platform_parameter_registry as registry
+from core.domain import user_services
+from core.platform import models
+
+current_user_services = models.Registry.import_current_user_services()
 
 
 ALL_FEATURES_LIST = (
@@ -111,7 +116,9 @@ def is_feature_enabled(feature_name):
     Returns:
         bool. The value of the feature flag, True if it's enabled.
     """
-    return _evaluate_feature_flag_value_for_server(feature_name)
+    user_settings = user_services.get_user_settings_by_gae_id(
+        current_user_services.get_current_gae_id())
+    return _evaluate_feature_flag_value_for_server(feature_name, user_settings)
 
 
 def update_feature_flag_rules(
@@ -153,18 +160,30 @@ def _get_server_mode():
         if constants.DEV_MODE else platform_parameter_domain.SERVER_MODES.prod)
 
 
-def _create_evaluation_context_for_server():
+def _create_evaluation_context_for_server(user_settings):
     """Returns evaluation context with information of the server.
 
+    Args:
+        user_settings: UserSettings|None. The settings of the current logged in
+            user, if any.
+
     Returns:
         EvaluationContext. The context for evaluation.
     """
+    language_code = 'en' # Default language code if no preference is set.
+    if (
+            user_settings is not None and
+            user_settings.preferred_site_language_code is not None):
+        language_code = user_settings.preferred_site_language_code
+
+    # TODO(#11073): Properly set app version below, and rename the user_locale
+    # to correspond to a selected language code. Consider also renaming
+    # client_type to something that better encapsulates frontends and backends.
     return platform_parameter_domain.EvaluationContext.from_dict(
         {
-            'client_type': None,
-            'browser_type': None,
+            'client_type': 'Backend',
             'app_version': None,
-            'user_locale': None,
+            'user_locale': language_code,
         },
         {
             'server_mode': _get_server_mode()
@@ -201,18 +220,20 @@ def _evaluate_feature_flag_values_for_context(feature_names_set, context):
     return result_dict
 
 
-def _evaluate_feature_flag_value_for_server(feature_name):
+def _evaluate_feature_flag_value_for_server(feature_name, user_settings):
     """Evaluates and returns the values of the feature flag, using context
     from the server only.
 
     Args:
         feature_name: str. The name of the feature flag that needs to
             be evaluated.
+        user_settings: UserSettings|None. The settings of the current logged in
+            user, if any.
 
     Returns:
         bool. The value of the feature flag, True if it's enabled.
     """
-    context = _create_evaluation_context_for_server()
+    context = _create_evaluation_context_for_server(user_settings)
     values_dict = _evaluate_feature_flag_values_for_context(
         set([feature_name]), context)
     return values_dict[feature_name]
diff --git a/core/domain/platform_parameter_domain.py b/core/domain/platform_parameter_domain.py
--- a/core/domain/platform_parameter_domain.py
+++ b/core/domain/platform_parameter_domain.py
@@ -128,13 +128,29 @@ def server_mode(self):
         """
         return self._server_mode
 
-    def validate(self):
-        """Validates the EvaluationContext domain object."""
-        if self._client_type not in ALLOWED_CLIENT_TYPES:
-            raise utils.ValidationError(
-                'Invalid client type \'%s\', must be one of %s.' % (
-                    self._client_type, ALLOWED_CLIENT_TYPES))
+    @property
+    def is_valid(self):
+        """Returns whether this context object is valid for evaluating
+        parameters. An invalid context object usually indicates that one of the
+        object's required fields is missing or an unexpected value. Note that
+        objects which are not valid will still pass validation. This method
+        should return true and validate() should not raise an exception before
+        using this object for platform evaluation.
+
+        Returns:
+            bool. Whether this context object can be used for evaluating
+            parameters.
+        """
+        return (
+            self._client_type is not None and
+            self._client_type in ALLOWED_CLIENT_TYPES and
+            self._user_locale is not None and
+            self._user_locale in ALLOWED_USER_LOCALES)
 
+    def validate(self):
+        """Validates the EvaluationContext domain object, raising an exception
+        if the object is in an irrecoverable error state.
+        """
         if (
                 self._browser_type is not None and
                 self._browser_type not in ALLOWED_BROWSER_TYPES):
@@ -156,11 +172,6 @@ def validate(self):
                     ' specified.' % (
                         match.group(2), ALLOWED_APP_VERSION_FLAVORS))
 
-        if self._user_locale not in ALLOWED_USER_LOCALES:
-            raise utils.ValidationError(
-                'Invalid user locale \'%s\', must be one of %s.' % (
-                    self._user_locale, ALLOWED_USER_LOCALES))
-
         if self._server_mode not in ALLOWED_SERVER_MODES:
             raise utils.ValidationError(
                 'Invalid server mode \'%s\', must be one of %s.' % (
@@ -693,15 +704,21 @@ def evaluate(self, context):
         """Evaluates the value of the platform parameter in the given context.
         The value of first matched rule is returned as the result.
 
+        Note that if the provided context is in an invalid state (e.g. its
+        is_valid property returns false) then this parameter will defer to its
+        default value since it may not be safe to partially evaluate the
+        parameter for an unrecognized or partially recognized context.
+
         Args:
             context: EvaluationContext. The context for evaluation.
 
         Returns:
             *. The evaluate result of the platform parameter.
         """
-        for rule in self._rules:
-            if rule.evaluate(context):
-                return rule.value_when_matched
+        if context.is_valid:
+            for rule in self._rules:
+                if rule.evaluate(context):
+                    return rule.value_when_matched
         return self._default_value
 
     def to_dict(self):


Test patch:
diff --git a/core/controllers/platform_feature_test.py b/core/controllers/platform_feature_test.py
--- a/core/controllers/platform_feature_test.py
+++ b/core/controllers/platform_feature_test.py
@@ -89,18 +89,71 @@ def test_get_dev_mode_android_client_returns_correct_flag_values(self):
                 result,
                 {self.dev_feature.name: False, self.prod_feature.name: True})
 
-    def test_get_with_invalid_client_type_context_raises_400(self):
-        resp_dict = self.get_json(
-            '/platform_features_evaluation_handler',
-            params={
-                'client_type': 'Invalid',
-                'app_version': '1.0.0',
-                'user_locale': 'en',
-            },
-            expected_status_int=400
-        )
-        self.assertEqual(
-            resp_dict['error'],
-            'Invalid client type \'Invalid\', must be one of [u\'Web\', '
-            'u\'Android\'].'
-        )
+    def test_get_features_invalid_client_type_returns_features_disabled(self):
+        with self.swap(constants, 'DEV_MODE', True):
+            result = self.get_json(
+                '/platform_features_evaluation_handler',
+                params={
+                    'client_type': 'invalid',
+                    'user_locale': 'en',
+                },
+                expected_status_int=200
+            )
+            self.assertEqual(
+                result,
+                {self.dev_feature.name: False, self.prod_feature.name: False})
+
+    def test_get_features_missing_client_type_returns_features_disabled(self):
+        with self.swap(constants, 'DEV_MODE', True):
+            result = self.get_json(
+                '/platform_features_evaluation_handler',
+                params={
+                    'user_locale': 'en',
+                }
+            )
+            self.assertEqual(
+                result,
+                {self.dev_feature.name: False, self.prod_feature.name: False})
+
+    def test_get_features_invalid_user_locale_returns_features_disabled(self):
+        with self.swap(constants, 'DEV_MODE', True):
+            result = self.get_json(
+                '/platform_features_evaluation_handler',
+                params={
+                    'client_type': 'Android',
+                    'user_locale': 'invalid',
+                },
+                expected_status_int=200
+            )
+            self.assertEqual(
+                result,
+                {self.dev_feature.name: False, self.prod_feature.name: False})
+
+    def test_get_features_missing_user_locale_returns_features_disabled(self):
+        with self.swap(constants, 'DEV_MODE', True):
+            result = self.get_json(
+                '/platform_features_evaluation_handler',
+                params={
+                    'client_type': 'Android',
+                }
+            )
+            self.assertEqual(
+                result,
+                {self.dev_feature.name: False, self.prod_feature.name: False})
+
+    def test_get_features_invalid_version_flavor_raises_400(self):
+        with self.swap(constants, 'DEV_MODE', True):
+            resp_dict = self.get_json(
+                '/platform_features_evaluation_handler',
+                params={
+                    'client_type': 'Android',
+                    'app_version': '1.0.0-abcdefg-invalid',
+                    'user_locale': 'en',
+                },
+                expected_status_int=400
+            )
+            self.assertEqual(
+                resp_dict['error'],
+                'Invalid version flavor \'invalid\', must be one of '
+                '[u\'test\', u\'alpha\', u\'beta\', u\'release\'] if specified.'
+            )
diff --git a/core/domain/platform_feature_services_test.py b/core/domain/platform_feature_services_test.py
--- a/core/domain/platform_feature_services_test.py
+++ b/core/domain/platform_feature_services_test.py
@@ -24,6 +24,7 @@
 from core.domain import platform_feature_services as feature_services
 from core.domain import platform_parameter_domain
 from core.domain import platform_parameter_registry as registry
+from core.domain import user_services
 from core.tests import test_utils
 import utils
 
@@ -153,8 +154,7 @@ def test_get_all_feature_flag_values_in_dev_returns_correct_values(self):
                 {
                     self.dev_feature.name: True,
                     self.prod_feature.name: True,
-                }
-            )
+                })
 
     def test_get_all_feature_flag_values_in_prod_returns_correct_values(self):
         with self.swap(constants, 'DEV_MODE', False):
@@ -170,8 +170,7 @@ def test_get_all_feature_flag_values_in_prod_returns_correct_values(self):
                 {
                     self.dev_feature.name: False,
                     self.prod_feature.name: True,
-                }
-            )
+                })
 
     def test_evaluate_dev_feature_for_dev_server_returns_true(self):
         with self.swap(constants, 'DEV_MODE', True):
@@ -194,6 +193,149 @@ def test_evaluate_prod_feature_for_prod_server_returns_true(
             self.assertTrue(
                 feature_services.is_feature_enabled(self.prod_feature.name))
 
+    def test_evaluate_feature_for_prod_server_matches_to_backend_filter(
+            self):
+        registry.Registry.update_platform_parameter(
+            self.prod_feature.name, self.user_id, 'edit rules',
+            [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [
+                                [
+                                    '=',
+                                    platform_parameter_domain.SERVER_MODES.prod
+                                ]
+                            ],
+                        },
+                        {
+                            'type': 'client_type',
+                            'conditions': [
+                                [
+                                    '=',
+                                    'Backend'
+                                ]
+                            ],
+                        }
+                    ],
+                    'value_when_matched': True
+                }
+            ]
+        )
+        with self.swap(constants, 'DEV_MODE', False):
+            self.assertTrue(
+                feature_services.is_feature_enabled(self.prod_feature.name))
+
+    def test_evaluate_feature_for_prod_server_no_user_defaults_match_to_en_lang(
+            self):
+        registry.Registry.update_platform_parameter(
+            self.prod_feature.name, self.user_id, 'edit rules',
+            [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [
+                                [
+                                    '=',
+                                    platform_parameter_domain.SERVER_MODES.prod
+                                ]
+                            ],
+                        },
+                        {
+                            'type': 'user_locale',
+                            'conditions': [
+                                [
+                                    '=',
+                                    'en'
+                                ]
+                            ],
+                        }
+                    ],
+                    'value_when_matched': True
+                }
+            ]
+        )
+        with self.swap(constants, 'DEV_MODE', False):
+            self.assertTrue(
+                feature_services.is_feature_enabled(self.prod_feature.name))
+
+    def test_evaluate_feature_for_en_user_defaults_match_to_en_lang(
+            self):
+        user_services.update_preferred_site_language_code(self.user_id, 'en')
+        self.login(self.OWNER_EMAIL)
+        registry.Registry.update_platform_parameter(
+            self.prod_feature.name, self.user_id, 'edit rules',
+            [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [
+                                [
+                                    '=',
+                                    platform_parameter_domain.SERVER_MODES.prod
+                                ]
+                            ],
+                        },
+                        {
+                            'type': 'user_locale',
+                            'conditions': [
+                                [
+                                    '=',
+                                    'en'
+                                ]
+                            ],
+                        }
+                    ],
+                    'value_when_matched': True
+                }
+            ]
+        )
+        with self.swap(constants, 'DEV_MODE', False):
+            self.assertTrue(
+                feature_services.is_feature_enabled(self.prod_feature.name))
+
+    def test_evaluate_feature_for_fr_user_defaults_does_not_match_to_en_lang(
+            self):
+        user_services.update_preferred_site_language_code(self.user_id, 'fr')
+        self.login(self.OWNER_EMAIL)
+        registry.Registry.update_platform_parameter(
+            self.prod_feature.name, self.user_id, 'edit rules',
+            [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [
+                                [
+                                    '=',
+                                    platform_parameter_domain.SERVER_MODES.prod
+                                ]
+                            ],
+                        },
+                        {
+                            'type': 'user_locale',
+                            'conditions': [
+                                [
+                                    '=',
+                                    'en'
+                                ]
+                            ],
+                        }
+                    ],
+                    'value_when_matched': True
+                }
+            ]
+        )
+        # Since the feature is only enable for users who have French selected as
+        # their preferred site language code, the feature shouldn't be enabled
+        # for the current logged in user.
+        with self.swap(constants, 'DEV_MODE', False):
+            self.assertFalse(
+                feature_services.is_feature_enabled(self.prod_feature.name))
+
     def test_get_feature_flag_values_with_unknown_name_raises_error(self):
         with self.assertRaisesRegexp(
             Exception, 'Unknown feature flag'):
diff --git a/core/domain/platform_parameter_domain_test.py b/core/domain/platform_parameter_domain_test.py
--- a/core/domain/platform_parameter_domain_test.py
+++ b/core/domain/platform_parameter_domain_test.py
@@ -105,6 +105,74 @@ def test_create_context_from_dict_returns_correct_instance(self):
         self.assertEqual(context.user_locale, 'en')
         self.assertEqual(context.server_mode, 'dev')
 
+    def test_is_valid_with_invalid_client_type_returns_false(self):
+        context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'invalid',
+                'browser_type': None,
+                'app_version': '1.0.0',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertFalse(context.is_valid)
+
+    def test_is_valid_with_invalid_user_locale_returns_false(self):
+        context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Android',
+                'browser_type': None,
+                'app_version': '1.0.0',
+                'user_locale': 'invalid',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertFalse(context.is_valid)
+
+    def test_is_valid_with_valid_android_context_returns_true(self):
+        context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Android',
+                'browser_type': None,
+                'app_version': '1.0.0',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertTrue(context.is_valid)
+
+    def test_is_valid_with_valid_web_context_returns_true(self):
+        context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Web',
+                'browser_type': 'Chrome',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertTrue(context.is_valid)
+
+    def test_is_valid_with_valid_backend_context_returns_true(self):
+        context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Backend',
+                'app_version': '3.0.0',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertTrue(context.is_valid)
+
     def test_validate_with_valid_context_passes_without_exception(self):
         context = parameter_domain.EvaluationContext.from_dict(
             {
@@ -119,7 +187,7 @@ def test_validate_with_valid_context_passes_without_exception(self):
         )
         context.validate()
 
-    def test_validate_with_invalid_client_type_raises_exception(self):
+    def test_validate_with_invalid_client_type_does_not_raise_exception(self):
         context = parameter_domain.EvaluationContext.from_dict(
             {
                 'client_type': 'invalid',
@@ -131,9 +199,8 @@ def test_validate_with_invalid_client_type_raises_exception(self):
                 'server_mode': 'dev',
             },
         )
-        with self.assertRaisesRegexp(
-            utils.ValidationError, 'Invalid client type \'invalid\''):
-            context.validate()
+        # No exception should be raised since invalid client types are ignored.
+        context.validate()
 
     def test_validate_with_invalid_browser_type_raises_exception(self):
         context = parameter_domain.EvaluationContext.from_dict(
@@ -200,7 +267,7 @@ def test_validate_with_invalid_app_version_flavor_raises_exception(self):
             utils.ValidationError, 'Invalid version flavor \'invalid\''):
             context.validate()
 
-    def test_validate_with_invalid_user_locale_raises_exception(self):
+    def test_validate_with_invalid_user_locale_does_not_raise_exception(self):
         context = parameter_domain.EvaluationContext.from_dict(
             {
                 'client_type': 'Android',
@@ -212,9 +279,8 @@ def test_validate_with_invalid_user_locale_raises_exception(self):
                 'server_mode': 'dev',
             },
         )
-        with self.assertRaisesRegexp(
-            utils.ValidationError, 'Invalid user locale \'invalid\''):
-            context.validate()
+        # No exception should be raised since invalid user locales are ignored.
+        context.validate()
 
     def test_validate_with_invalid_server_mode_raises_exception(self):
         context = parameter_domain.EvaluationContext.from_dict(
@@ -335,6 +401,15 @@ def test_evaluate_en_user_locale_filter_with_zh_locale_returns_false(self):
         zh_context = self._create_example_context(user_locale='zh-hans')
         self.assertFalse(filter_domain.evaluate(zh_context))
 
+    def test_eval_backend_client_filter_with_backend_client_returns_true(self):
+        filter_dict = {'type': 'client_type', 'conditions': [('=', 'Backend')]}
+        filter_domain = (
+            parameter_domain
+            .PlatformParameterFilter.from_dict(filter_dict))
+
+        web_context = self._create_example_context(client_type='Backend')
+        self.assertTrue(filter_domain.evaluate(web_context))
+
     def test_evaluate_web_client_filter_with_web_client_returns_true(self):
         filter_dict = {'type': 'client_type', 'conditions': [('=', 'Web')]}
         filter_domain = (
@@ -1632,6 +1707,148 @@ def test_evaluate_without_matched_rule_returns_default_value(self):
         )
         self.assertEqual(parameter.evaluate(prod_context), '111')
 
+    def test_evaluate_matching_feature_invalid_client_type_returns_def(self):
+        parameter = parameter_domain.PlatformParameter.from_dict({
+            'name': 'parameter_a',
+            'description': 'for test',
+            'data_type': 'string',
+            'rules': [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [('=', 'dev')]
+                        }
+                    ],
+                    'value_when_matched': '222'
+                }
+            ],
+            'rule_schema_version': (
+                feconf.CURRENT_PLATFORM_PARAMETER_RULE_SCHEMA_VERSION),
+            'default_value': '111',
+            'is_feature': False,
+            'feature_stage': None,
+        })
+
+        dev_context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'invalid',
+                'browser_type': None,
+                'app_version': '1.2.3',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertEqual(parameter.evaluate(dev_context), '111')
+
+    def test_evaluate_matching_feature_missing_client_type_returns_def(self):
+        parameter = parameter_domain.PlatformParameter.from_dict({
+            'name': 'parameter_a',
+            'description': 'for test',
+            'data_type': 'string',
+            'rules': [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [('=', 'dev')]
+                        }
+                    ],
+                    'value_when_matched': '222'
+                }
+            ],
+            'rule_schema_version': (
+                feconf.CURRENT_PLATFORM_PARAMETER_RULE_SCHEMA_VERSION),
+            'default_value': '111',
+            'is_feature': False,
+            'feature_stage': None,
+        })
+
+        dev_context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'browser_type': None,
+                'app_version': '1.2.3',
+                'user_locale': 'en',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertEqual(parameter.evaluate(dev_context), '111')
+
+    def test_evaluate_matching_feature_invalid_user_locale_returns_def(self):
+        parameter = parameter_domain.PlatformParameter.from_dict({
+            'name': 'parameter_a',
+            'description': 'for test',
+            'data_type': 'string',
+            'rules': [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [('=', 'dev')]
+                        }
+                    ],
+                    'value_when_matched': '222'
+                }
+            ],
+            'rule_schema_version': (
+                feconf.CURRENT_PLATFORM_PARAMETER_RULE_SCHEMA_VERSION),
+            'default_value': '111',
+            'is_feature': False,
+            'feature_stage': None,
+        })
+
+        dev_context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Android',
+                'browser_type': None,
+                'app_version': '1.2.3',
+                'user_locale': 'invalid',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertEqual(parameter.evaluate(dev_context), '111')
+
+    def test_evaluate_matching_feature_missing_user_locale_returns_def(self):
+        parameter = parameter_domain.PlatformParameter.from_dict({
+            'name': 'parameter_a',
+            'description': 'for test',
+            'data_type': 'string',
+            'rules': [
+                {
+                    'filters': [
+                        {
+                            'type': 'server_mode',
+                            'conditions': [('=', 'dev')]
+                        }
+                    ],
+                    'value_when_matched': '222'
+                }
+            ],
+            'rule_schema_version': (
+                feconf.CURRENT_PLATFORM_PARAMETER_RULE_SCHEMA_VERSION),
+            'default_value': '111',
+            'is_feature': False,
+            'feature_stage': None,
+        })
+
+        dev_context = parameter_domain.EvaluationContext.from_dict(
+            {
+                'client_type': 'Android',
+                'browser_type': None,
+                'app_version': '1.2.3',
+            },
+            {
+                'server_mode': 'dev',
+            },
+        )
+        self.assertEqual(parameter.evaluate(dev_context), '111')
+
     def test_validate_feature_passes_without_exception(self):
         parameter = parameter_domain.PlatformParameter.from_dict({
             'name': 'parameter_a',