From aa3434c4aaf00383dcfe26fbb499d63e1b260aa7 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Thu, 30 Dec 2021 11:16:28 +0000
Subject: [PATCH] Issue #3049048 by danflanagan8, ndobromirov, mglaman, bbrala,
 alexpott, Wim Leers, gabesullice: Invalid JSON:API responses when maintenance
 mode is on

---
 core/core.services.yml                        |  4 +-
 .../MaintenanceModeSubscriber.php             | 64 ++++++++-----
 core/lib/Drupal/Core/Site/MaintenanceMode.php | 27 +++++-
 .../Core/Site/MaintenanceModeEvents.php       | 15 ++++
 .../Core/Site/MaintenanceModeInterface.php    |  8 ++
 .../config/install/jsonapi.settings.yml       |  3 +
 .../jsonapi/config/schema/jsonapi.schema.yml  | 10 +++
 core/modules/jsonapi/jsonapi.install          | 14 +++
 core/modules/jsonapi/jsonapi.services.yml     |  5 ++
 .../JsonapiMaintenanceModeSubscriber.php      | 90 +++++++++++++++++++
 .../jsonapi/tests/fixtures/update/jsonapi.php | 54 +++++++++++
 .../fixtures/update/jsonapi.settings.yml      |  2 +
 .../src/Functional/JsonApiFunctionalTest.php  | 33 +++++++
 .../Update/JsonApiUpdatePathTest.php          | 50 +++++++++++
 .../MaintenanceModeSubscriber.php             | 30 ++++++-
 15 files changed, 380 insertions(+), 29 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Site/MaintenanceModeEvents.php
 create mode 100644 core/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php
 create mode 100644 core/modules/jsonapi/tests/fixtures/update/jsonapi.php
 create mode 100644 core/modules/jsonapi/tests/fixtures/update/jsonapi.settings.yml
 create mode 100644 core/modules/jsonapi/tests/src/Functional/Update/JsonApiUpdatePathTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 0ddf970ed43f..f0c703651547 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1229,10 +1229,10 @@ services:
       - { name: access_check, needs_incoming_request: TRUE }
   maintenance_mode:
     class: Drupal\Core\Site\MaintenanceMode
-    arguments: ['@state']
+    arguments: ['@state', '@config.factory']
   maintenance_mode_subscriber:
     class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber
-    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger']
+    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger', '@event_dispatcher']
     tags:
       - { name: event_subscriber }
   route_access_response_subscriber:
diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
index 579d07d4380f..ee2129d7fced 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Core\EventSubscriber;
 
-use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Render\BareHtmlPageRendererInterface;
 use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Render\BareHtmlPageRendererInterface;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Site\MaintenanceModeEvents;
 use Drupal\Core\Site\MaintenanceModeInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
@@ -16,6 +16,7 @@
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\RequestEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Maintenance mode subscriber for controller requests.
@@ -66,6 +67,13 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
    */
   protected $messenger;
 
+  /**
+   * An event dispatcher instance to use for configuration events.
+   *
+   * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
   /**
    * Constructs a new MaintenanceModeSubscriber.
    *
@@ -83,8 +91,10 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
    *   The bare HTML page renderer.
    * @param \Drupal\Core\Messenger\MessengerInterface $messenger
    *   The messenger.
+   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
    */
-  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, MessengerInterface $messenger) {
+  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, MessengerInterface $messenger, EventDispatcherInterface $event_dispatcher = NULL) {
     $this->maintenanceMode = $maintenance_mode;
     $this->config = $config_factory;
     $this->stringTranslation = $translation;
@@ -92,6 +102,11 @@ public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFa
     $this->account = $account;
     $this->bareHtmlPageRenderer = $bare_html_page_renderer;
     $this->messenger = $messenger;
+    if (!$event_dispatcher) {
+      @trigger_error('Calling MaintenanceModeSubscriber::__construct() without the $event_dispatcher argument is deprecated in drupal:9.4.0 and the $event_dispatcher argument will be required in drupal:10.0.0. See https://www.drupal.org/node/3255799', E_USER_DEPRECATED);
+      $event_dispatcher = \Drupal::service('event_dispatcher');
+    }
+    $this->eventDispatcher = $event_dispatcher;
   }
 
   /**
@@ -108,20 +123,8 @@ public function onKernelRequestMaintenance(RequestEvent $event) {
       \Drupal::service('page_cache_kill_switch')->trigger();
 
       if (!$this->maintenanceMode->exempt($this->account)) {
-        // Deliver the 503 page if the site is in maintenance mode and the
-        // logged in user is not allowed to bypass it.
-
-        // If the request format is not 'html' then show default maintenance
-        // mode page else show a text/plain page with maintenance message.
-        if ($request->getRequestFormat() !== 'html') {
-          $response = new Response($this->getSiteMaintenanceMessage(), 503, ['Content-Type' => 'text/plain']);
-          $event->setResponse($response);
-          return;
-        }
-        drupal_maintenance_theme();
-        $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $this->getSiteMaintenanceMessage()], $this->t('Site under maintenance'), 'maintenance_page');
-        $response->setStatusCode(503);
-        $event->setResponse($response);
+        // When the account is not exempt, other subscribers handle request.
+        $this->eventDispatcher->dispatch($event, MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST);
       }
       else {
         // Display a message if the logged in user has access to the site in
@@ -140,15 +143,24 @@ public function onKernelRequestMaintenance(RequestEvent $event) {
   }
 
   /**
-   * Gets the site maintenance message.
+   * Returns response when site is in maintenance mode and user is not exempt.
    *
-   * @return \Drupal\Component\Render\MarkupInterface
-   *   The formatted site maintenance message.
+   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
+   *   The event to process.
    */
