Skip to content

Commit a66d9d6

Browse files
committed
Adjust docs and tests regarding PAGI distro split
1 parent 1e78eb5 commit a66d9d6

9 files changed

Lines changed: 174 additions & 20 deletions

File tree

Changes

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Revision history for Thunderhorse
22

33
{{$NEXT}}
4+
[Public interface changes]
5+
- Added connection attribute to Context
6+
- Added empty_res, send_res, try_send_res methods to Context
7+
- Added is_ready method to Response
8+
9+
[Breaking changes]
10+
- Response is now a value (via PAGI::Tools change)
11+
- awaiting response's calls which used to send data like "text", "html" etc. is now wrong - remove "await" calls
12+
- Thunderhorse dependencies no longer ensure pagi-server will be installed (via PAGI distribution split)
13+
- pagi-server is now a part of a separate PAGI::Server module, which is not a direct dependency for Thunderhorse
414

515
0.105 - 2026-05-25
616
[Changes]

lib/Thunderhorse.pm

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ sub build_handler ($controller, $destination)
110110
}
111111
}
112112

113-
# this needs to be here, since we want to use $send from this context
113+
# NOTE: this method will not send the response if we already sent or if
114+
# the response is not ready. It does not check whether the context is
115+
# consumed altogether, so a manually consumed context with a non-ready
116+
# response will not send anything, likely rendering some kind of error
117+
# page (but not 404). Currently, a PAGI error is raised, informing
118+
# about app returning without sending respnose.
119+
# NOTE: this needs to be here, since we want to use $send from this context
114120
await $ctx->try_send_res;
115121

116122
# if this is a bridge and bridge did not render, it means we are
@@ -217,6 +223,10 @@ Scripts must be called using C<pagi-server> (or other PAGI-specific software).
217223
Running the script using C<perl> does nothing, as the application cannot run
218224
itself - it will be built, but it will not set up a webserver.
219225
226+
C<pagi-server> can be obtained separately from L<PAGI::Server> module.
227+
Thunderhorse does not automatically include PAGI::Server as its dependency,
228+
since it is not coupled with any specific PAGI server implementation.
229+
220230
=back
221231
222232
=head2 The thunderhorse script
@@ -647,7 +657,7 @@ would expect.
647657
One unique feature of Thunderhorse is that it does not stop searching for
648658
matches once it finds a match. Instead, it gathers a list of matching locations
649659
and then proceeds to execute them in order. It stops once one of the handlers
650-
consumes the context, which is usually done by sending a response. If no
660+
consumes the context, which is usually done by setting a response body. If no
651661
handlers consumed the context, a I<404 Not Found> error page is rendered.
652662
653663
This allows for superb flexibility, but has a couple of interesting side
@@ -768,6 +778,30 @@ matching. If we let C<important_auth> run before C<login_page>, for example by
768778
setting its order to C<-2>, it will effectively become a bridge for
769779
C<login_page>.
770780
781+
Currently, the context can be consumed by:
782+
783+
=over
784+
785+
=item * Setting the response body in L<Thunderhorse::Context/res>
786+
787+
=item * Setting the response status to one of the statuses which does not require a body
788+
789+
=item * Closing a websocket connection in L<Thunderhorse::Context/ws>
790+
791+
=item * Closing a sse connection in L<Thunderhorse::Context/sse>
792+
793+
=item * Manually sending any response via PAGI, triggering C<response_started> in C<pagi.connection> scope key
794+
795+
=item * Manually consuming the context via L<Thunderhorse::Context/consume> call
796+
797+
=back
798+
799+
This system should be pretty bulletproof, however once you use the last option
800+
and call L<Thunderhorse::Context/consume>, all safety measures are off - it's
801+
now your responsibility to make sure the route handler will render something
802+
eventually. If it doesn't, you will get a low-level PAGI exception and an error
803+
page completely bypassing any Thunderhorse error rendering. Use with caution.
804+
771805
=head2 Controllers
772806
773807
By default, all routes defined in the application's C<build> method belong to

