Introduced KalamarErrors plugin

Change-Id: I72fc22a702e41af7beec9d1cbdf3874e65bd2bb4
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 8a3274a..31fcb47 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -144,7 +144,8 @@
     'Search',                    # Abstract Search framework
     'TagHelpers::MailToChiffre', # Obfuscate email addresses
     'KalamarHelpers',            # Specific Helpers for Kalamar
-    'KalamarUser',               # Specific Helpers for Kalamar
+    'KalamarErrors',             # Specific Errors for Kalamar
+    'KalamarUser',               # Specific Helpers for Kalamar Users
     'ClientIP',                  # Get client IP from X-Forwarded-For
     'ClosedRedirect',            # Redirect with OpenRedirect protection
     'TagHelpers::ContentBlock',  # Flexible content blocks
diff --git a/lib/Kalamar/Controller/Search2.pm b/lib/Kalamar/Controller/Search2.pm
index 7bb0b99..944b7a4 100644
--- a/lib/Kalamar/Controller/Search2.pm
+++ b/lib/Kalamar/Controller/Search2.pm
@@ -5,83 +5,343 @@
 use Mojo::ByteStream 'b';
 use POSIX 'ceil';
 
-# This should be implemented as a helper
-has api => '/api/';
-
 has no_cache => 0;
 
 has items_per_page => 25;
 
-
 # TODO:
 #   Support server timing API
 