-  protected function getSiteMaintenanceMessage() {
-    return new FormattableMarkup($this->config->get('system.maintenance')->get('message'), [
-      '@site' => $this->config->get('system.site')->get('name'),
-    ]);
+  public function onMaintenanceModeRequest(RequestEvent $event) {
+    $request = $event->getRequest();
+    if ($request->getRequestFormat() !== 'html') {
+      $response = new Response($this->maintenanceMode->getSiteMaintenanceMessage(), 503, ['Content-Type' => 'text/plain']);
+      // Calling RequestEvent::setResponse() also stops propagation of event.
+      $event->setResponse($response);
+      return;
+    }
+    drupal_maintenance_theme();
+    $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $this->maintenanceMode->getSiteMaintenanceMessage()], $this->t('Site under maintenance'), 'maintenance_page');
+    $response->setStatusCode(503);
+    // Calling RequestEvent::setResponse() also stops propagation of the event.
+    $event->setResponse($response);
   }
 
   /**
@@ -157,6 +169,10 @@ protected function getSiteMaintenanceMessage() {
   public static function getSubscribedEvents(): array {
     $events[KernelEvents::REQUEST][] = ['onKernelRequestMaintenance', 30];
     $events[KernelEvents::EXCEPTION][] = ['onKernelRequestMaintenance'];
+    $events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
+      'onMaintenanceModeRequest',
+      -1000,
+    ];
     return $events;
   }
 
diff --git a/core/lib/Drupal/Core/Site/MaintenanceMode.php b/core/lib/Drupal/Core/Site/MaintenanceMode.php
index 38b8bd7a8979..1713d6346c57 100644
--- a/core/lib/Drupal/Core/Site/MaintenanceMode.php
+++ b/core/lib/Drupal/Core/Site/MaintenanceMode.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Core\Site;
 
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\State\StateInterface;
@@ -18,14 +20,28 @@ class MaintenanceMode implements MaintenanceModeInterface {
    */
   protected $state;
 
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
   /**
    * Constructs a new maintenance mode service.
    *
    * @param \Drupal\Core\State\StateInterface $state
    *   The state.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
    */
-  public function __construct(StateInterface $state) {
+  public function __construct(StateInterface $state, ConfigFactoryInterface $config_factory = NULL) {
     $this->state = $state;
+    if (!$config_factory) {
+      @trigger_error('Calling MaintenanceMode::__construct() without the $config_factory argument is deprecated in drupal:9.4.0 and the $config_factory argument will be required in drupal:10.0.0. See https://www.drupal.org/node/3255815', E_USER_DEPRECATED);
+      $config_factory = \Drupal::service('config.factory');
+    }
+    $this->config = $config_factory;
   }
 
   /**
@@ -52,4 +68,13 @@ public function exempt(AccountInterface $account) {
     return $account->hasPermission('access site in maintenance mode');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getSiteMaintenanceMessage() {
+    return new FormattableMarkup($this->config->get('system.maintenance')->get('message'), [
+      '@site' => $this->config->get('system.site')->get('name'),
+    ]);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Site/MaintenanceModeEvents.php b/core/lib/Drupal/Core/Site/MaintenanceModeEvents.php
new file mode 100644
index 000000000000..2348fd54d7ad
--- /dev/null
+++ b/core/lib/Drupal/Core/Site/MaintenanceModeEvents.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Core\Site;
+
+/**
+ * Defines events for maintenance mode.
+ */
+final class MaintenanceModeEvents {
+
+  /**
+   * The name of the event fired when request is made in maintenance more.
+   */
+  const MAINTENANCE_MODE_REQUEST = 'site.maintenance_mode_request';
+
+}
diff --git a/core/lib/Drupal/Core/Site/MaintenanceModeInterface.php b/core/lib/Drupal/Core/Site/MaintenanceModeInterface.php
index bc606b50f255..b3f42f43f52a 100644
--- a/core/lib/Drupal/Core/Site/MaintenanceModeInterface.php
+++ b/core/lib/Drupal/Core/Site/MaintenanceModeInterface.php
@@ -32,4 +32,12 @@ public function applies(RouteMatchInterface $route_match);
    */
   public function exempt(AccountInterface $account);
 
+  /**
+   * Gets the site maintenance message.
+   *
+   * @return \Drupal\Component\Render\MarkupInterface
+   *   The formatted site maintenance message.
+   */
+  public function getSiteMaintenanceMessage();
+
 }