lib/Thunderhorse/Context.pm

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ used, or other middleware that will populate C<pagi.session> scope key.
239239
L<PAGI::Session> will throw an exception if C<pagi.session> is not present in
240240
scope.
241241
242+
=head3 connection
243+
244+
Connection object for this context, taken from PAGI scope. Connection objects
245+
are inserted into the scope by the server, and there is no clear subclass they
246+
will inherit from. They follow a duck-typed interface though, and can be
247+
trusted to have at least C<response_started> method.
248+
242249
=head2 Methods
243250
244251
=head3 new
@@ -289,6 +296,31 @@ Returns true if the context has been consumed either explicitly via
289296
L</consume>, or implicitly by sending a response, closing a WebSocket
290297
connection, or closing an SSE stream.
291298
299+
=head3 empty_res
300+
301+
$new_res = $ctx->empty_res()
302+
303+
Discards the current response attached to the context, then builds and returns
304+
a fresh one. Useful if you want to completely discard response data.
305+
306+
=head3 send_res
307+
308+
await $ctx->send_res()
309+
310+
Tries to send the response back to the client. Dies if the response has already
311+
been sent. Force-sends the response even if it has an empty body.
312+
313+
=head3 try_send_res
314+
315+
$ctx->try_send_res()
316+
317+
Similar as L</send_res>, but does nothing if the response has already been
318+
sent. Also skips sending the response if it is not ready yet, according to
319+
L<Thunderhorse::Response/is_ready>.
320+
321+
Note that you don't have to use this method explicitly. Thunderhorse will
322+
automatically use it after the route handler returns.
323+
292324
=head1 SEE ALSO
293325
294326
L<Thunderhorse>, L<Gears::Context>, L<Thunderhorse::Request>,

lib/Thunderhorse/Request.pm

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ constructor arguments.
8989
9090
=head3 update
9191
92-
$req->update()
92+
$req->update($scope, $receive, $send)
9393
94-
Updates the internal PAGI scope and receiver from the context's PAGI tuple.
95-
Called automatically when the context's PAGI tuple changes via
96-
setter of L<Thunderhorse::Context/pagi>.
94+
Updates the internal PAGI scope and receiver. Called automatically when the
95+
context's PAGI tuple changes via setter of L<Thunderhorse::Context/pagi>.
9796
9897
=head1 SEE ALSO
9998

lib/Thunderhorse/Response.pm

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Thunderhorse::Response - Response wrapper for Thunderhorse
5858
5959
Thunderhorse::Response is a thin wrapper around L<PAGI::Response> that
6060
integrates with L<Thunderhorse::Context>. It provides a fluent interface for
61-
building and sending HTTP responses, including JSON, HTML, redirects, and file
61+
building HTTP responses, including JSON, HTML, redirects, and file
6262
downloads.
6363
6464
This class extends L<PAGI::Response> and mixes in C<Thunderhorse::Message> to
@@ -88,11 +88,21 @@ constructor arguments.
8888
8989
=head3 update
9090
91-
$res->update()
91+
$res->update($scope, $receive, $send)
9292
93-
Updates the internal PAGI scope and sender from the context's PAGI tuple.
94-
Called automatically when the context's PAGI tuple changes via
95-
setter of L<Thunderhorse::Context/pagi>.
93+
Updates the internal PAGI scope. Called automatically when the context's PAGI
94+
tuple changes via setter of L<Thunderhorse::Context/pagi>.
95+
96+
=head3 is_ready
97+
98+
$bool = $res->is_ready()
99+
100+
Returns whether this response is ready as far as Thunderhorse is concerned.
101+
Responses which are ready will cause the context to become consumed after the
102+
route handler returns.
103+
104+
Response is ready if it has a body, or if it has a status which does not
105+
require body like C<204 No Content> or C<3XX>.
96106
97107
=head1 SEE ALSO
98108

lib/Thunderhorse/SSE.pm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ constructor arguments.
7676
7777
=head3 update
7878
79-
$sse->update()
79+
$sse->update($scope, $receive, $send)
8080
81-
Updates the internal PAGI scope, receiver, and sender from the context's PAGI
82-
tuple. Called automatically when the context's PAGI tuple changes via
83-
setter of L<Thunderhorse::Context/pagi>.
81+
Updates the internal PAGI scope, receiver, and sender. Called automatically
82+
when the context's PAGI tuple changes via setter of
83+
L<Thunderhorse::Context/pagi>.
8484
8585
=head1 SEE ALSO
8686