-# Catch connection errors
-sub _catch_http_errors {
-  my $tx = shift;
-  my $err = $tx->error;
+# Query endpoint
+sub query {
+  my $c = shift;
 
-  if ($err) {
-    # print $err->code, "\n";
-    return Mojo::Promise->new->reject([
-      [$err->{code}, $err->{message}]
-    ]);
-  };
-  return $tx->result;
-};
+  # Validate user input
+  my $v = $c->validation;
 
+  # In case the user is not known, it is assumed, the user is not logged in
+  my $user = $c->stash('user') // 'not_logged_in';
 
-# Catch koral errors
-sub _catch_koral_errors {
-  my $json = shift;
+  $v->optional('q', 'trim');
+  $v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
+  $v->optional('collection', 'trim'); # Legacy
+  $v->optional('cq', 'trim');
+  # $v->optional('action'); # action 'inspect' is no longer valid
+  # $v->optional('snippet');
+  $v->optional('cutoff')->in(qw/true false/);
+  $v->optional('count')->num(1, undef);
+  $v->optional('p', 'trim')->num(1, undef); # Start page
+  $v->optional('o', 'trim')->num(1, undef); # Offset
+  $v->optional('context');
 
-  # Get errors
-  my $err = $json->{errors};
+  # Get query
+  my $query = $v->param('q');
 
-  # Create error message
-  if ($err) {
-    return Mojo::Promise->new->reject($err);
+  # TODO:
+  #   Check for validation errors!
+
+  # No query
+  unless ($query) {
+    return $c->render($c->loc('Template_intro', 'intro'));
   };
 
-  # TODO: What does status mean?
-  if ($json->{status}) {
-    return Mojo::Promise->new->reject([
-      [undef, 'Middleware error ' . $json->{'status'}]
-    ]);
+  my %query = ();
+  $query{q}       = $v->param('q');
+  $query{ql}      = $v->param('ql') // 'poliqarp';
+  $query{p}       = $v->param('p') // 1; # Start page
+  $query{count}   = $v->param('count') // $c->items_per_page;
+  $query{cq}      = $v->param('cq') // $v->param('collection');
+  $query{cutoff}  = $v->param('cutoff');
+
+  # Before: 'base/s:p'/'paragraph'
+  $query{context} = $v->param('context') // '40-t,40-t';
+
+  my $items_per_page = $c->items_per_page;
+
+  # Set count
+  if ($query{count} && $query{count} <= $c->items_per_page ) {
+    $items_per_page = delete $query{count};
   };
 
-  return $json;
-};
+  $c->stash(items_per_page => $items_per_page);
+
+  # Set offset
+  # From Mojolicious::Plugin::Search::Index
+  $query{o} = $v->param('o') || ((($query{p} // 1) - 1) * ($items_per_page || 1));
 
 
-# Notify the user in case of warnings
-sub _notify_on_warnings {
-  my ($self, $warnings) = @_;
+  # already set by stash - or use plugin param
+  # else {
+  #   $items_per_page = $c->stash('search.count') // $plugin->items_per_page
+  # };
 
-  # TODO: Check for ref!
-  foreach my $w (@$warnings) {
-    $self->notify(
-      warn =>
-        ($w->[0] ? $w->[0] . ': ' : '') .
-        $w->[1]
+  # Set start page based on param
+  #if ($query{p}) {
+  #  $index->start_page(delete $param{start_page});
+  #}
+  ## already set by stash
+  #elsif ($c->stash('search.start_page')) {
+  #  $index->start_page($c->stash('search.start_page'));
+  #};
+
+
+  # Create remote request URL
+  my $url = Mojo::URL->new($c->korap->api);
+  $url->path('search');
+  $url->query(\%query);
+
+  # Check if total results is cached
+  my $total_results = -1;
+  unless ($c->no_cache) {
+
+    # Get total results value
+    $total_results = $c->chi->get('total-' . $user . '-' . $url->to_string);
+
+    # Set stash if cache exists
+    $c->stash(total_results => $total_results) if $total_results;
+    $c->app->log->debug('Get total result from cache');
+
+    # Set cutoff unless already set
+    $url->query({cutoff => 'true'});
+  };
+
+  # Check if the request is cached
+  my $url_string = $url->to_string;
+
+  # Set api request for debugging
+  $c->stash(api_request => $url_string);
+
+  # Debugging
+  $c->app->log->debug("Search for $url_string");
+
+  # Check for cache
+  my $json = $c->chi->get("matches-$user-$url_string");
+
+  # Initialize promise object
+  my $promise;
+
+  # Result is cached
+  if ($json) {
+    $json->{cached} = 'true';
+
+    # The promise is already satisfied by the cache
+    $promise = Mojo::Promise->new->resolve($json)->then(
+      sub {
+        my $json = shift;
+        $c->notify_on_warnings($json);
+        return $json;
+      }
+    );
+  }
+
+  # Retrieve from URL
+  else {
+
+    # Wrap a user agent method with a promise
+    $promise = $c->user->auth_request_p(get => $url)->then(
+      sub {
+
+        # Catch errors and warnings
+        return $c->catch_errors_and_warnings(shift)
+      }
+    );
+  };
+
+  # Wait for rendering
+  $c->render_later;
+
+  # Choose the snippet based on the parameter
+  # scalar $v->param('snippet') ? 'snippet' : 'search2';
+  my $template = 'search2';
+  $c->stash(template => $template);
+
+  # Process response
+  $promise->then(
+    sub {
+      my $json = shift;
+
+      #######################
+      # Cache total results #
+      #######################
+      # The stash is set in case the total results value is from the cache,
+      # so in that case, it does not need to be cached again
+      my $total_results = $c->stash('total_results');
+      if (!$total_results) {
+
+        # There are results to remember
+        if ($json->{meta}->{totalResults} >= 0) {
+
+          # Remove cutoff requirement again
+          $url->query([cutoff => 'true']);
+
+          $total_results = $json->{meta}->{totalResults};
+          $c->stash(total_results => $total_results);
+
+          # Set cache
+          $c->chi->set(
+            'total-' . $user . '-' . $url->to_string => $total_results
+          );
+        };
+      }
+      else {
+        $c->stash(total_results => -1);
+      }
+
+      $c->stash(total_pages => 0);
+
+      # Set total pages
+      # From Mojolicious::Plugin::Search::Index
+      if ($total_results > 0) {
+        $c->stash(
+          total_pages => ceil($total_results / ($c->stash('items_per_page') || 1))
+        );
+      };
+
+      # Cache result
+      $c->chi->set('matches-' . $user . '-' . $url_string => $json);
+
+      # Process match results
+      $c->_process_query_response($json);
+
+      # Render result
+      return $c->render(
+        q => $query,
+        ql => $query{ql},
+        start_page => $query{p},
       );
-  };
+
+    }
+
+      # Deal with errors
+  )->catch(
+    sub {
+
+      # $c->_notify_on_errors(shift);
+      return $c->render(
+        results => c(),
+        q => $query,
+        ql => $query{ql},
+        start_page => 1
+      );
+    }
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
 };
 
-sub _notify_on_errors {
-  my ($self, $errors) = @_;
-  foreach my $e (@$errors) {
-    $self->notify(
-      error =>
-        ($e->[0] ? $e->[0] . ': ' : '') .
-        ($e->[1] || 'Unknown')
-      );
+
+# Match info endpoint
+sub match_info {
+  my $c = shift;
+
+  # Validate user input
+  my $v = $c->validation;
+  $v->optional('foundry');
+  $v->optional('layer');
+  $v->optional('spans')->in(qw/true false/);
+
+  # Old API foundry/layer usage
+  my $foundry = '*';
+  my %query = (foundry => '*');
+  if ($v->param('foundry')) {
+    $query{foundry} = $v->param('foundry');
+    $query{layer} = $v->param('layer') if $v->param('layer');
+    $query{spans} = $v->param('spans') if $v->param('spans');
   };
+
+  # Create new request API
+  my $url = Mojo::URL->new($c->korap->api);
+
+  # Use stash information to create url path
+  $url->path(
+    join('/', (
+      'corpus',
+      $c->stash('corpus_id'),
+      $c->stash('doc_id'),
+      $c->stash('text_id'),
+      $c->stash('match_id'),
+      'matchInfo'
+    ))
+  );
+
+  # Set query parameters
+  $url->query(\%query);
+
+  $c->render_later;
+
+  # TODO: Add caching!
+  $c->user->auth_request_p(get => $url)->then(
+    sub {
+      return $c->catch_errors_and_warnings(shift);
+    }
+  )
+  ->then(
+    sub {
+      my $json = shift;
+
+      # Process results
+      $json = _map_match($json);
+      $c->stash(results => $json);
+
+      return $c->render(
+        json => $c->notifications(json => $json),
+        status => 200
+      );
+
+      return $json;
+    }
+  )
+  # Deal with errors
+  ->catch(
+    sub {
+      return $c->render(
+        json => $c->notifications('json')
+      )
+    }
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
+};
+
+
+# Cleanup array of matches
+sub _map_matches {
+  return c() unless $_[0];
+  c(map { _map_match($_) } @{ shift() });
+};
+
+
+# Cleanup single match
+sub _map_match {
+  my $match = shift or return;
+
+  # Legacy match id
+  if ($match->{matchID}) {
+    $match->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
+      $match->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
+  };
+
+  return unless $match->{textSigle};
+
+  # Set IDs based on the sigle
+  (
+    $match->{corpusID},
+    $match->{docID},
+    $match->{textID}
+  ) = ($match->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
+
+  return $match;
 };
 
 
 # Process response and set stash values
-sub _process_matches {
+sub _process_query_response {
   my ($self, $json) = @_;
 
     # Process meta
@@ -96,6 +356,7 @@
   # if ($benchmark && $benchmark =~ s/\s+(m)?s$//) {
   #   $benchmark = sprintf("%.2f", $benchmark) . ($1 ? $1 : '') . 's';
   # };
+  #
   # # Set benchmark
   # $self->stash(benchmark => $benchmark);
 
@@ -139,420 +400,7 @@
 };
 
 
-# Cleanup array of matches
-sub _map_matches {
-  return c() unless $_[0];
-  c(map { _map_match($_) } @{ shift() });
-};
-
-
-# Cleanup single match
-sub _map_match {
-  my $match = shift or return;
-
-  # Legacy match id
-  if ($match->{matchID}) {
-    $match->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
-      $match->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
-  };
-
-  return unless $match->{textSigle};
-
-  # Set IDs based on the sigle
-  (
-    $match->{corpusID},
-    $match->{docID},
-    $match->{textID}
-  ) = ($match->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
-
-  return $match;
-};
-
-
-# Query endpoint
-sub query {
-  my $c = shift;
-
-  # Validate user input
-  my $v = $c->validation;
-
-  # In case the user is not known, it is assumed, the user is not logged in
-  my $user = $c->stash('user') // 'not_logged_in';
-
-  $v->optional('q', 'trim');
-  $v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
-  $v->optional('collection', 'trim'); # Legacy
-  $v->optional('cq', 'trim');
-  # $v->optional('action'); # action 'inspect' is no longer valid
-  $v->optional('snippet');
-  $v->optional('cutoff')->in(qw/true false/);
-  $v->optional('count')->num(1, undef);
-  $v->optional('p', 'trim')->num(1, undef); # Start page
-  $v->optional('o', 'trim')->num(1, undef); # Offset
-  $v->optional('context');
-
-  # Get query
-  my $query = $v->param('q');
-
-
-  # TODO:
-  #   Check for validation errors!
-
-  # No query
-  unless ($query) {
-    return $c->render($c->loc('Template_intro', 'intro'));
-  };
-
-  my %query = ();
-  $query{q}       = $v->param('q');
-  $query{ql}      = $v->param('ql') // 'poliqarp';
-  $query{p}       = $v->param('p') // 1; # Start page
-  $query{count}   = $v->param('count') // $c->items_per_page;
-  $query{cq}      = $v->param('cq') // $v->param('collection');
-  $query{cutoff}  = $v->param('cutoff');
-  $query{context} = $v->param('context') // '40-t,40-t'; # 'base/s:p'/'paragraph'
-
-  my $items_per_page = $c->items_per_page;
-
-  # Set count
-  if ($query{count} && $query{count} <= $c->items_per_page ) {
-    $items_per_page = delete $query{count};
-  };
-
-  $c->stash(items_per_page => $items_per_page);
-
-  # Set offset
-  # From Mojolicious::Plugin::Search::Index
-  $query{o} = $v->param('o') || ((($query{p} // 1) - 1) * ($items_per_page || 1));
-
-
-  # already set by stash - or use plugin param
-  # else {
-  #   $items_per_page = $c->stash('search.count') // $plugin->items_per_page
-  # };
-
-  # Set start page based on param
-  #if ($query{p}) {
-  #  $index->start_page(delete $param{start_page});
-  #}
-  ## already set by stash
-  #elsif ($c->stash('search.start_page')) {
-  #  $index->start_page($c->stash('search.start_page'));
-  #};
-
-
-  # Create remote request URL
-  my $url = Mojo::URL->new($c->api);
-  $url->path('search');
-  $url->query(\%query);
-
-  # Check if total results is cached
-  my $total_results = -1;
-  unless ($c->no_cache) {
-
-    # Get total results value
-    $total_results = $c->chi->get('total-' . $user . '-' . $url->to_string);
-
-    # Set stash if cache exists
-    $c->stash(total_results => $total_results) if $total_results;
-    $c->app->log->debug('Get total result from cache');
-
-    # Set cutoff unless already set
-    $url->query({cutoff => 'true'});
-  };
-
-  # Establish 'search_results' taghelper
-  # This is based on Mojolicious::Plugin::Search
-  $c->app->helper(
-    search_results => sub {
-      my $c = shift;
-
-      # This is a tag helper for templates
-      my $cb = shift;
-      if (!ref $cb || !(ref $cb eq 'CODE')) {
-        $c->app->log->error('search_results expects a code block');
-        return '';
-      };
-
-      my $coll = $c->stash('results');
-
-      # Iterate over results
-      my $string = $coll->map(
-        sub {
-          # Call hit callback
-          # $c->stash('search.hit' => $_);
-          local $_ = $_[0];
-          return $cb->($_);
-        })->join;
-
-      # Remove hit from stash
-      # delete $c->stash->{'search.hit'};
-      return b($string);
-    }
-  );
-
-  # Check if the request is cached
-  my $url_string = $url->to_string;
-
-  # Set api request for debugging
-  $c->stash('api_request' => $url_string);
-
-  # Debugging
-  $c->app->log->debug('Search for ' . $url_string);
-
-  # Check for cache
-  my $json = $c->chi->get('matches-' . $user . '-' . $url_string);
-
-  # Initialize promise object
-  my $promise;
-
-  # Result is cached
-  if ($json) {
-    $json->{cached} = 'true';
-
-    # The promise is already satisfied by the cache
-    $promise = Mojo::Promise->new->resolve($json);
-  }
-
-  # Retrieve from URL
-  else {
-
-    # Wrap a user agent method with a promise
-    $promise = $c->user->auth_request_p(get => $url)
-
-      # TODO: Better use a single then
-      ->then(\&_catch_http_errors)
-      ->then(
-        sub {
-          my $json = shift->json;
-
-          unless ($json) {
-            return Mojo::Promise->new->reject([
-              [undef, 'JSON response is invalid']
-            ]);
-          };
-
-          $c->stash('api_response' => $json);
-          return $json;
-        })
-      ->then(\&_catch_koral_errors)
-      ;
-  };
-
-  # Wait for rendering
-  $c->render_later;
-
-  # Choose the snippet based on the parameter
-  my $template = scalar $v->param('snippet') ? 'snippet' : 'search2';
-  $c->stash(template => $template);
-
-  # Process response
-  $promise->then(
-    sub {
-      my $json = shift;
-
-      # Prepare warnings
-      $c->_notify_on_warnings($json->{warnings}) if $json->{warnings};
-
-      # Cache total results
-      # The stash is set in case the total results value is from the cache,
-      # so in that case, it does not need to be cached again
-      my $total_results = $c->stash('total_results');
-      if (!$total_results) {
-
-        # There are results to remember
-        if ($json->{meta}->{totalResults} >= 0) {
-
-          # Remove cutoff requirement again
-          $url->query([cutoff => 'true']);
-
-          $total_results = $json->{meta}->{totalResults};
-          $c->stash(total_results => $total_results);
-
-          # Set cache
-          $c->chi->set(
-            'total-' . $user . '-' . $url->to_string => $total_results
-          );
-        };
-      }
-      else {
-        $c->stash(total_results => -1);
-      }
-
-      $c->stash(total_pages => 0);
-
-      # Set total pages
-      # From Mojolicious::Plugin::Search::Index
-      if ($total_results > 0) {
-        $c->stash(total_pages => ceil($total_results / ($c->stash('items_per_page') || 1)));
-      };
-
-      # Cache result
-      $c->chi->set('matches-' . $user . '-' . $url_string => $json);
-
-      # Process match results
-      return $c->_process_matches($json);
-    }
-
-  # Render template
-  )->then(
-    sub {
-      return $c->render(
-        q => $query,
-        ql => $query{ql},
-        start_page => $query{p},
-      );
-    }
-
-  # Deal with errors
-  )->catch(
-    sub {
-      $c->_notify_on_errors(shift);
-      return $c->render(
-        results => c(),
-        q => $query,
-        ql => $query{ql},
-        start_page => 1
-      );
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-
-  return 1;
-};
-
-
-sub match_info {
-  my $c = shift;
-
-  # Validate user input
-  my $v = $c->validation;
-  $v->optional('foundry');
-  $v->optional('layer');
-  $v->optional('spans')->in(qw/true false/);
-
-  # Old API foundry/layer usage
-  my $foundry = '*';
-  my %query = (foundry => '*');
-  if ($v->param('foundry')) {
-    $query{foundry} = $v->param('foundry');
-    $query{layer} = $v->param('layer') if $v->param('layer');
-    $query{spans} = $v->param('spans') if $v->param('spans');
-  };
-
-  # Create new request API
-  my $url = Mojo::URL->new($c->api);
-
-  # Use stash information to create url path
-  $url->path(
-    join('/', (
-      'corpus',
-      $c->stash('corpus_id'),
-      $c->stash('doc_id'),
-      $c->stash('text_id'),
-      $c->stash('match_id'),
-      'matchInfo'
-    ))
-  );
-
-  # Set query parameters
-  $url->query(\%query);
-
-  $c->app->log->debug('Text info: ' . $url);
-
-  $c->render_later;
-
-  # TODO: Add caching!
-  $c->user->auth_request_p(
-    get => $url
-  )
-  ->then(\&_catch_http_errors)
-  ->then(
-    sub {
-      my $json = shift->json;
-      unless ($json) {
-        return Mojo::Promise->new->reject([
-          [undef, 'JSON response is invalid']
-        ]);
-      };
-
-      $c->stash(results => _map_match($json));
-#      $c->stash(results => $json);
-      return $json;
-    }
-  )
-  ->then(
-    \&_catch_koral_errors
-  )
-  # Deal with errors
-  ->catch(
-    sub {
-      $c->_notify_on_errors(shift);
-    }
-  )
-
-  ->finally(
-    sub {
-      # Add notifications to the matching json
-      # TODO: There should be a special notification engine doing that!
-
-      my $json = $c->stash('results');
-      my $notes = $c->notifications(json => $json);
-      return $c->render(
-        json => $notes,
-        status => 200
-      );
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-  return 1;
-};
-
 1;
 
 
 __END__
-
-  # Async
-  $c->render_later;
-
-  # Use the API for fetching matching information non-blocking
-  $c->search->match(
-    corpus_id => $c->stash('corpus_id'),
-    doc_id    => $c->stash('doc_id'),
-    text_id   => $c->stash('text_id'),
-    match_id  => $c->stash('match_id'),
-    %query,
-
-    # Callback for async search
-    sub {
-      my $index = shift;
-      return $c->respond_to(
-
-        # Render json if requested
-        json => sub {
-          # Add notifications to the matching json
-          # TODO: There should be a special notification engine doing that!
-          my $notes = $c->notifications(json => $index->results->[0]);
-          $c->render(
-            json => $notes,
-            status => $index->status
-          );
-        },
-
-        # Render html if requested
-        html => sub {
-          return $c->render(
-            layout   => 'default',
-            template => 'match_info'
-          )
-        }
-      );
-    }
-  );
diff --git a/lib/Kalamar/Plugin/KalamarErrors.pm b/lib/Kalamar/Plugin/KalamarErrors.pm
new file mode 100644
index 0000000..fd4a174
--- /dev/null
+++ b/lib/Kalamar/Plugin/KalamarErrors.pm
@@ -0,0 +1,117 @@
+package Kalamar::Plugin::KalamarErrors;
+use Mojo::Base 'Mojolicious::Plugin';
+
+
+# Register error plugin
+sub register {
+  my ($plugin, $mojo) = @_;
+
+
+  # Notify on warnings
+  $mojo->helper(
+    notify_on_warnings => sub {
+      my ($c, $json) = @_;
+
+      my $warnings = $json->{warnings};
+
+      return unless $warnings;
+
+      # TODO: Check for ref!
+      foreach my $w (@$warnings) {
+        $c->notify(
+          warn =>
+            ($w->[0] ? $w->[0] . ': ' : '') .
+            $w->[1]
+          );
+      };
+
+      return 1;
+    }
+  );
+
+  # Notify on errors
+  $mojo->helper(
+    notify_on_errors => sub {
+      my ($c, $json) = @_;
+
+      my $errors = $json->{errors};
+
+      return unless $errors;
+
+      foreach my $e (@$errors) {
+        $c->notify(
+          error =>
+            ($e->[0] ? $e->[0] . ': ' : '') .
+            ($e->[1] || 'Unknown')
+          );
+      };
+
+      return 1;
+    }
+  );
+
+  # Catch connection errors
+  $mojo->helper(
+    catch_errors_and_warnings => sub {
+      my ($c, $tx) = @_;
+
+      my $err = $tx->error;
+
+      if ($err && $err->{code} != 500) {
+        $c->stash(status => $err->{code});
+      };
+
+      # Check the response
+      my $res = $tx->res;
+      my $json;
+      $json = $res->json if $res->body;
+
+      # There is no json and no error
+      if (!$json && !$err) {
+
+        $c->notify(error => 'JSON response is invalid');
+        return Mojo::Promise->new->reject;
+      };
+
+      # There is json
+      if ($json) {
+        $c->stash(api_response => $json);
+
+        # TODO:
+        #   Check for references of errors and warnings!
+
+        # There are errors
+        if ($c->notify_on_errors($json)) {
+
+          # Return on errors - ignore warnings
+          return Mojo::Promise->new->reject;
+        };
+
+        # Notify on warnings
+        $c->notify_on_warnings($json);
+
+        # What does status mean?
+        if ($json->{status}) {
+
+          $c->notify(error => 'Middleware error ' . $json->{'status'});
+          return Mojo::Promise->new->reject;
+        };
+      }
+
+      # There is an error but no json
+      else {
+
+        # Send rejection promise
+        $c->notify(error => $err->{code} . ': ' . $err->{message});
+        return Mojo::Promise->new->reject;
+      };
+
+      return $json;
+    }
+  );
+};
+
+
+
+
+1;
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index 0fee4c8..b6a67c9 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -240,6 +240,42 @@
       $c->stash('kalamar.test_port' => 0);
       return 0;
     });
+
+  # Establish 'search_results' taghelper
+  # This is based on Mojolicious::Plugin::Search
+  $mojo->helper(
+    search_results2 => sub {
+      my $c = shift;
+
+      # This is a tag helper for templates
+      my $cb = shift;
+      if (!ref $cb || !(ref $cb eq 'CODE')) {
+        $c->app->log->error('search_results expects a code block');
+        return '';
+      };
+
+      my $coll = $c->stash('results');
+
+      # Iterate over results
+      my $string = $coll->map(
+        sub {
+          # Call hit callback
+          # $c->stash('search.hit' => $_);
+          local $_ = $_[0];
+          return $cb->($_);
+        })->join;
+
+      # Remove hit from stash
+      # delete $c->stash->{'search.hit'};
+      return b($string);
+    }
+  );
+
+  $mojo->helper(
+    'korap.api' => sub {
+      return shift->config('Search')->{api};
+    }
+  );
 };
 
 
diff --git a/t/match_info.t b/t/match_info.t
index 3e4b72c..1b6f8ef 100644
--- a/t/match_info.t
+++ b/t/match_info.t
@@ -50,7 +50,7 @@
 $t->get_ok('/corpus2/fail/x/x/p0-0')
   ->status_is(200)
   ->json_is('/notifications/0/0', 'error')
-  ->json_is('/notifications/0/1', '500: Internal Server Error')
+  ->json_is('/notifications/0/1', 'Unable to load query response from /home/ndiewald/Repositories/korap-git/Kalamar/t/fixtures/response_matchinfo_fail_x_x_p0-0.json')
   ;
 
 # TODO:
@@ -64,10 +64,11 @@
 # TODO:
 #   It's surprising, that it doesn't return a 404!
 $t->get_ok('/corpus2/notfound2/X/X/p0-1')
-  ->status_is(200)
+  ->status_is(404)
   ->json_is('/notifications/0/0', 'error')
   ->json_is('/notifications/0/1', '404: Not Found')
   ;
 
 
 done_testing;
+__END__
diff --git a/t/query.t b/t/query.t
index bf82ba2..4bc97dc 100644
--- a/t/query.t
+++ b/t/query.t
@@ -67,4 +67,11 @@
   ->text_is('li:nth-of-type(1) p.ref span.sigle', '[GOE/AGI/00000]')
   ;
 
+
+$t->get_ok('/q2?q=[orth=das')
+  ->status_is(400)
+  ->text_is('div.notify-error:nth-of-type(1)', '302: Parantheses/brackets unbalanced.')
+  ->text_like('div.notify-error:nth-of-type(2)', qr!302: Could not parse query .+? \[orth=das.+?!)
+  ;
+
 done_testing;
diff --git a/templates/search2.html.ep b/templates/search2.html.ep
index 200d693..5a72ca5 100644
--- a/templates/search2.html.ep
+++ b/templates/search2.html.ep
@@ -1,8 +1,7 @@
 % layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
-
 <div id="resultinfo" <% if (stash('results')->size) { %> class="found"<%} %>>
   <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
-% my $found = stash('total_results');
+% my $found = stash('total_results') // 0;
   <p class="found">\
 % if ($found != -1) {
 %   my $found_text = loc('numf', number => $found);
@@ -20,9 +19,9 @@
 %= include 'query2'
 
 <div id="search">
-% if (stash('total_results') != 0 && stash('results')->size) {
+% if (stash('results')->size && stash('total_results') != 0) {
   <ol class="align-left">
-%=  search_results begin
+%=  search_results2 begin
 %=    include 'match', match => $_
 %   end
   </ol>