diff --git a/config/schema/redirect.schema.yml b/config/schema/redirect.schema.yml
index 1a521a6..5d83391 100644
--- a/config/schema/redirect.schema.yml
+++ b/config/schema/redirect.schema.yml
@@ -45,4 +45,4 @@ redirect.settings:
       label: 'Set Content Location Header'
     term_path_handler:
       type: boolean
-      label: 'Taxonomy Term Path Handler'
\ No newline at end of file
+      label: 'Taxonomy Term Path Handler'
diff --git a/modules/redirect_404/config/install/redirect_404.settings.yml b/modules/redirect_404/config/install/redirect_404.settings.yml
new file mode 100644
index 0000000..2bac39a
--- /dev/null
+++ b/modules/redirect_404/config/install/redirect_404.settings.yml
@@ -0,0 +1 @@
+row_limit: 10000
diff --git a/modules/redirect_404/config/install/views.view.redirect_404.yml b/modules/redirect_404/config/install/views.view.redirect_404.yml
new file mode 100644
index 0000000..1d6cd3c
--- /dev/null
+++ b/modules/redirect_404/config/install/views.view.redirect_404.yml
@@ -0,0 +1,523 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - redirect_404
+    - user
+id: redirect_404
+label: 'Redirect 404'
+module: views
+description: ''
+tag: ''
+base_table: redirect_404
+base_field: ''
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access site reports'
+      cache:
+        type: none
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Filter
+          reset_button: true
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: ‹‹
+            next: ››
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            path: path
+            count: count
+            timestamp: timestamp
+          info:
+            path:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            count:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            timestamp:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: count
+          empty_table: false
+      row:
+        type: fields
+      fields:
+        path:
+          table: redirect_404
+          field: path
+          id: path
+          entity_type: null
+          entity_field: null
+          plugin_id: standard
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Path
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+        count:
+          id: count
+          table: redirect_404
+          field: count
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Count
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          format: unserialized
+          key: ''
+          plugin_id: serialized
+        timestamp:
+          id: timestamp
+          table: redirect_404
+          field: timestamp
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Last accessed'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          date_format: short
+          custom_date_format: ''
+          timezone: ''
+          plugin_id: date
+        langcode:
+          id: langcode
+          table: redirect_404
+          field: langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Language
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          plugin_id: standard
+        redirect_404_operations:
+          id: redirect_404_operations
+          table: redirect_404
+          field: redirect_404_operations
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          plugin_id: redirect_404_operations
+      filters:
+        path:
+          id: path
+          table: redirect_404
+          field: path
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: contains
+          value: ''
+          group: 1
+          exposed: true
+          expose:
+            operator_id: path_op
+            label: Path
+            description: ''
+            use_operator: false
+            operator: path_op
+            identifier: path
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          plugin_id: string
+        langcode:
+          id: langcode
+          table: redirect_404
+          field: langcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: langcode_op
+            label: Language
+            description: ''
+            use_operator: false
+            operator: langcode_op
+            identifier: langcode
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          plugin_id: in_operator
+        resolved:
+          id: resolved
+          table: redirect_404
+          field: resolved
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: false
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: Resolved
+            description: ''
+            use_operator: false
+            operator: resolved_op
+            identifier: resolved
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          plugin_id: boolean
+      sorts: {  }
+      title: 'Fix 404 pages'
+      header: {  }
+      footer: {  }
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: true
+          tokenize: false
+          content: 'There are no 404 errors to fix.'
+          plugin_id: text_custom
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: admin/config/search/redirect/404
+      display_description: 'Lists 404 error paths with no redirect assigned yet.'
+      enabled: true
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user.permissions
+      tags: {  }
diff --git a/modules/redirect_404/config/schema/redirect_404.schema.yml b/modules/redirect_404/config/schema/redirect_404.schema.yml
new file mode 100644
index 0000000..b3d0cf3
--- /dev/null
+++ b/modules/redirect_404/config/schema/redirect_404.schema.yml
@@ -0,0 +1,9 @@
+# Schema for the configuration files of the redirect_404 module.
+
+redirect_404.settings:
+  type: config_object
+  label: '404 error database logging settings.'
+  mapping:
+    row_limit:
+      type: integer
+      label: '404 error database logs to keep.'
diff --git a/modules/redirect_404/config/schema/redirect_404.views.schema.yml b/modules/redirect_404/config/schema/redirect_404.views.schema.yml
new file mode 100644
index 0000000..5798b98
--- /dev/null
+++ b/modules/redirect_404/config/schema/redirect_404.views.schema.yml
@@ -0,0 +1,7 @@
+views.field.redirect_404_operations:
+  type: views_field
+  label: 'Redirect 404 operations'
+  mapping:
+    text:
+      type: label
+      label: 'Redirect 404 operations'
diff --git a/modules/redirect_404/redirect_404.info.yml b/modules/redirect_404/redirect_404.info.yml
new file mode 100644
index 0000000..b946582
--- /dev/null
+++ b/modules/redirect_404/redirect_404.info.yml
@@ -0,0 +1,9 @@
+name: 'Redirect 404'
+type: module
+description: 'Logs 404 errors and allows users to create redirects for often requested but missing pages.'
+core: 8.x
+
+dependencies:
+ - redirect
+ - language
+ - views
diff --git a/modules/redirect_404/redirect_404.install b/modules/redirect_404/redirect_404.install
new file mode 100644
index 0000000..6b11060
--- /dev/null
+++ b/modules/redirect_404/redirect_404.install
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Update hooks for the redirect_404 module.
+ */
+
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Implements hook_schema().
+ */
+function redirect_404_schema() {
+  $schema['redirect_404'] = [
+    'description' => 'Stores 404 requests.',
+    'fields' => [
+      'path' => [
+        'description' => 'The path of the request.',
+        'type' => 'varchar',
+        'length' => 191,
+        'not null' => TRUE,
+      ],
+      'langcode' => [
+        'description' => 'The language of this request.',
+        'type' => 'varchar_ascii',
+        'length' => 12,
+        'not null' => TRUE,
+        'default' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      ],
+      'count' => [
+        'description' => 'The number of requests with that path and language.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'timestamp' => [
+        'description' => 'The timestamp of the last request with that path and language.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'resolved' => [
+        'description' => 'Boolean indicating whether or not this path has a redirect assigned.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+      'relevancy' => [
+        'description' => 'A float number that defines the relevancy of a record.',
+        'type' => 'float',
+        'not null' => TRUE,
+        'default' => 1.00,
+      ],
+    ],
+    'primary key' => ['path', 'langcode'],
+  ];
+  return $schema;
+}
diff --git a/modules/redirect_404/redirect_404.module b/modules/redirect_404/redirect_404.module
new file mode 100644
index 0000000..bdc5ca0
--- /dev/null
+++ b/modules/redirect_404/redirect_404.module
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Module file for redirect_404.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\redirect\Entity\Redirect;
+
+/**
+ * Implements hook_cron().
+ *
+ * Adds clean up job to drop the irrelevant rows from the redirect_404 table.
+ */
+function redirect_404_cron() {
+  /** @var \Drupal\redirect_404\SqlRedirectNotFoundStorage $redirect_storage */
+  $redirect_storage = \Drupal::service('redirect.not_found_storage');
+  $redirect_storage->purgeOldRequests();
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for system_logging_settings().
+ */
+function redirect_404_form_system_logging_settings_alter(&$form, FormStateInterface $form_state) {
+  $row_limits = [100, 1000, 10000, 100000, 1000000];
+  $form['redirect_404_row_limit'] = [
+    '#type' => 'select',
+    '#title' => t('404 error database logs to keep'),
+    '#default_value' => \Drupal::configFactory()->getEditable('redirect_404.settings')->get('row_limit'),
+    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
+    '#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => \Drupal::url('system.status')])
+  ];
+
+  $form['#submit'][] = 'redirect_404_logging_settings_submit';
+}
+
+/**
+ * Form submission handler for system_logging_settings().
+ *
+ * @see redirect_404_form_system_logging_settings_alter()
+ */
+function redirect_404_logging_settings_submit($form, FormStateInterface $form_state) {
+  \Drupal::configFactory()
+    ->getEditable('redirect_404.settings')
+    ->set('row_limit', $form_state->getValue('redirect_404_row_limit'))
+    ->save();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_presave() for redirect entities.
+ */
+function redirect_404_redirect_presave(Redirect $redirect) {
+  $path = $redirect->getSourcePathWithQuery();
+  $langcode = $redirect->get('language')->value;
+
+  // Mark a potentially existing log entry for this path as resolved.
+  \Drupal::service('redirect.not_found_storage')->resolveLogRequest($path, $langcode);
+}
diff --git a/modules/redirect_404/redirect_404.routing.yml b/modules/redirect_404/redirect_404.routing.yml
new file mode 100644
index 0000000..b019ae8
--- /dev/null
+++ b/modules/redirect_404/redirect_404.routing.yml
@@ -0,0 +1,7 @@
+redirect.fix_404:
+  path: '/admin/config/search/redirect/404'
+  defaults:
+    _title: 'Fix 404 pages'
+    _form: '\Drupal\redirect_404\Form\RedirectFix404Form'
+  requirements:
+    _permission: 'administer redirects'
diff --git a/modules/redirect_404/redirect_404.services.yml b/modules/redirect_404/redirect_404.services.yml
new file mode 100644
index 0000000..46c2cca
--- /dev/null
+++ b/modules/redirect_404/redirect_404.services.yml
@@ -0,0 +1,11 @@
+services:
+  redirect.404_subscriber:
+    class: Drupal\redirect_404\EventSubscriber\Redirect404Subscriber
+    arguments: ['@path.current', '@request_stack', '@language_manager', '@redirect.not_found_storage']
+    tags:
+      - { name: event_subscriber }
+  redirect.not_found_storage:
+    class: Drupal\redirect_404\SqlRedirectNotFoundStorage
+    arguments: ['@database']
+    tags:
+      - { name: backend_overridable }
diff --git a/modules/redirect_404/redirect_404.views.inc b/modules/redirect_404/redirect_404.views.inc
new file mode 100644
index 0000000..bc18f93
--- /dev/null
+++ b/modules/redirect_404/redirect_404.views.inc
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Provide views data for redirect_404.module.
+ */
+
+use Drupal\redirect_404\SqlRedirectNotFoundStorage;
+
+/**
+ * Implements hook_views_data().
+ */
+function redirect_404_views_data() {
+  $data = [];
+
+  // Only define views data if the service uses our specific implementation.
+  if (!\Drupal::service('redirect.not_found_storage') instanceof SqlRedirectNotFoundStorage) {
+    return $data;
+  }
+
+  $data['redirect_404']['table']['group'] = t('Redirect 404');
+
+  $data['redirect_404']['table']['base'] = [
+    'field' => '',
+    'title' => t('Fix 404 pages'),
+    'help' => t('Overview for 404 error paths with no redirect assigned yet.'),
+  ];
+
+  $data['redirect_404']['path'] = [
+    'title' => t('Path'),
+    'help' => t('The path of the request.'),
+    'field' => [
+      'id' => 'standard',
+    ],
+    'filter' => [
+      'id' => 'string',
+    ],
+  ];
+
+  $data['redirect_404']['langcode'] = [
+    'title' => t('Language'),
+    'help' => t('The language of this request.'),
+    'field' => [
+      'id' => 'redirect_404_langcode',
+    ],
+    'filter' => [
+      'id' => 'language',
+    ],
+  ];
+
+  $data['redirect_404']['count'] = [
+    'title' => t('Count'),
+    'help' => t('The number of requests with that path and language.'),
+    'field' => [
+      'id' => 'numeric',
+      'click sortable' => TRUE,
+    ],
+    'filter' => [
+      'id' => 'numeric',
+    ],
+  ];
+
+  $data['redirect_404']['timestamp'] = [
+    'title' => t('Timestamp'),
+    'help' => t('The timestamp of the last request with that path and language.'),
+    'field' => [
+      'id' => 'date',
+      'click sortable' => TRUE,
+    ],
+    'filter' => [
+      'id' => 'date',
+    ],
+  ];
+
+  $data['redirect_404']['resolved'] = [
+    'title' => t('Resolved'),
+    'help' => t('Whether or not this path has a redirect assigned.'),
+    'field' => [
+      'id' => 'boolean',
+    ],
+    'filter' => [
+      'id' => 'boolean',
+      'label' => t('Resolved'),
+      'use_equal' => TRUE,
+    ],
+  ];
+
+  $data['redirect_404']['redirect_404_operations'] = [
+    'title' => t('Operations'),
+    'help' => t('Provide operation buttons to handle the 404 path.'),
+    'field' => [
+      'id' => 'redirect_404_operations',
+      'additional fields' => ['path', 'langcode'],
+      'real field' => 'path',
+    ],
+  ];
+
+  return $data;
+}
diff --git a/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
new file mode 100644
index 0000000..1e43e73
--- /dev/null
+++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\redirect_404\EventSubscriber;
+
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\redirect_404\RedirectNotFoundStorageInterface;
+use Drupal\Core\Path\CurrentPathStack;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * An EventSubscriber that listens to redirect 404 errors.
+ */
+class Redirect404Subscriber implements EventSubscriberInterface {
+
+  /**
+   * The current path.
+   *
+   * @var \Drupal\Core\Path\CurrentPathStack
+   */
+  protected $currentPath;
+
+  /**
+   * The request stack (get the URL argument(s) and combined it with the path).
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The redirect storage.
+   *
+   * @var \Drupal\redirect_404\RedirectNotFoundStorageInterface
+   */
+  protected $redirectStorage;
+
+  /**
+   * Constructs a new Redirect404Subscriber.
+   *
+   * @param \Drupal\Core\Path\CurrentPathStack $current_path
+   *   The current path.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\redirect_404\RedirectNotFoundStorageInterface $redirect_storage
+   *   A redirect storage.
+   */
+  public function __construct(CurrentPathStack $current_path, RequestStack $request_stack, LanguageManagerInterface $language_manager, RedirectNotFoundStorageInterface $redirect_storage) {
+    $this->currentPath = $current_path;
+    $this->requestStack = $request_stack;
+    $this->languageManager = $language_manager;
+    $this->redirectStorage = $redirect_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::EXCEPTION][] = 'onKernelException';
+    return $events;
+  }
+
+  /**
+   * Logs an exception of 404 Redirect errors.
+   *
+   * @param GetResponseForExceptionEvent $event
+   *   Is given by the event dispatcher.
+   */
+  public function onKernelException(GetResponseForExceptionEvent $event) {
+    // Only log page not found (404) errors.
+    if ($event->getException() instanceof NotFoundHttpException) {
+
+      // Allow to store paths with arguments.
+      if ($query_string = $this->requestStack->getCurrentRequest()->getQueryString()) {
+        $query_string = '?' . $query_string;
+      }
+      $path = $this->currentPath->getPath() . $query_string;
+      $langcode = $this->languageManager->getCurrentLanguage()->getId();
+
+      // Write record.
+      $this->redirectStorage->logRequest($path, $langcode);
+    }
+  }
+
+}
diff --git a/modules/redirect_404/src/Form/RedirectFix404Form.php b/modules/redirect_404/src/Form/RedirectFix404Form.php
new file mode 100644
index 0000000..484f14f
--- /dev/null
+++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\redirect_404\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a form that lists all 404 error paths and no redirect assigned yet.
+ *
+ * Keep this form, if they want to implement different backends to better fit
+ * Redis or MongoDB's capped collections. Otherwise drop this class and the
+ * SqlRedirectNotFoundStorage::listRequests() method. See discussion in
+ * #1559310 comment #55.
+ */
+class RedirectFix404Form extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'redirect_fix_404_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $destination = $this->getDestinationArray();
+
+    $search = $this->getRequest()->get('search');
+    $form['#attributes'] = ['class' => ['search-form']];
+
+    $form['basic'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Filter 404s'),
+      '#attributes' => ['class' => ['container-inline']],
+    ];
+    $form['basic']['filter'] = [
+      '#type' => 'textfield',
+      '#title' => '',
+      '#default_value' => $search,
+      '#maxlength' => 128,
+      '#size' => 25,
+    ];
+    $form['basic']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Filter'),
+      '#action' => 'filter',
+    ];
+    if ($search) {
+      $form['basic']['reset'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Reset'),
+        '#action' => 'reset',
+      ];
+    }
+
+    $languages = \Drupal::languageManager()->getLanguages(LanguageInterface::STATE_ALL);
+    $multilingual = \Drupal::languageManager()->isMultilingual();
+
+    $header = [
+      ['data' => $this->t('Path'), 'field' => 'source'],
+      ['data' => $this->t('Count'), 'field' => 'count', 'sort' => 'desc'],
+      ['data' => $this->t('Last accessed'), 'field' => 'timestamp'],
+    ];
+    if ($multilingual) {
+      $header[] = ['data' => $this->t('Language'), 'field' => 'language'];
+    }
+    $header[] = ['data' => $this->t('Operations')];
+
+    /** @var \Drupal\redirect_404\SqlRedirectNotFoundStorage $redirect_storage */
+    $redirect_storage = \Drupal::service('redirect.not_found_storage');
+    $results = $redirect_storage->listRequests($header, $search);
+
+    $rows = [];
+    foreach ($results as $result) {
+      $path = ltrim($result->path, '/');
+
+      $row = [];
+      $row['source'] = $path;
+      $row['count'] = $result->count;
+      $row['timestamp'] = \Drupal::service('date.formatter')->format($result->timestamp, 'short');
+      if ($multilingual) {
+        if (isset($languages[$result->langcode])) {
+          $row['language'] = $languages[$result->langcode]->getName();
+        }
+        else {
+          $row['language'] = $this->t('Undefined @langcode', ['@langcode' => $result->langcode]);
+        }
+      }
+
+      $operations = [];
+      if (\Drupal::entityTypeManager()->getAccessControlHandler('redirect')->createAccess()) {
+        $operations['add'] = [
+          'title' => $this->t('Add redirect'),
+          'url' => Url::fromRoute('redirect.add', [], ['query' => ['source' => $path, 'language' => $result->langcode] + $destination]),
+        ];
+      }
+      $row['operations'] = [
+        'data' => [
+          '#type' => 'operations',
+          '#links' => $operations,
+        ],
+      ];
+
+      $rows[] = $row;
+    }
+
+    $form['redirect_404_table']  = [
+      '#theme' => 'table',
+      '#header' => $header,
+      '#rows' => $rows,
+      '#empty' => $this->t('There are no 404 errors to fix.'),
+    ];
+    $form['redirect_404_pager'] = ['#type' => 'pager'];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+
+    if ($form_state->getTriggeringElement()['#action'] == 'filter') {
+      $form_state->setRedirect('redirect.fix_404', [], ['query' => ['search' => trim($form_state->getValue('filter'))]]);
+    }
+    else {
+      $form_state->setRedirect('redirect.fix_404');
+    }
+  }
+
+}
diff --git a/modules/redirect_404/src/Plugin/views/field/Language.php b/modules/redirect_404/src/Plugin/views/field/Language.php
new file mode 100644
index 0000000..3776ee8
--- /dev/null
+++ b/modules/redirect_404/src/Plugin/views/field/Language.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\redirect_404\Plugin\views\field;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\views\Plugin\views\field\LanguageField;
+
+/**
+ * Provides a views field for the 404 error language.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("redirect_404_langcode")
+ */
+class Language extends LanguageField {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account) {
+    return \Drupal::languageManager()->isMultilingual();
+  }
+
+}
diff --git a/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php b/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php
new file mode 100644
index 0000000..802f929
--- /dev/null
+++ b/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\redirect_404\Plugin\views\field;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
+use Drupal\views\ResultRow;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a views field for the redirect operation buttons.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("redirect_404_operations")
+ */
+class Redirect404Operations extends FieldPluginBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructor for the redirect operations view field.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clickSortable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(ResultRow $values) {
+    $link = [];
+    $query = [
+      'query' => [
+        'source' => ltrim($this->getValue($values, 'path'), '/'),
+        'language' => $this->getValue($values, 'langcode'),
+        'destination' => $this->view->getPath(),
+      ]
+    ];
+    $link['add'] = [
+      'title' => $this->t('Add redirect'),
+      'url' => Url::fromRoute('redirect.add', [], $query),
+    ];
+
+    $operations['data'] = [
+      '#type' => 'operations',
+      '#links' => $link,
+    ];
+    return $this->renderer->render($operations);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account) {
+    return $this->entityTypeManager->getAccessControlHandler('redirect')->createAccess();
+  }
+
+}
diff --git a/modules/redirect_404/src/RedirectNotFoundStorageInterface.php b/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
new file mode 100644
index 0000000..b158e9b
--- /dev/null
+++ b/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\redirect_404;
+
+/**
+ * Interface for redirect 404 services.
+ */
+interface RedirectNotFoundStorageInterface {
+
+  /**
+   * Merges a 404 request log in the database.
+   *
+   * @param string $path
+   *   The path of the current request.
+   * @param string $langcode
+   *   The ID of the language code.
+   */
+  public function logRequest($path, $langcode);
+
+  /**
+   * Marks a 404 request log as resolved.
+   *
+   * @param string $path
+   *   The path of the current request.
+   * @param string $langcode
+   *   The ID of the language code.
+   */
+  public function resolveLogRequest($path, $langcode);
+
+  /**
+   * Returns the 404 request data.
+   *
+   * @param array $header
+   *   An array containing arrays of the redirect_404 fields data.
+   * @param string $search
+   *   The search text. It is possible to have multiple '*' as a wildcard.
+   *
+   * @return array
+   *   A list of objects with the properties:
+   *   - path
+   *   - count
+   *   - timestamp
+   *   - langcode
+   *   - resolved
+   *   - relevancy
+   */
+  public function listRequests(array $header = [], $search = NULL);
+
+  /**
+   * Cleans the irrelevant 404 request logs.
+   */
+  public function purgeOldRequests();
+
+}
diff --git a/modules/redirect_404/src/SqlRedirectNotFoundStorage.php b/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
new file mode 100644
index 0000000..9c2a947
--- /dev/null
+++ b/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\redirect_404;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Provides an SQL implementation for redirect not found storage.
+ *
+ * To keep a limited amount of relevant records, we implement an exponential
+ * decay similar to the radioactivity process decay (see radioactivity module).
+ *
+ * The relevancy:
+ * - represents the relevancy for timestamp of each record
+ * - is recalculated for an individual record on each hit for current time
+ *
+ * To clean the records, we calculate the effective relevancy for NOW(),
+ * determine the relevancy cutoff value and drop the less relevant rows.
+ * The half life of relevancy is (float) 86400.00 = 1 day
+ * Each hit adds 1.
+ *
+ * Relevancy formula: $relevancy = 1 + $relevancy * $decay, where
+ * $decay = POW(2, - (NOW() - timestamp) / $halflife)
+ */
+class SqlRedirectNotFoundStorage implements RedirectNotFoundStorageInterface {
+
+  /**
+   * Active database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * Constructs a new SqlRedirectNotFoundStorage.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   A Database connection to use for reading and writing database data.
+   */
+  public function __construct(Connection $database) {
+    $this->database = $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function logRequest($path, $langcode) {
+    // If the request is not new, update its relevancy for the time interval
+    // since the last hit.
+    $this->database->merge('redirect_404')
+      ->key('path', $path)
+      ->key('langcode', $langcode)
+      ->expression('count', 'count + 1')
+      ->expression('relevancy', 'relevancy * pow(2, -( UNIX_TIMESTAMP(NOW()) - IFNULL( timestamp, UNIX_TIMESTAMP(NOW()) ) )/86400.00) + 1')
+      ->fields([
+        'timestamp' => REQUEST_TIME,
+        'count' => 1,
+        'resolved' => 0,
+        'relevancy' => 1.00,
+      ])
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resolveLogRequest($path, $langcode) {
+    $this->database->update('redirect_404')
+      ->fields(['resolved' => 1])
+      ->condition('path', $path)
+      ->condition('langcode', $langcode)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function purgeOldRequests() {
+    $row_limit = \Drupal::config('redirect_404.settings')->get('row_limit');
+    $cutoff_exp = '(relevancy * pow(2, -(UNIX_TIMESTAMP(NOW()) - timestamp)/86400.00))';
+
+    // Determine cutoff level to get the current min relevancy we want to keep.
+    $query = $this->database->select('redirect_404', 'r404');
+    $query->addExpression($cutoff_exp, 'cutoff');
+    $query->orderBy('cutoff', 'DESC');
+    $cutoff = $query->range($row_limit, 1)->execute()->fetchField();
+
+    // Delete records below cutoff value, if given. Otherwise skip the cleanup.
+    if (!empty($cutoff)) {
+      $this->database
+        ->delete('redirect_404')
+        ->where( $cutoff_exp . ' < :cutoff', [':cutoff' => $cutoff])
+        ->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function listRequests(array $header = [], $search = NULL) {
+    $query = $this->database
+      ->select('redirect_404', 'r404')
+      ->extend('Drupal\Core\Database\Query\TableSortExtender')
+      ->orderByHeader($header)
+      ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
+      ->limit(25)
+      ->fields('r404');
+
+    if ($search) {
+      // Replace wildcards with PDO wildcards.
+      // @todo Find a way to write a nicer pattern.
+      $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
+      $query->condition('path', $wildcard, 'LIKE');
+    }
+    $results = $query->condition('resolved', 0, '=')->execute()->fetchAll();
+
+    return $results;
+  }
+
+}
diff --git a/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
new file mode 100644
index 0000000..79ba9eb
--- /dev/null
+++ b/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace Drupal\redirect_404\Tests;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Url;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\redirect\Tests\AssertRedirectTrait;
+
+/**
+ * UI tests for redirect_404 module with language and content translation.
+ *
+ * This runs the exact same tests as Fix404RedirectUITest, but with both
+ * language and content translation modules enabled.
+ *
+ * @group redirect_404
+ */
+class Fix404RedirectUILanguageTest extends Redirect404TestBase {
+
+  use AssertRedirectTrait;
+
+  /**
+   * Additional modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['content_translation'];
+
+  /**
+   * Admin user's permissions for this test.
+   *
+   * @var array
+   */
+  protected $adminPermissions = [
+    'administer redirects',
+    'access site reports',
+    'access content',
+    'bypass node access',
+    'create url aliases',
+    'administer url aliases',
+    'administer languages',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Enable some languages for this test.
+    $language = ConfigurableLanguage::createFromLangcode('de');
+    $language->save();
+    $language = ConfigurableLanguage::createFromLangcode('es');
+    $language->save();
+    $language = ConfigurableLanguage::createFromLangcode('fr');
+    $language->save();
+  }
+
+  /**
+   * Tests the fix 404 pages workflow with language and content translation.
+   */
+  public function testFix404RedirectList() {
+    // Visit a non existing page to have the 404 redirect_error entry.
+    $this->drupalGet('fr/testing');
+
+    $redirect = db_select('redirect_404')
+      ->fields('redirect_404')
+      ->condition('path', '/testing')
+      ->execute()
+      ->fetchAll();
+    if (count($redirect) == 0) {
+      $this->fail('No record was added');
+    }
+
+    // Go to the "fix 404" page and check the listing.
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('testing');
+    $this->assertLanguageInTableBody('French');
+    // Check the Language view filter uses the default language filter.
+    $this->assertOption('edit-langcode', 'All');
+    $this->assertOption('edit-langcode', 'en');
+    $this->assertOption('edit-langcode', 'de');
+    $this->assertOption('edit-langcode', 'es');
+    $this->assertOption('edit-langcode', 'fr');
+    $this->assertOption('edit-langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
+    $this->clickLink(t('Add redirect'));
+
+    // Check if we generate correct Add redirect url and if the form is
+    // pre-filled.
+    $destination = Url::fromRoute('redirect.fix_404')->getInternalPath();
+    $options = [
+      'query' => [
+        'source' => 'testing',
+        'language' => 'fr',
+        'destination' => $destination,
+      ]
+    ];
+    $this->assertUrl('admin/config/search/redirect/add', $options);
+    $this->assertFieldByName('redirect_source[0][path]', 'testing');
+    $this->assertOptionSelected('edit-language-0-value', 'fr');
+    // Save the redirect.
+    $edit = ['redirect_redirect[0][uri]' => '/node'];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/config/search/redirect/404');
+    $this->assertText('There are no 404 errors to fix.');
+    // Check if the redirect works as expected.
+    $this->assertRedirect('fr/testing', 'fr/node', 'HTTP/1.1 301 Moved Permanently');
+
+    // Test removing a redirect assignment, visit again the non existing page.
+    $this->drupalGet('admin/config/search/redirect');
+    $this->assertText('testing');
+    $this->assertLanguageInTableBody('French');
+    $this->clickLink('Delete', 0);
+    $this->drupalPostForm(NULL, [], 'Delete');
+    $this->assertUrl('admin/config/search/redirect');
+    $this->assertText('There is no redirect yet.');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('There are no 404 errors to fix.');
+    // Should be listed again in the 404 overview.
+    $this->drupalGet('fr/testing');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertLanguageInTableBody('French');
+    // Check the error path visit count.
+    $this->assertFieldByXPath('//table/tbody/tr/td[2]', 2);
+    $this->clickLink('Add redirect');
+    // Save the redirect with a different langcode.
+    $this->assertFieldByName('redirect_source[0][path]', 'testing');
+    $this->assertOptionSelected('edit-language-0-value', 'fr');
+    $edit['language[0][value]'] = 'es';
+    $this->drupalPostForm(NULL, $edit, 'Save');
+    $this->assertUrl('admin/config/search/redirect/404');
+    // Should still be listed, redirecting to another language does not resolve
+    // the path.
+    $this->assertLanguageInTableBody('French');
+    $this->drupalGet('admin/config/search/redirect');
+    $this->assertLanguageInTableBody('Spanish');
+    // Check if the redirect works as expected.
+    $this->assertRedirect('es/testing', 'es/node', 'HTTP/1.1 301 Moved Permanently');
+
+    // Visit multiple non existing pages to test the Redirect 404 View.
+    $this->drupalGet('testing1');
+    $this->drupalGet('de/testing2');
+    $this->drupalGet('de/testing2?test=1');
+    $this->drupalGet('de/testing2?test=2');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertLanguageInTableBody('French');
+    $this->assertLanguageInTableBody('English');
+    $this->assertLanguageInTableBody('German');
+    $this->assertText('testing1');
+    $this->assertText('testing2');
+    $this->assertText('testing2?test=1');
+    $this->assertText('testing2?test=2');
+
+    // Test the Language view filter.
+    $this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'de']]);
+    $this->assertText('English');
+    $this->assertNoLanguageInTableBody('English');
+    $this->assertLanguageInTableBody('German');
+    $this->assertNoText('testing1');
+    $this->assertText('testing2');
+    $this->assertText('testing2?test=1');
+    $this->assertText('testing2?test=2');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertLanguageInTableBody('English');
+    $this->assertLanguageInTableBody('German');
+    $this->assertText('testing1');
+    $this->assertText('testing2');
+    $this->assertText('testing2?test=1');
+    $this->assertText('testing2?test=2');
+    $this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'en']]);
+    $this->assertLanguageInTableBody('English');
+    $this->assertNoLanguageInTableBody('German');
+    $this->assertText('testing1');
+    $this->assertNoText('testing2');
+    $this->assertNoText('testing2?test=1');
+    $this->assertNoText('testing2?test=2');
+
+    // Assign a redirect to 'testing1'.
+    $this->clickLink('Add redirect');
+    $options = [
+      'query' => [
+        'source' => 'testing1',
+        'language' => 'en',
+        'destination' => $destination,
+      ]
+    ];
+    $this->assertUrl('admin/config/search/redirect/add', $options);
+    $this->assertFieldByName('redirect_source[0][path]', 'testing1');
+    $this->assertOptionSelected('edit-language-0-value', 'en');
+    $edit = ['redirect_redirect[0][uri]' => '/node'];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/config/search/redirect/404');
+    $this->assertNoLanguageInTableBody('English');
+    $this->assertLanguageInTableBody('German');
+    $this->drupalGet('admin/config/search/redirect');
+    $this->assertLanguageInTableBody('Spanish');
+    $this->assertLanguageInTableBody('English');
+    // Check if the redirect works as expected.
+    $this->assertRedirect('/testing1', '/node', 'HTTP/1.1 301 Moved Permanently');
+  }
+
+}
diff --git a/modules/redirect_404/src/Tests/Fix404RedirectUITest.php b/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
new file mode 100644
index 0000000..200a1be
--- /dev/null
+++ b/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\redirect_404\Tests;
+
+use Drupal\Core\Url;
+
+/**
+ * UI tests for redirect_404 module.
+ *
+ * @group redirect_404
+ */
+class Fix404RedirectUITest extends Redirect404TestBase {
+
+  /**
+   * Tests the fix 404 pages workflow.
+   */
+  public function testFix404Pages() {
+    // Visit a non existing page to have the 404 redirect_error entry.
+    $this->drupalGet('non-existing0');
+
+    // Go to the "fix 404" page and check the listing.
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('non-existing0');
+    $this->clickLink(t('Add redirect'));
+
+    // Check if we generate correct Add redirect url and if the form is
+    // pre-filled.
+    $destination = Url::fromRoute('redirect.fix_404')->getInternalPath();
+    $options = [
+      'query' => [
+        'source' => 'non-existing0',
+        'language' => 'en',
+        'destination' => $destination,
+      ]
+    ];
+    $this->assertUrl('admin/config/search/redirect/add', $options);
+    $this->assertFieldByName('redirect_source[0][path]', 'non-existing0');
+    // Save the redirect.
+    $edit = ['redirect_redirect[0][uri]' => '/node'];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/config/search/redirect/404');
+    $this->assertText('There are no 404 errors to fix.');
+    // Check if the redirect works as expected.
+    $this->drupalGet('non-existing0');
+    $this->assertUrl('node');
+
+    // Test removing a redirect assignment, visit again the non existing page.
+    $this->drupalGet('admin/config/search/redirect');
+    $this->assertText('non-existing0');
+    $this->clickLink('Delete', 0);
+    $this->drupalPostForm(NULL, [], 'Delete');
+    $this->assertUrl('admin/config/search/redirect');
+    $this->assertText('There is no redirect yet.');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('There are no 404 errors to fix.');
+    // Should be listed again in the 404 overview.
+    $this->drupalGet('non-existing0');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('non-existing0');
+
+    // Visit multiple non existing pages to test the Redirect 404 View.
+    $this->drupalGet('non-existing0?test=1');
+    $this->drupalGet('non-existing0?test=2');
+    $this->drupalGet('non-existing1');
+    $this->drupalGet('non-existing2');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('non-existing0?test=1');
+    $this->assertText('non-existing0?test=2');
+    $this->assertText('non-existing0');
+    $this->assertText('non-existing1');
+    $this->assertText('non-existing2');
+
+    // Test the Path view filter.
+    $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'test=']]);
+    $this->assertText('non-existing0?test=1');
+    $this->assertText('non-existing0?test=2');
+    $this->assertNoText('non-existing1');
+    $this->assertNoText('non-existing2');
+    $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'existing1']]);
+    $this->assertNoText('non-existing0?test=1');
+    $this->assertNoText('non-existing0?test=2');
+    $this->assertNoText('non-existing0');
+    $this->assertText('non-existing1');
+    $this->assertNoText('non-existing2');
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('non-existing0?test=1');
+    $this->assertText('non-existing0?test=2');
+    $this->assertText('non-existing0');
+    $this->assertText('non-existing1');
+    $this->assertText('non-existing2');
+    $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'g2']]);
+    $this->assertNoText('non-existing0?test=1');
+    $this->assertNoText('non-existing0?test=2');
+    $this->assertNoText('non-existing0');
+    $this->assertNoText('non-existing1');
+    $this->assertText('non-existing2');
+
+    // Assign a redirect to 'non-existing2'.
+    $this->clickLink('Add redirect');
+    $options = [
+      'query' => [
+        'source' => 'non-existing2',
+        'language' => 'en',
+        'destination' => $destination,
+      ]
+    ];
+    $this->assertUrl('admin/config/search/redirect/add', $options);
+    $this->assertFieldByName('redirect_source[0][path]', 'non-existing2');
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertUrl('admin/config/search/redirect/404');
+    $this->assertText('non-existing0?test=1');
+    $this->assertText('non-existing0?test=2');
+    $this->assertText('non-existing0');
+    $this->assertText('non-existing1');
+    $this->assertNoText('non-existing2');
+    // Check if the redirect works as expected.
+    $this->drupalGet('admin/config/search/redirect');
+    $this->assertText('non-existing2');
+  }
+
+}
diff --git a/modules/redirect_404/src/Tests/Redirect404TestBase.php b/modules/redirect_404/src/Tests/Redirect404TestBase.php
new file mode 100644
index 0000000..f0be2b0
--- /dev/null
+++ b/modules/redirect_404/src/Tests/Redirect404TestBase.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Drupal\redirect_404\Tests;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * This class provides methods specifically for testing redirect 404 paths.
+ */
+abstract class Redirect404TestBase extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'redirect_404',
+    'node',
+    'path',
+    'dblog',
+  ];
+
+  /**
+   * Permissions for the admin user.
+   *
+   * @var array
+   */
+  protected $adminPermissions = [
+    'administer redirects',
+    'access site reports',
+    'access content',
+    'bypass node access',
+    'create url aliases',
+    'administer url aliases',
+  ];
+
+  /**
+   * A user with administrative permissions.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create an admin user.
+    $this->adminUser = $this->drupalCreateUser($this->adminPermissions);
+    $this->drupalLogin($this->adminUser);
+  }
+
+  /**
+   * Passes if the language of the 404 path IS found on the loaded page.
+   *
+   * Because assertText() checks also in the Language select options, this
+   * specific assertion in the redirect 404 table body is needed.
+   *
+   * @param string $language
+   *   The language to assert in the redirect 404 table body.
+   * @param string $body
+   *   (optional) The table body xpath where to assert the language. Defaults
+   *   to '//table/tbody'.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertLanguageInTableBody($language, $body = '//table/tbody', $message = '') {
+    return $this->assertLanguageInTableBodyHelper($language, $body, $message, FALSE);
+  }
+
+  /**
+   * Passes if the language of the 404 path is NOT found on the loaded page.
+   *
+   * Because assertText() checks also in the Language select options, this
+   * specific assertion in the redirect 404 table body is needed.
+   *
+   * @param string $language
+   *   The language to assert in the redirect 404 table body.
+   * @param string $body
+   *   (optional) The table body xpath where to assert the language. Defaults
+   *   to '//table/tbody'.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertNoLanguageInTableBody($language, $body = '//table/tbody', $message = '') {
+    return $this->assertLanguageInTableBodyHelper($language, $body, $message, TRUE);
+  }
+
+  /**
+   * Helper for assertLanguageInTableBody and assertNoLanguageInTableBody.
+   *
+   * @param array $language
+   *   The language to assert in the redirect 404 table body.
+   * @param string $body
+   *   (optional) The table body xpath where to assert the language. Defaults
+   *   to '//table/tbody'.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
+   *   variables in the message text, not t(). If left blank, a default message
+   *   will be displayed.
+   * @param bool $not_exists
+   *   (optional) TRUE if this language should not exist, FALSE if it should.
+   *   Defaults to TRUE.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertLanguageInTableBodyHelper($language, $body = '//table/tbody', $message = '', $not_exists = TRUE) {
+    if (!$message) {
+      if (!$not_exists) {
+        $message = new FormattableMarkup('Language "@language" found in 404 table.', ['@language' => $language]);
+      }
+      else {
+        $message = new FormattableMarkup('Language "@language" not found in 404 table.', ['@language' => $language]);
+      }
+    }
+
+    if ($not_exists) {
+      return $this->assertFalse(strpos($this->xpath($body)[0]->asXML(), $language), $message);
+    }
+    else {
+      return $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), $language), $message);
+    }
+  }
+
+}
diff --git a/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php b/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
new file mode 100644
index 0000000..bc6d992
--- /dev/null
+++ b/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Drupal\Tests\redirect_404\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the clean up cron job for redirect_404.
+ *
+ * @group redirect_404
+ */
+class Fix404RedirectCronJobTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['redirect_404'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installSchema('redirect_404', 'redirect_404');
+
+    // Set the limit to 5 just for the test.
+    \Drupal::configFactory()
+      ->getEditable('redirect_404.settings')
+      ->set('row_limit', 5)
+      ->save();
+  }
+
+  /**
+   * Tests adding and deleting rows form redirect_404 table.
+   */
+  function testRedirect404CronJob() {
+    // Insert some records in the redirect test table with hardcoded relevancy
+    // for each row to not delete all rows after running the cronjob.
+    $this->insert404Row('/test5', 'en', 91.97781);
+    $this->insert404Row('/test6', 'en', 82.63037);
+    $this->insert404Row('/test2', 'en', 71.79651);
+    $this->insert404Row('/test7', 'en', 67.85126);
+    $this->insert404Row('/test4', 'en', 53.98305);
+    $this->insert404Row('/test3', 'en', 42.81542);
+    // The following rows have smaller relevancy than the cutoff value defined
+    // by the row_limit and will be dropped.
+    $this->insert404Row('/test9', 'en', 37.99983);
+    $this->insert404Row('/test1', 'en', 28.99964);
+    $this->insert404Row('/test0', 'en', 15.79511);
+    $this->insert404Row('/test8', 'en', 3.929704);
+
+    // Check that there are 10 rows in the redirect_404 table.
+    $result = db_query("SELECT COUNT(*) as rows FROM {redirect_404}")->fetchField();
+    $this->assertEquals(10, $result);
+
+    // Run cron to drop 4 rows from the redirect_404 test table.
+    redirect_404_cron();
+
+    // Check that there are only the first 6 rows in the redirect_404 table.
+    $this->assertNo404Row('/test0');
+    $this->assertNo404Row('/test1');
+    $this->assert404Row('/test2');
+    $this->assert404Row('/test3');
+    $this->assert404Row('/test4');
+    $this->assert404Row('/test5');
+    $this->assert404Row('/test6');
+    $this->assert404Row('/test7');
+    $this->assertNo404Row('/test8');
+    $this->assertNo404Row('/test9');
+  }
+
+  /**
+   * Inserts a 404 request log in the redirect_404 test table.
+   *
+   * @param string $path
+   *   The path of the request.
+   * @param string $langcode
+   *   (optional) The langcode of the request.
+   * @param float $relevancy
+   *   (optional) The relevancy of this path.
+   */
+  protected function insert404Row($path, $langcode = 'en', $relevancy = 1.00) {
+    db_insert('redirect_404')
+    ->fields([
+      'path' => $path,
+      'langcode' => $langcode,
+      'count' => 1,
+      'timestamp' => 1465082407,
+      'resolved' => 0,
+      'relevancy' => $relevancy,
+    ])
+    ->execute();
+  }
+
+  /**
+   * Passes if the row with the given parameters is in the redirect_404 table.
+   *
+   * @param string $path
+   *   The path of the request.
+   * @param string $langcode
+   *   (optional) The langcode of the request.
+   * @param int $resolved
+   *   (optional) Boolean indicating if this path is already resolved or not.
+   */
+  protected function assert404Row($path, $langcode = 'en', $resolved = 0) {
+    $this->assert404RowHelper($path, $langcode, $resolved, FALSE);
+  }
+
+  /**
+   * Passes if the row with the given parameters is NOT in the redirect_404 table.
+   *
+   * @param string $path
+   *   The path of the request.
+   * @param string $langcode
+   *   (optional) The langcode of the request.
+   * @param int $resolved
+   *   (optional) Integer indicating if this path is already resolved or not.
+   */
+  protected function assertNo404Row($path, $langcode = 'en', $resolved = 0) {
+    $this->assert404RowHelper($path, $langcode, $resolved, TRUE);
+  }
+
+  /**
+   * Passes if the row with the given parameters is in the redirect_404 table.
+   *
+   * @param string $path
+   *   The path of the request.
+   * @param string $langcode
+   *   (optional) The langcode of the request.
+   * @param int $resolved
+   *   (optional) Integer indicating if this path is already resolved or not.
+   * @param bool $not_exists
+   *   (optional) TRUE if this 404 row should not exist in the redirect_404
+   *   table, FALSE if it should. Defaults to TRUE.
+   */
+  protected function assert404RowHelper($path, $langcode = 'en', $resolved = 0, $not_exists = TRUE) {
+    $result = db_select('redirect_404', 'r404')
+      ->fields('r404', ['path'])
+      ->condition('path', $path)
+      ->condition('langcode', $langcode)
+      ->condition('resolved', $resolved)
+      ->execute()
+      ->fetchField();
+
+    if ($not_exists) {
+      $this->assertNotEquals($path, $result);
+    }
+    else {
+      $this->assertEquals($path, $result);
+    }
+  }
+}
diff --git a/redirect.install b/redirect.install
index 4acfd13..131411a 100644
--- a/redirect.install
+++ b/redirect.install
@@ -1,6 +1,7 @@
 <?php
 
 /**
+ * @file
  * Update hooks for the Redirect module.
  */
 