lib/Thunderhorse/WebSocket.pm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ constructor arguments.
7676
7777
=head3 update
7878
79-
$ws->update()
79+
$ws->update($scope, $receive, $send)
8080
81-
Updates the internal PAGI scope, receiver, and sender from the context's PAGI
82-
tuple. Called automatically when the context's PAGI tuple changes via
83-
setter of L<Thunderhorse::Context/pagi>.
81+
Updates the internal PAGI scope, receiver, and sender. Called automatically
82+
when the context's PAGI tuple changes via setter of
83+
L<Thunderhorse::Context/pagi>.
8484
8585
=head1 SEE ALSO
8686

t/edge.t

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ package EdgeApp {
4444
$r->add('/only_post' => {to => 'print_method', action => 'http.post'});
4545

4646
$r->add('/future' => {to => 'return_future'});
47+
48+
$r->add('/bad_consume' => {to => 'bad_consume'});
4749
}
4850

4951
sub stash_and_return ($self, $ctx, $msg)
@@ -68,6 +70,12 @@ package EdgeApp {
6870
{
6971
return $ctx->res->text('return text without await');
7072
}
73+
74+
sub bad_consume ($self, $ctx)
75+
{
76+
# this consumes the context without sending anything
77+
$ctx->consume;
78+
}
7179
};
7280

7381
my $app = EdgeApp->new;
@@ -130,5 +138,14 @@ subtest 'returning future from handler should work' => sub {
130138
http_text_is 'return text without await';
131139
};
132140

141+
subtest 'should throw an exception when the context is consumed without sending anything' => sub {
142+
my $ex = dies {
143+
http $app, GET '/bad_consume';
144+
fail "call succeeded with error " . http->status;
145+
};
146+
147+
like $ex, qr{App returned without sending response}, 'exception ok';
148+
};
149+
133150
done_testing;
134151

t/facade.t

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package FacadeApp::Controller::Test::Facade {
1717
{
1818
await $self->app->loop->delay_future(after => 0.5);
1919
$self->res->text('Something');
20-
await $self->send_res;
2120
}
2221
}
2322

@@ -47,6 +46,38 @@ package FacadeApp::Controller::Test {
4746
}
4847
);
4948

49+
# ditto, but return a promise instead of awaiting
50+
$router->add(
51+
'/good2' => {
52+
to => sub ($self, $ctx) {
53+
return $ctx->send_something_later;
54+
}
55+
}
56+
);
57+
58+
# this is also good, because calling send_res explicitly should not
59+
# hurt TH as a whole.
60+
$router->add(
61+
'/good_send' => {
62+
to => async sub ($self, $ctx) {
63+
await $ctx->send_something_later;
64+
await $ctx->send_res;
65+
}
66+
}
67+
);
68+
69+
# ditto, but less confidently
70+
$router->add(
71+
'/good_send2' => {
72+
to => async sub ($self, $ctx) {
73+
$ctx->consume;
74+
await $ctx->send_something_later;
75+
await $ctx->try_send_res;
76+
return;
77+
}
78+
}
79+
);
80+
5081
# this is bad, because it consumes the context explicitly but does not
5182
# await. This behavior is wrong on PAGI level, which forces fully
5283
# rendered response before server finishes handling the app
@@ -92,6 +123,27 @@ subtest 'should render /good' => sub {
92123
http_text_is 'Something';
93124
};
94125

126+
subtest 'should render /good2' => sub {
127+
http $app, GET '/good2';
128+
http_status_is 200;
129+
http_header_is 'Content-Type', 'text/plain; charset=utf-8';
130+
http_text_is 'Something';
131+
};
132+
133+
subtest 'should render /good_send' => sub {
134+
http $app, GET '/good_send';
135+
http_status_is 200;
136+
http_header_is 'Content-Type', 'text/plain; charset=utf-8';
137+
http_text_is 'Something';
138+
};
139+
140+
subtest 'should render /good_send2' => sub {
141+
http $app, GET '/good_send2';
142+
http_status_is 200;
143+
http_header_is 'Content-Type', 'text/plain; charset=utf-8';
144+
http_text_is 'Something';
145+
};
146+
95147
subtest 'should not render /consumed' => sub {
96148
like dies {
97149
http $app, GET '/consumed';

0 commit comments

Comments
 (0)