diff --git a/core/modules/jsonapi/config/install/jsonapi.settings.yml b/core/modules/jsonapi/config/install/jsonapi.settings.yml
index c94a4047576d..6ec8f68a4793 100644
--- a/core/modules/jsonapi/config/install/jsonapi.settings.yml
+++ b/core/modules/jsonapi/config/install/jsonapi.settings.yml
@@ -1,2 +1,5 @@
 langcode: en
 read_only: true
+maintenance_header_retry_seconds:
+  min: 5
+  max: 10
diff --git a/core/modules/jsonapi/config/schema/jsonapi.schema.yml b/core/modules/jsonapi/config/schema/jsonapi.schema.yml
index 0fdec75d1c37..23cde29c466b 100644
--- a/core/modules/jsonapi/config/schema/jsonapi.schema.yml
+++ b/core/modules/jsonapi/config/schema/jsonapi.schema.yml
@@ -5,3 +5,13 @@ jsonapi.settings:
     read_only:
       type: boolean
       label: 'Restrict JSON:API to only read operations'
+    maintenance_header_retry_seconds:
+      type: mapping
+      label: 'Maintenance mode Retry-After header settings'
+      mapping:
+        min:
+          type: integer
+          label: 'Minimum value for Retry-After header in seconds'
+        max:
+          type: integer
+          label: 'Maximum value for Retry-After header in seconds'
diff --git a/core/modules/jsonapi/jsonapi.install b/core/modules/jsonapi/jsonapi.install
index 36bc4f021105..80009b525a46 100644
--- a/core/modules/jsonapi/jsonapi.install
+++ b/core/modules/jsonapi/jsonapi.install
@@ -82,3 +82,17 @@ function jsonapi_requirements($phase) {
 function jsonapi_update_last_removed() {
   return 8701;
 }
+
+/**
+ * Set values for maintenance_header_retry_seconds min and max.
+ *
+ * @see https://www.drupal.org/node/3247453
+ */
+function jsonapi_update_9401() {
+  $config = \Drupal::configFactory()->getEditable('jsonapi.settings');
+  $config->set('maintenance_header_retry_seconds', [
+    'min' => 5,
+    'max' => 10,
+  ]);
+  $config->save(TRUE);
+}
diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml
index 56e585f90072..33852841aa80 100644
--- a/core/modules/jsonapi/jsonapi.services.yml
+++ b/core/modules/jsonapi/jsonapi.services.yml
@@ -211,6 +211,11 @@ services:
       - [setValidator, []]
     tags:
       - { name: event_subscriber, priority: 1000 }
+  jsonapi.maintenance_mode_subscriber:
+    class: Drupal\jsonapi\EventSubscriber\JsonapiMaintenanceModeSubscriber
+    arguments: ['@maintenance_mode', '@config.factory']
+    tags:
+      - { name: event_subscriber }
 
   # Revision management.
   jsonapi.version_negotiator:
diff --git a/core/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php b/core/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php
new file mode 100644
index 000000000000..a00db170c5f4
--- /dev/null
+++ b/core/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\jsonapi\EventSubscriber;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Site\MaintenanceModeEvents;
+use Drupal\Core\Site\MaintenanceModeInterface;
+use Drupal\jsonapi\JsonApiResource\ErrorCollection;
+use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\JsonApiResource\LinkCollection;
+use Drupal\jsonapi\JsonApiResource\NullIncludedData;
+use Drupal\jsonapi\ResourceResponse;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Maintenance mode subscriber for JSON:API requests.
+ *
+ * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
+ *   may change at any time and could break any dependencies on it.
+ */
+class JsonapiMaintenanceModeSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The maintenance mode.
+   *
+   * @var \Drupal\Core\Site\MaintenanceMode
+   */
+  protected $maintenanceMode;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Constructs a new JsonapiMaintenanceModeSubscriber.
+   *
+   * @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode
+   *   The maintenance mode.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory) {
+    $this->maintenanceMode = $maintenance_mode;
+    $this->config = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+    $events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
+      'onMaintenanceModeRequest',
+      -800,
+    ];
+    return $events;
+  }
+
+  /**
+   * Returns response when site is in maintenance mode and user is not exempt.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
+   *   The event to process.
+   */
+  public function onMaintenanceModeRequest(RequestEvent $event) {
+    $request = $event->getRequest();
+
+    if ($request->getRequestFormat() !== 'api_json') {
+      return;
+    }
+    // Retry-After will be random within a range defined in jsonapi settings.
+    // The goals are to keep it short and to reduce the thundering herd problem.
+    $header_settings = $this->config->get('jsonapi.settings')->get('maintenance_header_retry_seconds');
+    $retry_after_time = rand($header_settings['min'], $header_settings['max']);
+    $http_exception = new HttpException(503, $this->maintenanceMode->getSiteMaintenanceMessage());
+    $document = new JsonApiDocumentTopLevel(new ErrorCollection([$http_exception]), new NullIncludedData(), new LinkCollection([]));
+    $response = new ResourceResponse($document, $http_exception->getStatusCode(), [
+      'Content-Type' => 'application/vnd.api+json',
+      'Retry-After' => $retry_after_time,
+    ]);
+    // Calling RequestEvent::setResponse() also stops propagation of event.
+    $event->setResponse($response);
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/fixtures/update/jsonapi.php b/core/modules/jsonapi/tests/fixtures/update/jsonapi.php
new file mode 100644
index 000000000000..462590152a1b
--- /dev/null
+++ b/core/modules/jsonapi/tests/fixtures/update/jsonapi.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Test fixture.
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Serialization\Yaml;
+
+$connection = Database::getConnection();
+
+$connection->insert('key_value')
+  ->fields([
+    'collection',
+    'name',
+    'value',
+  ])
+  ->values([
+    'collection' => 'system.schema',
+    'name' => 'jsonapi',
+    'value' => serialize(9000),
+  ])
+  ->execute();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['jsonapi'] = 0;
+$extensions['module']['serialization'] = 0;
+$connection->update('config')
+  ->fields(['data' => serialize($extensions)])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
+$jsonapi_settings = Yaml::decode(file_get_contents(__DIR__ . '/jsonapi.settings.yml'));
+$connection->insert('config')
+  ->fields([
+    'collection',
+    'name',
+    'data',
+  ])
+  ->values([
+    'collection' => '',
+    'name' => 'jsonapi.settings',
+    'data' => serialize($jsonapi_settings),
+  ])
+  ->execute();
diff --git a/core/modules/jsonapi/tests/fixtures/update/jsonapi.settings.yml b/core/modules/jsonapi/tests/fixtures/update/jsonapi.settings.yml
new file mode 100644
index 000000000000..c94a4047576d
--- /dev/null
+++ b/core/modules/jsonapi/tests/fixtures/update/jsonapi.settings.yml
@@ -0,0 +1,2 @@
+langcode: en
+read_only: true
diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
index 272fbdf395c4..167e279b444c 100644
--- a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
@@ -513,6 +513,39 @@ public function testRead() {
     ]));
     $this->assertSession()->statusCodeEquals(200);
     $this->assertCount(0, $collection_output['data']);