diff --git a/redirect.routing.yml b/redirect.routing.yml
index 36b433d..4384701 100644
--- a/redirect.routing.yml
+++ b/redirect.routing.yml
@@ -46,14 +46,6 @@ redirect.settings:
   requirements:
     _permission: 'administer redirects'
 
-redirect.fix_404:
-  path: '/admin/config/search/redirect/404'
-  defaults:
-    _title: 'Fix 404 pages'
-    _form: '\Drupal\redirect\Form\RedirectFix404Form'
-  requirements:
-    _permission: 'administer redirects'
-
 #redirect.devel_generate:
 #  requirements:
 #    _module_dependencies: 'devel'
diff --git a/src/Entity/Redirect.php b/src/Entity/Redirect.php
index 8042855..815dd99 100644
--- a/src/Entity/Redirect.php
+++ b/src/Entity/Redirect.php
@@ -175,6 +175,20 @@ class Redirect extends ContentEntityBase {
   }
 
   /**
+   * Gets the source URL path with its query.
+   *
+   * @return string
+   *   The source URL path, evtl. with its query.
+   */
+  public function getSourcePathWithQuery() {
+    $path = '/' . $this->get('redirect_source')->path;
+    if ($this->get('redirect_source')->query) {
+      $path .= '?' . UrlHelper::buildQuery($this->get('redirect_source')->query);
+    }
+    return $path;
+  }
+
+  /**
    * Gets the redirect URL data.
    *
    * @return array
diff --git a/src/Form/RedirectFix404Form.php b/src/Form/RedirectFix404Form.php
deleted file mode 100644
index 3dd1dda..0000000
--- a/src/Form/RedirectFix404Form.php
+++ /dev/null
@@ -1,173 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\redirect\Form\RedirectFix404Form
- */
-
-namespace Drupal\redirect\Form;
-
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Database\Query\SelectInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\Request;
-
-class RedirectFix404Form extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'redirect_fix_404_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $destination = drupal_get_destination();
-
-    $search = $this->getRequest()->get('search');
-    $form['#attributes'] = array('class' => array('search-form'));
-
-    // The following requires the dblog module. If it's not available, provide a message.
-    if (! \Drupal::moduleHandler()->moduleExists('dblog')) {
-      $form['message'] = [
-        '#type' => 'markup',
-        '#markup' => t('This page requires the Database Logging (dblog) module, which is currently not installed.'),
-      ];
-      return $form;
-    }
-
-    $form['basic'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Filter 404s'),
-      '#attributes' => array('class' => array('container-inline')),
-    );
-    $form['basic']['filter'] = array(
-      '#type' => 'textfield',
-      '#title' => '',
-      '#default_value' => $search,
-      '#maxlength' => 128,
-      '#size' => 25,
-    );
-    $form['basic']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Filter'),
-      '#action' => 'filter',
-    );
-    if ($search) {
-      $form['basic']['reset'] = array(
-        '#type' => 'submit',
-        '#value' => t('Reset'),
-        '#action' => 'reset',
-      );
-    }
-
-    $header = array(
-      array('data' => t('Page'), 'field' => 'message'),
-      array('data' => t('Count'), 'field' => 'count', 'sort' => 'desc'),
-      array('data' => t('Last accessed'), 'field' => 'timestamp'),
-      array('data' => t('Operations')),
-    );
-
-    $count_query = db_select('watchdog', 'w');
-    $count_query->addExpression('COUNT(DISTINCT(w.message))');
-    $count_query->leftJoin('redirect', 'r', 'w.message = r.redirect_source__path');
-    $count_query->condition('w.type', 'page not found');
-    $count_query->isNull('r.rid');
-    $this->filterQuery($count_query, array('w.message'), $search);
-
-    $query = db_select('watchdog', 'w')
-      ->extend('Drupal\Core\Database\Query\TableSortExtender')->orderByHeader($header)
-      ->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit(25);
-    $query->fields('w', array('message', 'variables'));
-    $query->addExpression('COUNT(wid)', 'count');
-    $query->addExpression('MAX(timestamp)', 'timestamp');
-    $query->leftJoin('redirect', 'r', 'w.message = r.redirect_source__path');
-    $query->isNull('r.rid');
-    $query->condition('w.type', 'page not found');
-    $query->groupBy('w.message');
-    $query->groupBy('w.variables');
-    $this->filterQuery($query, array('w.message'), $search);
-    $query->setCountQuery($count_query);
-    $results = $query->execute();
-
-    $rows = array();
-    foreach ($results as $result) {
-
-      // @todo Detect the language from the url.
-      $url = SafeMarkup::format($result->message, unserialize($result->variables));
-
-      $request = Request::create($url, 'GET', [], [], [], \Drupal::request()->server->all());
-      $path = ltrim($request->getPathInfo(), '/');
-
-      $row = array();
-      $row['source'] = \Drupal::l($url, Url::fromUri('base:' . $path, array('query' => $destination)));
-      $row['count'] = $result->count;
-      $row['timestamp'] = format_date($result->timestamp, 'short');
-
-      $operations = array();
-      if (\Drupal::entityManager()->getAccessControlHandler('redirect')->createAccess()) {
-        $operations['add'] = array(
-          'title' => t('Add redirect'),
-          'url' => Url::fromRoute('redirect.add', [], ['query' => array('source' => $path) + $destination]),
-        );
-      }
-      $row['operations'] = array(
-        'data' => array(
-          '#theme' => 'links',
-          '#links' => $operations,
-          '#attributes' => array('class' => array('links', 'inline', 'nowrap')),
-        ),
-      );
-
-      $rows[] = $row;
-    }
-
-    $form['redirect_404_table']  = array(
-      '#theme' => 'table',
-      '#header' => $header,
-      '#rows' => $rows,
-      '#empty' => t('No 404 pages without redirects found.'),
-    );
-    $form['redirect_404_pager'] = array('#type' => 'pager');
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-
-    if ($form_state->getTriggeringElement()['#action'] == 'filter') {
-      $form_state->setRedirect('redirect.fix_404', array(), array('query' => array('search' => trim($form_state->getValue('filter')))));
-    }
-    else {
-      $form_state->setRedirect('redirect.fix_404');
-    }
-  }
-
-  /**
-   * Extends a query object for URL redirect filters.
-   *
-   * @param $query
-   *   Query object that should be filtered.
-   * @param $keys
-   *   The filter string to use.
-   */
-  protected function filterQuery(SelectInterface $query, array $fields, $keys = '') {
-    if ($keys && $fields) {
-      // Replace wildcards with PDO wildcards.
-      $conditions = db_or();
-      $wildcard = '%' . trim(preg_replace('!\*+!', '%', db_like($keys)), '%') . '%';
-      foreach ($fields as $field) {
-        $conditions->condition($field, $wildcard, 'LIKE');
-      }
-      $query->condition($conditions);
-    }
-  }
-
-}
diff --git a/src/RedirectStorageSchema.php b/src/RedirectStorageSchema.php
index acdb1d9..b080c79 100644
--- a/src/RedirectStorageSchema.php
+++ b/src/RedirectStorageSchema.php
@@ -27,9 +27,8 @@ class RedirectStorageSchema extends SqlContentEntityStorageSchema {
     ];
     $schema['redirect']['indexes'] += [
       // Limit length to 191.
-      'source_language' => [['redirect_source__path', 191],'language'],
+      'source_language' => [['redirect_source__path', 191], 'language'],
     ];
