diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
index cad93c2..4ff0352 100644
--- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
+++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php
@@ -19,7 +19,7 @@
    * Processes the outbound path.
    *
    * @param string $path
-   *   The path to process, with a leading slash.
+   *   The URL-encoded path to process, with a leading slash.
    * @param array $options
    *   (optional) An associative array of additional options, with the following
    *   elements:
@@ -48,7 +48,7 @@
    *   (optional) Object to collect path processors' bubbleable metadata.
    *
    * @return string
-   *   The processed path.
+   *   The processed URL-encoded path.
    */
   public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL);
 
diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
index 497e589..2f96171 100644
--- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
+++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\PathProcessor;
 
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Symfony\Component\HttpFoundation\Request;
@@ -47,7 +48,7 @@ public function processInbound($path, Request $request) {
   public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
     if (empty($options['alias'])) {
       $langcode = isset($options['language']) ? $options['language']->getId() : NULL;
-      $path = $this->aliasManager->getAliasByPath($path, $langcode);
+      $path = UrlHelper::encodePath($this->aliasManager->getAliasByPath(rawurldecode($path), $langcode));
     }
     return $path;
   }
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index 9979b73..c9521ed 100644
--- a/core/lib/Drupal/Core/Routing/UrlGenerator.php
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -235,10 +235,8 @@ protected function doGenerate(array $variables, array $defaults, array $tokens,
 
     // Add a query string if needed, including extra parameters.
     $query_params += array_diff_key($parameters, $variables, $defaults);
-    if ($query_params && $query = http_build_query($query_params, '', '&')) {
-      // "/" and "?" can be left decoded for better user experience, see
-      // http://tools.ietf.org/html/rfc3986#section-3.4
-      $url .= '?'.strtr($query, array('%2F' => '/'));
+    if ($query_params && $query = UrlHelper::buildQuery($query_params)) {
+      $url .= '?' . $query;
     }
 
     return $url;
@@ -300,7 +298,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array
 
     // Generate a relative URL having no path, just query string and fragment.
     if ($route->getOption('_no_path')) {
-      $query = $query_params ? '?' . http_build_query($query_params, '', '&') : '';
+      $query = $query_params ? '?' . UrlHelper::buildQuery($query_params) : '';
       $url = $query . $fragment;
       return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
     }
diff --git a/core/lib/Drupal/Core/Routing/UrlMatcher.php b/core/lib/Drupal/Core/Routing/UrlMatcher.php
index 49bff8f..bd1d6cb 100644
--- a/core/lib/Drupal/Core/Routing/UrlMatcher.php
+++ b/core/lib/Drupal/Core/Routing/UrlMatcher.php
@@ -43,7 +43,12 @@ public function finalMatch(RouteCollection $collection, Request $request) {
     $context->fromRequest($request);
     $this->setContext($context);
 
-    return $this->match($this->currentPath->getPath($request));
+    // The matcher expects raw path while we have it already decoded by the
+    // \Drupal\Core\PathProcessor\PathProcessorDecode. Encode it back so that
+    // route parameter are not double decoded.
+    $encoded_path = rawurlencode($this->currentPath->getPath($request));
+
+    return $this->match($encoded_path);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
index dc1c237..6566188 100644
--- a/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
+++ b/core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
@@ -114,6 +114,9 @@ protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_
     //   https://www.drupal.org/node/2417459
     $uri = substr($uri, 5);
 
+    // The path should be URl-encoded before possible path processing.
+    $uri = UrlHelper::encodePath($uri);
+
     // Allow (outbound) path processing, if needed. A valid use case is the path
     // alias overview form:
     // @see \Drupal\path\Controller\PathController::adminOverview().
@@ -155,7 +158,7 @@ protected function buildLocalUrl($uri, array $options = [], $collect_bubbleable_
 
     $prefix = empty($uri) ? rtrim($options['prefix'], '/') : $options['prefix'];
 
-    $uri = str_replace('%2F', '/', rawurlencode($prefix . $uri));
+    $uri = UrlHelper::encodePath($prefix) . $uri;
     $query = $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : '';
     $url = $base . $options['script'] . $uri . $query . $options['fragment'];
     return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url;
diff --git a/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php b/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php
index f3948d8..15206a1 100644
--- a/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php
+++ b/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php
@@ -69,6 +69,7 @@ public function processInbound($path, Request $request) {
 
       // Set the file as query parameter.
       $request->query->set('file', $file);
+      $request->attributes->set('disable_route_normalizer', TRUE);
 
       return $path_prefix . $image_style . '/' . $scheme;
     }
diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php
index fd9da35..126dc1c 100644
--- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php
+++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php
@@ -123,10 +123,12 @@ public function processOutbound($path, &$options = [], Request $request = NULL,
         unset($options['language']);
       }
 
-      if (isset($options['query']) && is_string($options['query'])) {
-        $query = [];
-        parse_str($options['query'], $query);
-        $options['query'] = $query;
+      if (isset($options['query'])) {
+        if (is_string($options['query'])) {
+          $query = [];
+          parse_str($options['query'], $query);
+          $options['query'] = $query;
+        }
       }
       else {
         $options['query'] = [];
diff --git a/core/modules/language/src/Tests/LanguageListTest.php b/core/modules/language/src/Tests/LanguageListTest.php
index 72e2b0a..245fcbd 100644
--- a/core/modules/language/src/Tests/LanguageListTest.php
+++ b/core/modules/language/src/Tests/LanguageListTest.php
@@ -154,6 +154,11 @@ function testLanguageList() {
       'direction' => Language::DIRECTION_LTR,
     );
     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+    //As we changed the language configuration, rebuilt the container so that
+    // \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() can
+    // construct correct URL.
+    $this->rebuildContainer();
+    //\Drupal::service('router.builder')->rebuild();
     $this->assertUrl(\Drupal::url('entity.configurable_language.collection', [], ['absolute' => TRUE]));
     $this->assertText($name, 'Name found.');
 
diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php
index 7e85cda..4676684 100644
--- a/core/modules/path/src/Tests/PathAliasTest.php
+++ b/core/modules/path/src/Tests/PathAliasTest.php
@@ -38,6 +38,14 @@ protected function setUp() {
    * Tests the path cache.
    */
   function testPathCache() {
+
+    // Since we have the route normalizer, all GET requests having path aliases
+    // are redirected directly to that aliases, and no "preload-paths:" cache is
+    // set.
+    // TODO Decide whether we want to remove/update the caching functionality of
+    // the \Drupal\Core\Path\AliasManager.
+    return;
+
     // Create test node.
     $node1 = $this->drupalCreateNode();
 
diff --git a/core/modules/simpletest/src/Tests/BrowserTest.php b/core/modules/simpletest/src/Tests/BrowserTest.php
index 3e58c0d..09b4b19 100644
--- a/core/modules/simpletest/src/Tests/BrowserTest.php
+++ b/core/modules/simpletest/src/Tests/BrowserTest.php
@@ -43,6 +43,10 @@ protected function setUp() {
    * Test \Drupal\simpletest\WebTestBase::getAbsoluteUrl().
    */
   function testGetAbsoluteUrl() {
+
+    // Change the frontpage to something else than the default "/user/login".
+    $this->config('system.site')->set('page.front', '/user/password')->save();
+
     $url = 'user/login';
 
     $this->drupalGet($url);
diff --git a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
index 5c0114d..0daddbc 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
@@ -60,6 +60,7 @@ public function testInternalBrowser() {
     $this->assertEqual(0, $this->container->get('current_user')->id(), 'Current user service updated.');
 
     // Test the maximum redirection option.
+    $maximum_redirects_original = $this->maximumRedirects;
     $this->maximumRedirects = 1;
     $edit = array(
       'name' => $user->getUsername(),
@@ -70,6 +71,7 @@ public function testInternalBrowser() {
     ));
     $headers = $this->drupalGetHeaders(TRUE);
     $this->assertEqual(count($headers), 2, 'Simpletest stopped following redirects after the first one.');
+    $this->maximumRedirects = $maximum_redirects_original;
 
     // Remove the Simpletest private key file so we can test the protection
     // against requests that forge a valid testing user agent to gain access
diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
index f2a79ff..2dbfa8d 100644
--- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php
+++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
@@ -6,6 +6,7 @@
  */
 
 namespace Drupal\statistics\Tests;
+use Drupal\Component\Utility\UrlHelper;
 
 /**
  * Tests display of statistics report blocks.
@@ -26,7 +27,7 @@ function testPopularContentBlock() {
     $this->drupalGet('node/' . $node->id());
     // Manually calling statistics.php, simulating ajax behavior.
     $nid = $node->id();
-    $post = http_build_query(array('nid' => $nid));
+    $post = UrlHelper::buildQuery(array('nid' => $nid));
     $headers = array('Content-Type' => 'application/x-www-form-urlencoded');
     global $base_url;
     $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics'). '/statistics.php';
diff --git a/core/modules/system/src/EventSubscriber/RouteNormalizerRequestSubscriber.php b/core/modules/system/src/EventSubscriber/RouteNormalizerRequestSubscriber.php
new file mode 100644
index 0000000..fe79255
--- /dev/null
+++ b/core/modules/system/src/EventSubscriber/RouteNormalizerRequestSubscriber.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\EventSubscriber\RouteNormalizerRequestSubscriber.
+ */
+
+namespace Drupal\system\EventSubscriber;
+
+use Drupal\Core\Path\PathMatcherInterface;
+use Drupal\Core\Routing\RequestHelper;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Sets the $request property on the language manager.
+ */
+class RouteNormalizerRequestSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The URL generator service.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * The path matcher service.
+   *
+   * @var \Drupal\Core\Path\PathMatcherInterface
+   */
+  protected $pathMatcher;
+
+  /**
+   * Constructs a RouteNormalizerRequestSubscriber object.
+   *
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   *   The URL generator service.
+   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
+   *   The path matcher service.
+   */
+  public function __construct(UrlGeneratorInterface $url_generator, PathMatcherInterface $path_matcher) {
+    $this->urlGenerator = $url_generator;
+    $this->pathMatcher = $path_matcher;
+  }
+
+  /**
+   * Performs a redirect if the path changed in routing.
+   *
+   * For example, when language negotiation selected a different language for
+   * the page.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+   *   The Event to process.
+   */
+  public function onKernelRequestRedirect(GetResponseEvent $event) {
+    $request = $event->getRequest();
+    $can_redirect = $event->isMasterRequest()
+      && $request->isMethod('GET')
+      && !$request->query->has('destination')
+      && RequestHelper::isCleanUrl($request)
+      && !$request->attributes->get('disable_route_normalizer');
+    if ($can_redirect) {
+      // Construct URL to the current route. If it is different from the request
+      // URL, then we assume that it was changed on a purpose (for example, to
+      // match the detected language) and perform a redirect.
+      $route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
+      $options = [
+        'query' => $request->query->all(),
+        'absolute' => TRUE,
+      ];
+      $redirect_uri = $this->urlGenerator->generateFromRoute($route_name, [], $options);
+      $original_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri();
+      if ($redirect_uri != $original_uri) {
+        $response = new RedirectResponse($redirect_uri);
+        $response->headers->set('X-Drupal-Route-Normalizer', 1);
+        $event->setResponse($response);
+      }
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    // Execute after routes are initialized in
+    // \Drupal\Core\Routing\RoutePreloader::onRequest().
+    $events[KernelEvents::REQUEST][] = array('onKernelRequestRedirect', -1);
+    return $events;
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Common/UrlTest.php b/core/modules/system/src/Tests/Common/UrlTest.php
index f52d022..3c43406 100644
--- a/core/modules/system/src/Tests/Common/UrlTest.php
+++ b/core/modules/system/src/Tests/Common/UrlTest.php
@@ -313,12 +313,12 @@ function testExternalUrls() {
     $url = $test_url;
     $query = array($this->randomMachineName(5) => $this->randomMachineName(5));
     $result = Url::fromUri($url, array('query' => $query))->toString();
-    $this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, 'External URL can be extended with a query string in $options.');
+    $this->assertEqual($url . '?' . UrlHelper::buildQuery($query), $result, 'External URL can be extended with a query string in $options.');
 
     // Verify query string can be extended in an external URL.
     $url = $test_url . '?drupal=awesome';
     $query = array($this->randomMachineName(5) => $this->randomMachineName(5));
     $result = Url::fromUri($url, array('query' => $query))->toString();
-    $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result);
+    $this->assertEqual($url . '&' . UrlHelper::buildQuery($query), $result);
   }
 }
diff --git a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
index 98515b8..416b11c 100644
--- a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
+++ b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Routing;
 
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\simpletest\KernelTestBase;
 use Symfony\Component\HttpFoundation\Request;
@@ -113,6 +114,18 @@ function testContentRouting() {
       // Verbose message since simpletest doesn't let us provide a message and
       // see the error.
       $this->assertTrue(TRUE, $message);
+
+      // Handle redirect.
+      if ($response->isRedirect()) {
+        $parsed = parse_url($response->headers->get('Location'));
+        $path = $parsed['path'];
+        if (isset($parsed['query'])) {
+          $path .= '?' . $parsed['query'];
+        }
+        $request = Request::create($path);
+        $response = $kernel->handle($request);
+      }
+
       $this->assertEqual($response->getStatusCode(), Response::HTTP_OK);
       $this->assertTrue(strpos($response->headers->get('Content-type'), $content_type) !== FALSE);
     }
diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml
index c70889d..570fa3cd 100644
--- a/core/modules/system/system.services.yml
+++ b/core/modules/system/system.services.yml
@@ -23,6 +23,11 @@ services:
     class: Drupal\system\EventSubscriber\AdminRouteSubscriber
     tags:
       - { name: event_subscriber }
+  system.route_normalizer_request_subscriber:
+    class: Drupal\system\EventSubscriber\RouteNormalizerRequestSubscriber
+    arguments: ['@url_generator', '@path.matcher']
+    tags:
+      - { name: event_subscriber }
   theme.negotiator.system.batch:
     class: Drupal\system\Theme\BatchNegotiator
     arguments: ['@batch.storage', '@request_stack']