+
+    // Request in maintenance mode returns valid JSON.
+    $this->container->get('state')->set('system.maintenance_mode', TRUE);
+    $response = $this->drupalGet('/jsonapi/taxonomy_term/tags');
+    $this->assertSession()->statusCodeEquals(503);
+    $this->assertSession()->responseHeaderContains('Content-Type', 'application/vnd.api+json');
+    $retry_after_time = $this->getSession()->getResponseHeader('Retry-After');
+    $this->assertTrue($retry_after_time >= 5 && $retry_after_time <= 10);
+    $expected_message = 'Drupal is currently under maintenance. We should be back shortly. Thank you for your patience.';
+    $this->assertSame($expected_message, Json::decode($response)['errors'][0]['detail']);
+
+    // Test that logged in user does not get logged out in maintenance mode
+    // when hitting jsonapi route.
+    $this->container->get('state')->set('system.maintenance_mode', FALSE);
+    $this->drupalLogin($this->userCanViewProfiles);
+    $this->container->get('state')->set('system.maintenance_mode', TRUE);
+    $this->drupalGet('/jsonapi/taxonomy_term/tags');
+    $this->assertSession()->statusCodeEquals(503);
+    $this->assertTrue($this->drupalUserIsLoggedIn($this->userCanViewProfiles));
+    // Test that user gets logged out when hitting non-jsonapi route.
+    $this->drupalGet('/some/normal/route');
+    $this->assertFalse($this->drupalUserIsLoggedIn($this->userCanViewProfiles));
+    $this->container->get('state')->set('system.maintenance_mode', FALSE);
+
+    // Test that admin user can bypass maintenance mode.
+    $admin_user = $this->drupalCreateUser([], NULL, TRUE);
+    $this->drupalLogin($admin_user);
+    $this->container->get('state')->set('system.maintenance_mode', TRUE);
+    $this->drupalGet('/jsonapi/taxonomy_term/tags');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertTrue($this->drupalUserIsLoggedIn($admin_user));
+    $this->container->get('state')->set('system.maintenance_mode', FALSE);
+    $this->drupalLogout();
   }
 
   /**
diff --git a/core/modules/jsonapi/tests/src/Functional/Update/JsonApiUpdatePathTest.php b/core/modules/jsonapi/tests/src/Functional/Update/JsonApiUpdatePathTest.php
new file mode 100644
index 000000000000..ec9b1cbe0d85
--- /dev/null
+++ b/core/modules/jsonapi/tests/src/Functional/Update/JsonApiUpdatePathTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests adding retry-after header settings.
+ *
+ * @group legacy
+ * @group jsonapi
+ */
+class JsonApiUpdatePathTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.0.0.bare.standard.php.gz',
+      __DIR__ . '/../../../../tests/fixtures/update/jsonapi.php',
+    ];
+  }
+
+  /**
+   * Tests adding retry-after header settings.
+   *
+   * @see jsonapi_update_9401()
+   */
+  public function testUpdate9401() {
+    $config = $this->config('jsonapi.settings');
+    $this->assertTrue($config->get('read_only'));
+    $this->assertNull($config->get('maintenance_header_retry_seconds'));
+
+    // Run updates.
+    $this->runUpdates();
+
+    $config = $this->config('jsonapi.settings');
+    $this->assertTrue($config->get('read_only'));
+    $header_settings = $config->get('maintenance_header_retry_seconds');
+    $this->assertSame(5, $header_settings['min']);
+    $this->assertSame(10, $header_settings['max']);
+  }
+
+}
diff --git a/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php b/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php
index e805162c0825..390dc182cd62 100644
--- a/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php
@@ -4,12 +4,12 @@
 
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Site\MaintenanceModeEvents;
 use Drupal\Core\Site\MaintenanceModeInterface;
 use Drupal\Core\Url;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpKernel\Event\RequestEvent;
