In Mojolicious, what's the correct way to render a response from a Promise? In my code below, I get:
[2023-04-12] [trace] Template "example/search.html.ep" not found
[2023-04-12] [trace] Nothing has been rendered, expecting delayed response
example/search.html.ep
? I didn't ask for it.My sample app has 3 files:
script/my_app
#!/usr/bin/env perl
use strict;
use warnings;
use Mojo::File qw(curfile);
use lib curfile->dirname->sibling('lib')->to_string;
use Mojolicious::Commands;
Mojolicious::Commands->start_app('MyApp');
lib/MyApp.pm
package MyApp;
use Mojo::Base 'Mojolicious', -signatures;
sub startup ($self) {
$self->secrets('s3cret');
my $r = $self->routes;
$r->get('/')->to('Example#index');
$r->post('/')->to('Example#search');
}
1;
lib/MyApp/Controller/Example.pm
package MyApp::Controller::Example;
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub index ($self)
{
return $self->render(inline => '<html><body><form method="post"><textarea name="numbers" maxlength="11">123</textarea><button type="submit">Go</button></form></body></html>');
}
sub search ($self)
{
my $v = $self->validation;
$v->required("numbers");
return $self->render(text=>"Validation Error") if $v->has_error;
my $numbers = $v->param("numbers");
my @numbers = split(/\r?\n/, $numbers);
Mojo::Promise
->map(
{concurrency => 2},
sub {
$self->ua->get_p("https://httpbin.org/delay/1?q=$_" => {'api-key'=>'shhh'});
}, @numbers)
->then(
sub{
my @results = @_;
my @json = map { $_->[0]->res->json } @results;
return $self->render(json => \@json);
})
->catch(
sub {
my $err = shift;
return $self->render(text => $err);
})
->wait;
#return $self->render(text => "This shall result is 'Unhandled rejected promise: A response has already been rendered'");
}
1;
To run the app, I do:
morbo script/my_app
then navigate to http://localhost:3000
and post the form. Multiple lines will result in concurrent calls.
Add a render_later
before invoking the Promise:
$self->render_later();
Mojo::Promise
->map(
{concurrency => 2},
...
Without it, when the function search
exits, Mojolicious will by default render the associated template (example/search.html.ep
), and fail because this template doesn't exist (see Automatic rendering in Mojolicious::Guide::Rendering). The render_later
disables this automatic rendering, basically telling Mojolicious "keep the connection open, I'll render something later".
Your confusion may come from the use of wait
, which doesn't really "wait" in this context. If you add a print before and after the whole promise thing:
say "Before";
Mojo::Promise->map(...);
say "After";
Then the log will be:
[2023-04-13 09:23:08.78601] [1201283] [trace] [rR7TxcCsrygT] POST "/"
[2023-04-13 09:23:08.78692] [1201283] [trace] [rR7TxcCsrygT] Routing to controller "MyApp::Controller::Example" and action "search"
Before
After
[2023-04-13 09:23:08.78953] [1201283] [trace] [rR7TxcCsrygT] Template "example/search.html.ep" not found
...
Which does show that the wait
didn't really wait (if you're not convinced, you can even add prints in the then
and catch
: they'll be printed after After
).
Looking at the documentation of wait
, it says:
Start "ioloop" and stop it again once the promise has been fulfilled or rejected, does nothing when "ioloop" is already running.
Here, since you are running a server, the IOLoop is always running, so wait
does nothing. This is actually desirable: if wait
were to actually wait, then there would be no point in using a Promise.
On the other hand, when you use Mojo::Promise on its own (ie, not in a Mojolicious server), you might need to manually run the IOLoop with wait
in order to not exit your script before your promise is finished.