diff --git a/core/lib/Drupal/Component/Utility/UrlHelper.php b/core/lib/Drupal/Component/Utility/UrlHelper.php
index bd550460f9..16cb4ab452 100644
--- a/core/lib/Drupal/Component/Utility/UrlHelper.php
+++ b/core/lib/Drupal/Component/Utility/UrlHelper.php
@@ -32,9 +32,6 @@ class UrlHelper {
    * @param array $query
    *   The query parameter array to be processed; for instance,
    *   \Drupal::request()->query->all().
-   * @param string $parent
-   *   (optional) Internal use only. Used to build the $query array key for
-   *   nested items. Defaults to an empty string.
    *
    * @return string
    *   A rawurlencoded string which can be used as or appended to the URL query
@@ -42,27 +39,16 @@ class UrlHelper {
    *
    * @ingroup php_wrappers
    */
-  public static function buildQuery(array $query, $parent = '') {
-    $params = [];
-
-    foreach ($query as $key => $value) {
-      $key = ($parent ? $parent . rawurlencode('[' . $key . ']') : rawurlencode($key));
-
-      // Recurse into children.
-      if (is_array($value)) {
-        $params[] = static::buildQuery($value, $key);
-      }
-      // If a query parameter value is NULL, only append its key.
-      elseif (!isset($value)) {
-        $params[] = $key;
-      }
-      else {
-        // For better readability of paths in query strings, we decode slashes.
-        $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
-      }
-    }
-
-    return implode('&', $params);
+  public static function buildQuery(array $query) {
+    // Do some conversions to match previous implementations.
+    array_walk_recursive($query, function (&$value, $key) {
+      // Previous versions of Drupal created query arguments with NULL values
+      // but http_build_query throws them away. It also throws away objects.
+      // As a convenience and backward compatibility we convert all values to a
+      // string.
+      $value = (string) $value;
+    });
+    return http_build_query($query, '', '&', PHP_QUERY_RFC3986);
   }
 
   /**
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
index d1ba348e12..216ebe7d5c 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipePlaceholderTestCases.php
@@ -7,6 +7,8 @@
 namespace Drupal\big_pipe_test;
 
 use Drupal\big_pipe\Render\BigPipeMarkup;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -56,29 +58,33 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
         ],
       ]
     );
-    $status_messages->bigPipePlaceholderId = 'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA';
+
+    $placeholder_id = UrlHelper::buildQuery(['callback' => 'Drupal\Core\Render\Element\StatusMessages::renderMessages', 'args' => [NULL], 'token' => 'a8c34b5e']);
+    $encoded_placeholder_id = Html::escape($placeholder_id);
+    $status_messages->bigPipePlaceholderId = $encoded_placeholder_id;
     $status_messages->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
+      '#markup' => '<span data-big-pipe-placeholder-id="' . $encoded_placeholder_id . '"></span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
         'drupalSettings' => [
           'bigPipePlaceholderIds' => [
-            'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args%5B0%5D&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => TRUE,
+            $placeholder_id => TRUE,
           ],
         ],
         'big_pipe_placeholders' => [
-          'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA' => $status_messages->placeholderRenderArray,
+          $encoded_placeholder_id => $status_messages->placeholderRenderArray,
         ],
       ],
     ];
-    $status_messages->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>';
+
+    $status_messages->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="' . $encoded_placeholder_id . '"></span>';
     $status_messages->bigPipeNoJsPlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>',
+      '#markup' => '<span data-big-pipe-nojs-placeholder-id="' . $encoded_placeholder_id . '"></span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'big_pipe_nojs_placeholders' => [
-          '<span data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args%5B0%5D&amp;token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"></span>' => $status_messages->placeholderRenderArray,
+          '<span data-big-pipe-nojs-placeholder-id="' . $encoded_placeholder_id . '"></span>' => $status_messages->placeholderRenderArray,
         ],
       ],
     ];
@@ -87,7 +93,7 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
         [
           'command' => 'insert',
           'method' => 'replaceWith',
-          'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args%5B0%5D&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]',
+          'selector' => '[data-big-pipe-placeholder-id="' . $placeholder_id . '"]',
           'data' => '<div data-drupal-messages>' . "\n" . ' <div role="contentinfo" aria-label="Status message" class="messages messages--status">' . "\n" . ' <h2 class="visually-hidden">Status message</h2>' . "\n" . ' Hello from BigPipe!' . "\n" . ' </div>' . "\n" . ' </div>' . "\n",
           'settings' => NULL,
         ],
@@ -245,24 +251,26 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
         '#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
       ]
     );
-    $exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU';
+    $placeholder_id = UrlHelper::buildQuery(['callback' => '\Drupal\big_pipe_test\BigPipeTestController::exception', 'args' => ['llamas', 'suck'], 'token' => '68a75f1a']);
+    $encoded_placeholder_id = Html::escape($placeholder_id);
+    $exception->bigPipePlaceholderId = $encoded_placeholder_id;
     $exception->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></span>',
+      '#markup' => '<span data-big-pipe-placeholder-id="' . $encoded_placeholder_id . '"></span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
         'drupalSettings' => [
           'bigPipePlaceholderIds' => [
-            'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&args%5B0%5D=llamas&args%5B1%5D=suck&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => TRUE,
+            $placeholder_id => TRUE,
           ],
         ],
         'big_pipe_placeholders' => [
-          'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => $exception->placeholderRenderArray,
+          $encoded_placeholder_id => $exception->placeholderRenderArray,
         ],
       ],
     ];
     $exception->embeddedAjaxResponseCommands = NULL;
-    $exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3Aexception&amp;args%5B0%5D=llamas&amp;args%5B1%5D=suck&amp;token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></span>';
+    $exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="' . $encoded_placeholder_id . '"></span>';
     $exception->bigPipeNoJsPlaceholderRenderArray = [
       '#markup' => $exception->bigPipeNoJsPlaceholder,
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
@@ -285,24 +293,26 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
         '#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
       ]
     );
-    $embedded_response_exception->bigPipePlaceholderId = 'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU';
+    $placeholder_id = UrlHelper::buildQuery(['callback' => '\Drupal\big_pipe_test\BigPipeTestController::responseException', 'args' => [], 'token' => '2a9bd022']);
+    $encoded_placeholder_id = Html::escape($placeholder_id);
+    $embedded_response_exception->bigPipePlaceholderId = $encoded_placeholder_id;
     $embedded_response_exception->bigPipePlaceholderRenderArray = [
-      '#markup' => '<span data-big-pipe-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></span>',
+      '#markup' => '<span data-big-pipe-placeholder-id="' . $encoded_placeholder_id . '"></span>',
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
       '#attached' => [
         'library' => ['big_pipe/big_pipe'],
         'drupalSettings' => [
           'bigPipePlaceholderIds' => [
-            'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&&token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU' => TRUE,
+            $placeholder_id => TRUE,
           ],
         ],
         'big_pipe_placeholders' => [
-          'callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU' => $embedded_response_exception->placeholderRenderArray,
+          $encoded_placeholder_id => $embedded_response_exception->placeholderRenderArray,
         ],
       ],
     ];
     $embedded_response_exception->embeddedAjaxResponseCommands = NULL;
-    $embedded_response_exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="callback=%5CDrupal%5Cbig_pipe_test%5CBigPipeTestController%3A%3AresponseException&amp;&amp;token=PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU"></span>';
+    $embedded_response_exception->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="' . $encoded_placeholder_id . '"></span>';
     $embedded_response_exception->bigPipeNoJsPlaceholderRenderArray = [
       '#markup' => $embedded_response_exception->bigPipeNoJsPlaceholder,
       '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
diff --git a/core/modules/system/src/Tests/Database/SelectTableSortDefaultTest.php b/core/modules/system/src/Tests/Database/SelectTableSortDefaultTest.php
new file mode 100644
index 0000000000..f1505c88f6
--- /dev/null
+++ b/core/modules/system/src/Tests/Database/SelectTableSortDefaultTest.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\system\Tests\Database;
+
+/**
+ * Tests the tablesort query extender.
+ *
+ * @group Database
+ */
+class SelectTableSortDefaultTest extends DatabaseWebTestBase {
+
+  /**
+   * Confirms that a tablesort query returns the correct results.
+   *
+   * Note that we have to make an HTTP request to a test page handler
+   * because the pager depends on GET parameters.
+   */
+  function testTableSortQuery() {
+    $sorts = array(
+      array('field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'),
+      array('field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'),
+      array('field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'),
+      array('field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'),
+      // more elements here
+
+    );
+
+    foreach ($sorts as $sort) {
+      $this->drupalGet('database_test/tablesort/', array('query' => array('order' => $sort['field'], 'sort' => $sort['sort'])));
+      $data = json_decode($this->getRawContent());
+
+      $first = array_shift($data->tasks);
+      $last = array_pop($data->tasks);
+
+      $this->assertEqual($first->task, $sort['first'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
+      $this->assertEqual($last->task, $sort['last'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
+    }
+  }
+
+  /**
+   * Confirms precedence of tablesorts headers.
+   *
+   * If a tablesort's orderByHeader is called before another orderBy, then its
+   * header happens first.
+   */
+  function testTableSortQueryFirst() {
+    $sorts = array(
+      array('field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'),
+      array('field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'),
+      array('field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'),
+      array('field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'),
+      // more elements here
+
+    );
+
+    foreach ($sorts as $sort) {
+      $this->drupalGet('database_test/tablesort_first/', array('query' => array('order' => $sort['field'], 'sort' => $sort['sort'])));
+      $data = json_decode($this->getRawContent());
+
+      $first = array_shift($data->tasks);
+      $last = array_pop($data->tasks);
+
+      $this->assertEqual($first->task, $sort['first'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
+      $this->assertEqual($last->task, $sort['last'], format_string('Items appear in the correct order sorting by @field @sort.', array('@field' => $sort['field'], '@sort' => $sort['sort'])));
+    }
+  }
+
+  /**
+   * Confirms that tableselect is rendered without error.
+   *
+   * Specifically that no sort is set in a tableselect, and that header links
+   * are correct.
+   */
+  function testTableSortDefaultSort() {
+    $this->drupalGet('database_test/tablesort_default_sort');
+
+    // Verify that the table was displayed. Just the header is checked for
+    // because if there were any fatal errors or exceptions in displaying the
+    // sorted table, it would not print the table.
+    $this->assertText(t('Username'));
+
+    // Verify that the header links are built properly.
+    $this->assertLinkByHref('database_test/tablesort_default_sort');
+    $this->assertPattern('/\<a.*title\=\"' . t('sort by Username') . '\".*\>/');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
index ff1b423084..1c1fee67f7 100644
--- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\Component\Utility;
 
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Component\Utility\UrlHelper;
 use PHPUnit\Framework\TestCase;
 
@@ -19,11 +20,10 @@ class UrlHelperTest extends TestCase {
    */
   public function providerTestBuildQuery() {
     return [
-      [['a' => ' &#//+%20@۞'], 'a=%20%26%23//%2B%2520%40%DB%9E', 'Value was properly encoded.'],
-      [[' &#//+%20@۞' => 'a'], '%20%26%23%2F%2F%2B%2520%40%DB%9E=a', 'Key was properly encoded.'],
-      [['a' => '1', 'b' => '2', 'c' => '3'], 'a=1&b=2&c=3', 'Multiple values were properly concatenated.'],
-      [['a' => ['b' => '2', 'c' => '3'], 'd' => 'foo'], 'a%5Bb%5D=2&a%5Bc%5D=3&d=foo', 'Nested array was properly encoded.'],
-      [['foo' => NULL], 'foo', 'Simple parameters are properly added.'],
+      [['a' => ' &#//+%20@۞'], 'Value was properly encoded.'],
+      [['a' => '1', 'b' => '2', 'c' => '3'], 'Multiple values were properly concatenated.'],
+      [['a' => ['b' => '2', 'c' => '3'], 'd' => 'foo'], 'Nested array was properly encoded.'],
+      [['foo' => NULL], 'Simple parameters are properly added.'],
     ];
   }
 
@@ -35,13 +35,25 @@ public function providerTestBuildQuery() {
    *
    * @param array $query
    *   The array of query parameters.
-   * @param string $expected
-   *   The expected query string.
    * @param string $message
    *   The assertion message.
    */
-  public function testBuildQuery($query, $expected, $message) {
-    $this->assertEquals(UrlHelper::buildQuery($query), $expected, $message);
+  public function testBuildQuery($query, $message) {
+    parse_str(UrlHelper::buildQuery($query), $result);
+    $this->assertEquals($query, $result, $message);
+  }
+
+  /**
+   * Test query building of query's that can't be generalized.
+   */
+  public function testBuildQuerySpecial() {
+    // Parse string throws away the leading space in the key breaking our
+    // generalized test.
+    $this->assertEquals('%20%26%23%2F%2F%2B%2520%40%DB%9E=a', UrlHelper::buildQuery([' &#//+%20@۞' => 'a']), 'Key was properly encoded.');
+
+    // Ensure as a convenience we can put translatable and formatted markup
+    // objects into queries.
+    $this->assertEquals('a=foo', UrlHelper::buildQuery(['a' => new FormattableMarkup('foo', [])]), 'Objects cast as a string.');
   }
 
   /**