-use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
  * Maintenance mode subscriber to log out users.
@@ -48,8 +48,14 @@ public function __construct(MaintenanceModeInterface $maintenance_mode, AccountI
    *
    * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
    *   The event to process.
+   *
+   * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Use
+   *   \Drupal\user\EventSubscriber::onMaintenanceModeRequest() instead.
+   *
+   * @see https://www.drupal.org/node/3255799
    */
   public function onKernelRequestMaintenance(RequestEvent $event) {
+    @trigger_error('\Drupal\user\EventSubscriber::onKernelRequestMaintenance() is deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Use \Drupal\user\EventSubscriber::onMaintenanceModeRequest() instead. See https://www.drupal.org/node/3255799', E_USER_DEPRECATED);
     $request = $event->getRequest();
     $route_match = RouteMatch::createFromRequest($request);
     if ($this->maintenanceMode->applies($route_match)) {
@@ -64,11 +70,31 @@ public function onKernelRequestMaintenance(RequestEvent $event) {
     }
   }
 
+  /**
+   * Logout users if site is in maintenance mode and user is not exempt.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
+   *   The event to process.
+   */
+  public function onMaintenanceModeRequest(RequestEvent $event) {
+    // If the site is offline, log out unprivileged users.
+    if ($this->account->isAuthenticated()) {
+      user_logout();
+      // Redirect to homepage.
+      $event->setResponse(
+        new RedirectResponse(Url::fromRoute('<front>')->toString())
+      );
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
   public static function getSubscribedEvents(): array {
-    $events[KernelEvents::REQUEST][] = ['onKernelRequestMaintenance', 31];
+    $events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
+      'onMaintenanceModeRequest',
+      -900,
+    ];
     return $events;
   }
 
-- 
GitLab