-module(livery_timeout). -moduledoc """ Per-request deadline middleware. State: `#{after_ms => Ms}`. If the rest of the pipeline does not return within the deadline, the worker is killed and a 504 is emitted instead. A handler crash maps to 500. The deadline is enforced by running the downstream call in a spawned, monitored process. Body chunks (`livery_body:read/2`) are delivered to the request process, not this spawned child, so a handler that streams its request body will not see those chunks under this middleware. Pair `livery_timeout` with a body-buffering middleware in front of it, or apply it only to routes whose handlers do not stream input. """. -behaviour(livery_middleware). -export([call/3]). -doc "Enforce the deadline. Crashes map to 500, timeouts to 504.". -spec call( livery_req:req(), livery_middleware:next(), #{after_ms := pos_integer()} ) -> livery_resp:resp(). call(Req, Next, #{after_ms := Ms}) when is_integer(Ms), Ms > 0 -> Self = self(), Ref = make_ref(), {Pid, MRef} = spawn_monitor(fun() -> try Self ! {Ref, {ok, Next(Req)}} catch Class:Reason:Stack -> Self ! {Ref, {crash, Class, Reason, Stack}} end end), receive {Ref, {ok, Resp}} -> erlang:demonitor(MRef, [flush]), Resp; {Ref, {crash, _Class, _Reason, _Stack}} -> erlang:demonitor(MRef, [flush]), livery_resp:text(500, <<"internal server error">>); {'DOWN', MRef, process, Pid, _Reason} -> livery_resp:text(500, <<"internal server error">>) after Ms -> exit(Pid, kill), erlang:demonitor(MRef, [flush]), livery_resp:text(504, <<"request timeout">>) end.