-
     return $schema;
   }
 
diff --git a/src/Tests/AssertRedirectTrait.php b/src/Tests/AssertRedirectTrait.php
new file mode 100644
index 0000000..ae93688
--- /dev/null
+++ b/src/Tests/AssertRedirectTrait.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\redirect\Tests;
+
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Url;
+
+/**
+ * Asserts the redirect from a given path to the expected destination path.
+ */
+trait AssertRedirectTrait {
+
+  /**
+   * Asserts the redirect from $path to the $expected_ending_url.
+   *
+   * @param string $path
+   *   The request path.
+   * @param $expected_ending_url
+   *   The path where we expect it to redirect. If NULL value provided, no
+   *   redirect is expected.
+   * @param string $expected_ending_status
+   *   The status we expect to get with the first request.
+   */
+  public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') {
+    $this->drupalHead($path);
+    $headers = $this->drupalGetHeaders(TRUE);
+
+    $ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL;
+    $message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', [
+      '%from' => $path,
+      '%to' => $expected_ending_url,
+      '%url' => $ending_url,
+    ]);
+
+    if ($expected_ending_url == '<front>') {
+      $expected_ending_url = Url::fromUri('base:')->setAbsolute()->toString();
+    }
+    elseif (!empty($expected_ending_url)) {
+      // Check for absolute/external urls.
+      if (!parse_url($expected_ending_url, PHP_URL_SCHEME)) {
+        $expected_ending_url = Url::fromUri('base:' . $expected_ending_url)->setAbsolute()->toString();
+      }
+    }
+    else {
+      $expected_ending_url = NULL;
+    }
+
+    $this->assertEqual($expected_ending_url, $ending_url, $message);
+
+    $this->assertEqual($headers[0][':status'], $expected_ending_status);
+  }
+
+}
diff --git a/src/Tests/RedirectUITest.php b/src/Tests/RedirectUITest.php
index e2ace54..07fbd31 100644
--- a/src/Tests/RedirectUITest.php
+++ b/src/Tests/RedirectUITest.php
@@ -21,6 +21,8 @@ use Drupal\simpletest\WebTestBase;
  */
 class RedirectUITest extends WebTestBase {
 
+  use AssertRedirectTrait;
+
   /**
    * @var \Drupal\Core\Session\AccountInterface
    */
@@ -230,35 +232,6 @@ class RedirectUITest extends WebTestBase {
   }
 
   /**
-   * Tests the fix 404 pages workflow.
-   */
-  public function testFix404Pages() {
-    $this->drupalLogin($this->adminUser);
-
-    // Visit a non existing page to have the 404 watchdog entry.
-    $this->drupalGet('non-existing');
-
-    // Go to the "fix 404" page and check the listing.
-    $this->drupalGet('admin/config/search/redirect/404');
-    $this->assertText('non-existing');
-    $this->clickLink(t('Add redirect'));
-
-    // Check if we generate correct Add redirect url and if the form is
-    // pre-filled.
-    $destination = Url::fromUri('base:admin/config/search/redirect/404')->toString();
-    $this->assertUrl('admin/config/search/redirect/add', ['query' => ['source' => 'non-existing', 'destination' => $destination]]);
-    $this->assertFieldByName('redirect_source[0][path]', 'non-existing');
-
-    // Save the redirect.
-    $this->drupalPostForm(NULL, array('redirect_redirect[0][uri]' => '/node'), t('Save'));
-    $this->assertUrl('admin/config/search/redirect/404');
-
-    // Check if the redirect works as expected.
-    $this->drupalGet('non-existing');
-    $this->assertUrl('node');
-  }
-
-  /**
    * Tests redirects being automatically created upon path alias change.
    */
   public function testAutomaticRedirects() {
@@ -402,42 +375,6 @@ class RedirectUITest extends WebTestBase {
   }
 
   /**
-   * Asserts the redirect from $path to the $expected_ending_url.
-   *
-   * @param string $path
-   *   The request path.
-   * @param $expected_ending_url
-   *   The path where we expect it to redirect. If NULL value provided, no
-   *   redirect is expected.
-   * @param string $expected_ending_status
-   *   The status we expect to get with the first request.
-   */
-  public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') {
-    $this->drupalHead($path);
-    $headers = $this->drupalGetHeaders(TRUE);
-
-    $ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL;
-    $message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', array('%from' => $path, '%to' => $expected_ending_url, '%url' => $ending_url));
-
-    if ($expected_ending_url == '<front>') {
-      $expected_ending_url = Url::fromUri('base:')->setAbsolute()->toString();
-    }
-    elseif (!empty($expected_ending_url)) {
-      // Check for absolute/external urls.
-      if (!parse_url($expected_ending_url, PHP_URL_SCHEME)) {
-        $expected_ending_url = Url::fromUri('base:' . $expected_ending_url)->setAbsolute()->toString();
-      }
-    }
-    else {
-      $expected_ending_url = NULL;
-    }
-
-    $this->assertEqual($expected_ending_url, $ending_url, $message);
-
-    $this->assertEqual($headers[0][':status'], $expected_ending_status);
-  }
-
-  /**
    * Test cache tags.
    *
    * @todo Not sure this belongs in a UI test, but a full web test is needed.
