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..8947fdf
--- /dev/null
+++ b/modules/redirect_404/config/install/redirect_404.settings.yml
@@ -0,0 +1,2 @@
+row_limit: 10000
+pages: ''
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..b3a4e41
--- /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: '0'
+          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..2c47a24
--- /dev/null
+++ b/modules/redirect_404/config/schema/redirect_404.schema.yml
@@ -0,0 +1,11 @@
+# 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.'
+    pages:
+      type: string
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..1b479d0
--- /dev/null
+++ b/modules/redirect_404/redirect_404.install
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Update hooks for the redirect_404 module.
+ */
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\redirect_404\SqlRedirectNotFoundStorage;
+
+/**
+ * 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' => SqlRedirectNotFoundStorage::MAX_PATH_LENGTH,
+        '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.links.menu.yml b/modules/redirect_404/redirect_404.links.menu.yml
new file mode 100644
index 0000000..2862266
--- /dev/null
+++ b/modules/redirect_404/redirect_404.links.menu.yml
@@ -0,0 +1,6 @@
+redirect_404.fix_404:
+  title: 'Fix 404 pages'
+  parent: redirect.list
+  route_name: redirect_404.fix_404
+  description: 'Add redirects for 404 pages.'
+  menu_name: admin
diff --git a/modules/redirect_404/redirect_404.links.task.yml b/modules/redirect_404/redirect_404.links.task.yml
new file mode 100644
index 0000000..955161a
--- /dev/null
+++ b/modules/redirect_404/redirect_404.links.task.yml
@@ -0,0 +1,5 @@
+redirect_404.fix_404:
+  route_name: redirect_404.fix_404
+  base_route: redirect.list
+  title: Fix 404 pages
+  weight: 30
diff --git a/modules/redirect_404/redirect_404.module b/modules/redirect_404/redirect_404.module
new file mode 100644
index 0000000..8d12e5f
--- /dev/null
+++ b/modules/redirect_404/redirect_404.module
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Module file for redirect_404.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+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_redirect_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  $row_limits = [100, 1000, 10000, 100000, 1000000];
+  $form['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' => Url::fromRoute('system.status')->toString()])
+  ];
+
+  $ignored_pages = \Drupal::configFactory()->getEditable('redirect_404.settings')->get('pages');
+  // Add a new path to be ignored, if there is an ignore argument in the query.
+  if ($path_to_ignore = \Drupal::request()->query->get('ignore')) {
+    $ignored_pages .= "\n" . $path_to_ignore;
+  }
+
+  $form['ignore_pages'] = [
+    '#type' => 'textarea',
+    '#title' => t('Pages to ignore'),
+    '#default_value' => $ignored_pages,
+    '#description' => t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. An example path is %user-wildcard for every user page. %front is the front page.", [
+      '%user-wildcard' => '/user/*',
+      '%front' => '<front>',
+    ]),
+  ];
+
+  $form['#submit'][] = 'redirect_404_logging_settings_submit';
+}
+
+/**
+ * Form submission handler for system_logging_settings().
+ *
+ * @see redirect_404_form_redirect_settings_form_alter()
+ */
+function redirect_404_logging_settings_submit($form, FormStateInterface $form_state) {
+  // Make sure to store the 'pages to ignore' with the leading slash.
+  $ignore_pages = explode(PHP_EOL, $form_state->getValue('ignore_pages'));
+  $pages = '';
+  foreach ($ignore_pages as $page) {
+    if (!empty($page)) {
+      $pages .= '/' . ltrim($page, '/') . "\n";
+    }
+  }
+
+  \Drupal::configFactory()
+    ->getEditable('redirect_404.settings')
+    ->set('row_limit', $form_state->getValue('row_limit'))
+    ->set('pages', $pages)
+    ->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..04caa6c
--- /dev/null
+++ b/modules/redirect_404/redirect_404.routing.yml
@@ -0,0 +1,15 @@
+redirect_404.fix_404:
+  path: '/admin/config/search/redirect/404'
+  defaults:
+    _title: 'Fix 404 pages'
+    _form: '\Drupal\redirect_404\Form\RedirectFix404Form'
+  requirements:
+    _permission: 'administer redirects'
+
+redirect_404.ignore_404:
+  path: '/admin/config/search/redirect/404/ignore'
+  defaults:
+    _controller: '\Drupal\redirect_404\Controller\Fix404IgnoreController::ignorePath'
+  requirements:
+    _permission: 'administer redirects'
+    _csrf_token: 'TRUE'
diff --git a/modules/redirect_404/redirect_404.services.yml b/modules/redirect_404/redirect_404.services.yml
new file mode 100644
index 0000000..50a29c9
--- /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', '@path.matcher', '@request_stack', '@language_manager', '@redirect.not_found_storage', '@config.factory']
+    tags:
+      - { name: event_subscriber }
+  redirect.not_found_storage:
+    class: Drupal\redirect_404\SqlRedirectNotFoundStorage
+    arguments: ['@database', '@config.factory']
+    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/Controller/Fix404IgnoreController.php b/modules/redirect_404/src/Controller/Fix404IgnoreController.php
new file mode 100644
index 0000000..a003311
--- /dev/null
+++ b/modules/redirect_404/src/Controller/Fix404IgnoreController.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\redirect_404\Controller;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+use Drupal\redirect_404\RedirectNotFoundStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Controller to ignore a path from the 'Fix 404 pages' page.
+ */
+class Fix404IgnoreController extends ControllerBase {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configuration;
+
+  /**
+   * The redirect storage.
+   *
+   * @var \Drupal\redirect_404\RedirectNotFoundStorageInterface
+   */
+  protected $redirectStorage;
+
+  /**
+   * Constructs a Fix404Ignore object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\redirect_404\RedirectNotFoundStorageInterface $redirect_storage
+   *   A redirect storage.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, RedirectNotFoundStorageInterface $redirect_storage) {
+    $this->configuration = $config_factory;
+    $this->redirectStorage = $redirect_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('redirect.not_found_storage')
+    );
+  }
+
+  /**
+   * Adds path into the ignored list.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HttpRequest object representing the current request.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   */
+  public function ignorePath(Request $request) {
+    $ignored_paths = $this->config('redirect_404.settings')->get('pages');
+    $path = $request->query->get('path');
+    $langcode = $request->query->get('langcode');
+
+    if (empty($ignored_paths) || !strpos($path, $ignored_paths)) {
+      $this->redirectStorage->resolveLogRequest($path, $langcode);
+
+      drupal_set_message($this->t('Resolved the path %path in the database. Please check the ignored list and save the settings.', [
+        '%path' => $path,
+      ]));
+    }
+
+    $options = [
+      'query' => [
+        'ignore' => $path,
+        'destination' => Url::fromRoute('redirect_404.fix_404')->getInternalPath(),
+      ],
+    ];
+
+    return $this->redirect('redirect.settings', [], $options);
+  }
+
+}
diff --git a/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
new file mode 100644
index 0000000..7a54e86
--- /dev/null
+++ b/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Drupal\redirect_404\EventSubscriber;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Path\PathMatcherInterface;
+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 path matcher.
+   *
+   * @var \Drupal\Core\Path\PathMatcherInterface
+   */
+  protected $pathMatcher;
+
+  /**
+   * 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;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $config;
+
+  /**
+   * Constructs a new Redirect404Subscriber.
+   *
+   * @param \Drupal\Core\Path\CurrentPathStack $current_path
+   *   The current path.
+   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
+   *   The path matcher service.
+   * @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.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
+   *   The configuration factory.
+   */
+  public function __construct(CurrentPathStack $current_path, PathMatcherInterface $path_matcher, RequestStack $request_stack, LanguageManagerInterface $language_manager, RedirectNotFoundStorageInterface $redirect_storage, ConfigFactoryInterface $config) {
+    $this->currentPath = $current_path;
+    $this->pathMatcher = $path_matcher;
+    $this->requestStack = $request_stack;
+    $this->languageManager = $language_manager;
+    $this->redirectStorage = $redirect_storage;
+    $this->config = $config->get('redirect_404.settings');
+  }
+
+  /**
+   * {@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) {
+      $path = $this->currentPath->getPath();
+
+      // Ignore paths specified in the redirect settings.
+      if ($pages = Unicode::strtolower($this->config->get('pages'))) {
+        // Do not trim a trailing slash if that is the complete path.
+        $path_to_match = $path === '/' ? $path : rtrim($path, '/');
+
+        if ($this->pathMatcher->matchPath(Unicode::strtolower($path_to_match), $pages)) {
+          return;
+        }
+      }
+
+      // Allow to store paths with arguments.
+      if ($query_string = $this->requestStack->getCurrentRequest()->getQueryString()) {
+        $query_string = '?' . $query_string;
+      }
+      $path .= $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..7e01d1a
--- /dev/null
+++ b/modules/redirect_404/src/Form/RedirectFix404Form.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace Drupal\redirect_404\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Url;
+use Drupal\redirect_404\SqlRedirectNotFoundStorage;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form that lists all 404 error paths and no redirect assigned yet.
+ *
+ * This is a fallback for the provided default view.
+ */
+class RedirectFix404Form extends FormBase {
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The redirect storage.
+   *
+   * @var \Drupal\redirect_404\SqlRedirectNotFoundStorage
+   */
+  protected $redirectStorage;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a RedirectFix404Form.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\redirect_404\SqlRedirectNotFoundStorage $redirect_storage
+   *   The redirect storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date Formatter service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   */
+  public function __construct(LanguageManagerInterface $language_manager, SqlRedirectNotFoundStorage $redirect_storage, DateFormatterInterface $date_formatter, EntityTypeManagerInterface $entity_type_manager) {
+    $this->languageManager = $language_manager;
+    $this->redirectStorage = $redirect_storage;
+    $this->dateFormatter = $date_formatter;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('language_manager'),
+      $container->get('redirect.not_found_storage'),
+      $container->get('date.formatter'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@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 = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
+    $multilingual = $this->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')];
+
+    $rows = [];
+    $results = $this->redirectStorage->listRequests($header, $search);
+    foreach ($results as $result) {
+      $path = ltrim($result->path, '/');
+
+      $row = [];
+      $row['source'] = $path;
+      $row['count'] = $result->count;
+      $row['timestamp'] = $this->dateFormatter->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 ($this->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_404.fix_404', [], ['query' => ['search' => trim($form_state->getValue('filter'))]]);
+    }
+    else {
+      $form_state->setRedirect('redirect_404.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..4fc03d3
--- /dev/null
+++ b/modules/redirect_404/src/Plugin/views/field/Language.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\redirect_404\Plugin\views\field;
+
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\views\Plugin\views\field\LanguageField;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a views field for the 404 error language.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("redirect_404_langcode")
+ */
+class Language extends LanguageField {
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a Language object.
+   *
+   * @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\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(AccountInterface $account) {
+    return $this->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..72b335c
--- /dev/null
+++ b/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php
@@ -0,0 +1,117 @@
+<?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) {
+    $links = [];
+
+    $query = [
+      'query' => [
+        'source' => ltrim($this->getValue($values, 'path'), '/'),
+        'language' => $this->getValue($values, 'langcode'),
+        'destination' => $this->view->getPath(),
+      ]
+    ];
+    $links['add'] = [
+      'title' => $this->t('Add redirect'),
+      'url' => Url::fromRoute('redirect.add', [], $query),
+    ];
+
+    $links['ignore'] = [
+      'title' => $this->t('Ignore'),
+      'url' => Url::fromRoute('redirect_404.ignore_404', [
+        'path' => $this->getValue($values, 'path'),
+        'langcode' => $this->getValue($values, 'langcode'),
+      ]),
+    ];
+
+    $operations['data'] = [
+      '#type' => 'operations',
+      '#links' => $links,
+    ];
+
+    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..bd4cd2f
--- /dev/null
+++ b/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\redirect_404;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\ConfigFactoryInterface;
+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 {
+
+  /**
+   * Maximum column length for invalid paths.
+   */
+  const MAX_PATH_LENGTH = 191;
+
+  /**
+   * Active database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a new SqlRedirectNotFoundStorage.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   A Database connection to use for reading and writing database data.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   */
+  public function __construct(Connection $database, ConfigFactoryInterface $config_factory) {
+    $this->database = $database;
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function logRequest($path, $langcode) {
+    // If the request is not new, update its relevancy for the time interval
+    // since the last hit.
+    if (Unicode::strlen($path) > static::MAX_PATH_LENGTH) {
+      // Don't attempt to log paths that would result in an exception. There is
+      // no point in logging truncated paths, as they cannot be used to build a
+      // new redirect.
+      return;
+    }
+    // Ignore invalid UTF-8, which can't be logged.
+    if (!Unicode::validateUtf8($path)) {
+      return;
+    }
+
+    $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 = $this->configFactory->get('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..4428d89
--- /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_404.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..44b6644
--- /dev/null
+++ b/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
@@ -0,0 +1,185 @@
+<?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_404.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');
+  }
+
+  /**
+   * Tests the redirect ignore pages.
+   */
+  public function testIgnorePages() {
+    // Create two nodes.
+    $node1 = $this->drupalCreateNode(['type' => 'page']);
+    $node2 = $this->drupalCreateNode(['type' => 'page']);
+
+    // Set some pages to be ignored just for the test.
+    $node_to_ignore = '/node/' . $node1->id() . '/test';
+    $terms_to_ignore = '/term/*';
+    $pages = $node_to_ignore . "\r\n" . $terms_to_ignore;
+    \Drupal::configFactory()
+      ->getEditable('redirect_404.settings')
+      ->set('pages', $pages)
+      ->save();
+
+    // Visit ignored or non existing pages.
+    $this->drupalGet('node/' . $node1->id() . '/test');
+    $this->drupalGet('term/foo');
+    $this->drupalGet('term/1');
+    // Go to the "fix 404" page and check there are no 404 entries.
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertNoText('node/' . $node1->id() . '/test');
+    $this->assertNoText('term/foo');
+    $this->assertNoText('term/1');
+
+    // Visit non existing but 'unignored' page.
+    $this->drupalGet('node/' . $node2->id() . '/test');
+    // Go to the "fix 404" page and check there is a 404 entry.
+    $this->drupalGet('admin/config/search/redirect/404');
+    $this->assertText('node/' . $node2->id() . '/test');
+
+    // Add this 404 entry to the 'ignore path' list, assert it works properly.
+    $path_to_ignore = '/node/' . $node2->id() . '/test';
+    $destination = '&destination=admin/config/search/redirect/404';
+    $this->clickLink('Ignore');
+    $this->assertUrl('admin/config/search/redirect/settings?ignore=' . $path_to_ignore . $destination);
+    $this->assertText('Resolved the path ' . $path_to_ignore . ' in the database. Please check the ignored list and save the settings.');
+    $xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML();
+    $this->assertTrue(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found");
+    $this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found");
+    $this->assertTrue(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found");
+
+    // Save the path with wildcard, but omitting the leading slash.
+    $nodes_to_ignore = 'node/*';
+    $edit = ['ignore_pages' => $nodes_to_ignore . "\r\n" . $terms_to_ignore];
+    $this->drupalPostForm(NULL, $edit, 'Save configuration');
+    // Should redirect to 'Fix 404'. Check the 404 entry is not shown anymore.
+    $this->assertUrl('admin/config/search/redirect/404');
+    $this->assertText('Configuration was saved.');
+    $this->assertNoText('node/' . $node2->id() . '/test');
+    $this->assertText('There are no 404 errors to fix.');
+
+    // Go back to the settings to check the 'Path to ignore' configurations.
+    $this->drupalGet('admin/config/search/redirect/settings');
+    $xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML();
+    // Check that the new page to ignore has been saved with leading slash.
+    $this->assertTrue(strpos($xpath, '/' . $nodes_to_ignore), '/' . $nodes_to_ignore . " in 'Path to ignore' found");
+    $this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found");
+    $this->assertFalse(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found");
+    $this->assertFalse(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found");
+  }
+
+}
diff --git a/modules/redirect_404/src/Tests/Redirect404TestBase.php b/modules/redirect_404/src/Tests/Redirect404TestBase.php
new file mode 100644
index 0000000..3ce9cf2
--- /dev/null
+++ b/modules/redirect_404/src/Tests/Redirect404TestBase.php
@@ -0,0 +1,144 @@
+<?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',
+  ];
+
+  /**
+   * 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);
+
+    $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
+  }
+
+  /**
+   * 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/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php b/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php
new file mode 100644
index 0000000..b9ba88f
--- /dev/null
+++ b/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\redirect_404\Unit;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\redirect_404\SqlRedirectNotFoundStorage;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests that overly long paths aren't logged.
+ *
+ * @group redirect_404
+ */
+class SqlRedirectNotFoundStorageTest extends UnitTestCase {
+
+  /**
+   * Mock database connection.
+   *
+   * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->database = $this->getMockBuilder(Connection::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+  }
+
+  /**
+   * Tests that long paths aren't stored in the database.
+   */
+  public function testLongPath() {
+    $this->database->expects($this->never())
+      ->method('merge');
+    $storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub());
+    $storage->logRequest($this->randomMachineName(SqlRedirectNotFoundStorage::MAX_PATH_LENGTH + 1), LanguageInterface::LANGCODE_DEFAULT);
+  }
+
+  /**
+   * Tests that invalid UTF-8 paths are not stored in the database.
+   */
+  public function testInvalidUtf8Path() {
+    $this->database->expects($this->never())
+      ->method('merge');
+    $storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub());
+    $storage->logRequest("Caf\xc3", LanguageInterface::LANGCODE_DEFAULT);
+  }
+
+}
diff --git a/redirect.install b/redirect.install
index 04d0de4..d2194dd 100644
--- a/redirect.install
+++ b/redirect.install
@@ -1,11 +1,13 @@
 <?php
 
 /**
+ * @file
  * Update hooks for the Redirect module.
  */
 
 use Drupal\redirect\Entity\Redirect;
 use Drupal\Core\Database\Database;
+use Drupal\system\Entity\Action;
 use Drupal\views\Entity\View;
 use Symfony\Component\Yaml\Yaml;
 use Drupal\Core\Config\InstallStorage;
@@ -154,15 +156,24 @@ function redirect_update_8103() {
 /**
  * Save the bulk delete action to config.
  */
-function redirect_update_8002() {
-  $entity_type_manager = \Drupal::entityTypeManager();
-  $module_handler = \Drupal::moduleHandler();
-
-  // Save the bulk delete action to config.
-  $config_install_path = $module_handler->getModule('redirect')->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
-  $storage = new FileStorage($config_install_path);
-  $entity_type_manager
-    ->getStorage('action')
-    ->create($storage->read('system.action.redirect_delete_action'))
-    ->save();
+function redirect_update_8104() {
+  if (!Action::load('redirect_delete_action')) {
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $module_handler = \Drupal::moduleHandler();
+
+    // Save the bulk delete action to config.
+    $config_install_path = $module_handler->getModule('redirect')->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
+    $storage = new FileStorage($config_install_path);
+    $entity_type_manager
+      ->getStorage('action')
+      ->create($storage->read('system.action.redirect_delete_action'))
+      ->save();
+  }
+}
+
+/**
+ * Ensure to use the redirect_404 submodule.
+ */
+function redirect_update_8105() {
+  \Drupal::service('module_installer')->install(['redirect_404']);
 }
diff --git a/redirect.links.menu.yml b/redirect.links.menu.yml
index 1cd9ef2..9fff87d 100644
--- a/redirect.links.menu.yml
+++ b/redirect.links.menu.yml
@@ -18,20 +18,6 @@ redirect.settings:
   description: 'Configure behavior for URL redirects.'
   menu_name: admin
 
-redirect.fix_404:
-  title: 'Fix 404 pages'
-  parent: redirect.list
-  route_name: redirect.fix_404
-  description: 'Add redirects for 404 pages.'
-  menu_name: admin
-
-redirect.goto_fix_404:
-  title: 'Fix 404 pages'
-  parent: redirect.list
-  route_name: redirect.fix_404
-  description: 'Fix 404 pages with URL redirects.'
-  menu_name: admin
-
 #redirect.devel_generate:
 #  title: 'Generate redirects'
 #  parent: dblog.page_not_found
diff --git a/redirect.links.task.yml b/redirect.links.task.yml
index 1098fc5..4216cbc 100644
--- a/redirect.links.task.yml
+++ b/redirect.links.task.yml
@@ -8,9 +8,3 @@ redirect.settings:
   base_route: redirect.list
   title: Settings
   weight: 50
-
-redirect.fix_404:
-  route_name: redirect.fix_404
-  base_route: redirect.list
-  title: Fix 404 pages
-  weight: 30
diff --git a/redirect.module b/redirect.module
index e14bacf..c000454 100644
--- a/redirect.module
+++ b/redirect.module
@@ -57,8 +57,14 @@ function redirect_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dd>' . t('Redirect is accessed from three tabs that help you manage <a href=":list">URL Redirects</a>.', [':list' => Url::fromRoute('redirect.list')->toString()]) . '</dd>';
       $output .= '<dt>' . t('Manage URL Redirects') . '</dt>';
       $output .= '<dd>' . t('The <a href=":redirect">"URL Redirects"</a> page is used to setup and manage URL Redirects.  New redirects are created here using the <a href=":add_form">Add redirect</a> button which presents a form to simplify the creation of redirects . The URL redirects page provides a list of all redirects on the site and allows you to edit them.', [':redirect' => Url::fromRoute('redirect.list')->toString(), ':add_form' => Url::fromRoute('redirect.add')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Fix 404 pages') . '</dt>';
-      $output .= '<dd>' . t('<a href=":fix_404">"Fix 404 pages"</a> lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them. This 404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested.', [':fix_404' => Url::fromRoute('redirect.fix_404')->toString()]) . '</dd>';
+      if (\Drupal::moduleHandler()->moduleExists('redirect_404')) {
+        $output .= '<dt>' . t('Fix 404 pages') . '</dt>';
+        $output .= '<dd>' . t('<a href=":fix_404">"Fix 404 pages"</a> lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them. This 404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested.', [':fix_404' => Url::fromRoute('redirect_404.fix_404')->toString()]) . '</dd>';
+      }
+      elseif (!\Drupal::moduleHandler()->moduleExists('redirect_404') && \Drupal::currentUser()->hasPermission('administer modules')) {
+        $output .= '<dt>' . t('Fix 404 pages') . '</dt>';
+        $output .= '<dd>' . t('404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested. Please install the <a href=":extend">Redirect 404</a> submodule to be able to log all paths that have resulted in 404 errors.', [':extend' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
+      }
       $output .= '<dt>' . t('Configure Global Redirects') . '</dt>';
       $output .= '<dd>' . t('The <a href=":settings">"Settings"</a> page presents you with a number of means to adjust redirect settings.', [':settings' => Url::fromRoute('redirect.settings')->toString()]) . '</dd>';
       $output .= '</dl>';
diff --git a/redirect.routing.yml b/redirect.routing.yml
index d38af53..c7a9e0c 100644
--- a/redirect.routing.yml
+++ b/redirect.routing.yml
@@ -53,14 +53,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 4a1cc7f..ae54ec5 100644
--- a/src/Entity/Redirect.php
+++ b/src/Entity/Redirect.php
@@ -170,6 +170,20 @@ class Redirect extends ContentEntityBase {
   }
 
   /**
+   * Gets the source URL path with its query.
+   *
+   * @return string
+   *   The source URL path, eventually 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 bc19092..0000000
--- a/src/Form/RedirectFix404Form.php
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-
-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 8b08c4b..e034362 100644
--- a/src/RedirectStorageSchema.php
+++ b/src/RedirectStorageSchema.php
@@ -22,7 +22,7 @@ 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 7df1eb9..ca341c0 100644
--- a/src/Tests/RedirectUITest.php
+++ b/src/Tests/RedirectUITest.php
@@ -16,6 +16,8 @@ use Drupal\simpletest\WebTestBase;
  */
 class RedirectUITest extends WebTestBase {
 
+  use AssertRedirectTrait;
+
   /**
    * @var \Drupal\Core\Session\AccountInterface
    */
@@ -235,35 +237,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() {
@@ -407,42 +380,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.
